mirror of
https://github.com/certd/certd.git
synced 2026-05-14 20:17:32 +08:00
perf(用户资料): 新增手机号邮箱绑定功能
实现用户邮箱和手机号的绑定与修改功能,包括: 1. 添加联系方式绑定API接口 2. 实现身份验证流程 3. 添加前端绑定对话框组件 4. 完善用户资料页面的联系方式展示和编辑入口 5. 添加联系方式冲突检测逻辑 6. 实现验证码校验功能
This commit is contained in:
@@ -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({});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { 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<UserEntity> {
|
||||
|
||||
|
||||
@InjectEntityModel(UserEntity)
|
||||
repository: Repository<UserEntity>;
|
||||
@Inject()
|
||||
@@ -44,10 +51,9 @@ export class UserService extends BaseService<UserEntity> {
|
||||
@Inject()
|
||||
dbAdapter: DbAdapter;
|
||||
|
||||
@Inject()
|
||||
@Inject()
|
||||
oauthBoundService: OauthBoundService;
|
||||
|
||||
|
||||
//@ts-ignore
|
||||
getRepository() {
|
||||
return this.repository;
|
||||
@@ -145,7 +151,7 @@ export class UserService extends BaseService<UserEntity> {
|
||||
return bcrypt.hashSync(plainPassword, salt);
|
||||
}
|
||||
|
||||
async findOne(param: Record<string,any>) {
|
||||
async findOne(param: Record<string, any>) {
|
||||
return this.repository.findOne({
|
||||
where: param,
|
||||
});
|
||||
@@ -177,12 +183,11 @@ export class UserService extends BaseService<UserEntity> {
|
||||
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) {
|
||||
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<UserEntity> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!user.username) {
|
||||
user.username = 'user_' + simpleNanoId();
|
||||
}
|
||||
@@ -235,7 +239,7 @@ export class UserService extends BaseService<UserEntity> {
|
||||
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<UserEntity> {
|
||||
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<UserEntity> {
|
||||
}
|
||||
|
||||
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<UserEntity> {
|
||||
where: {
|
||||
status: 1,
|
||||
},
|
||||
})
|
||||
});
|
||||
return users.map(item => item.id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user