197 lines
6.3 KiB
TypeScript
197 lines
6.3 KiB
TypeScript
// src/service/user.service.ts
|
|
import { Body, httpError, Inject, Provide } from '@midwayjs/core';
|
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
|
import { Like, Repository } from 'typeorm';
|
|
import * as bcrypt from 'bcryptjs';
|
|
import { JwtService } from '@midwayjs/jwt';
|
|
import { User } from '../entity/user.entity';
|
|
import { LoginResDTO } from '../dto/user.dto';
|
|
import { plainToInstance } from 'class-transformer';
|
|
import { MailService } from './mail.service';
|
|
import { AuthCodeService } from './authCode.service';
|
|
import { DeviceWhitelistService } from './deviceWhitelist.service';
|
|
|
|
@Provide()
|
|
export class UserService {
|
|
@InjectEntityModel(User)
|
|
userModel: Repository<User>;
|
|
|
|
@Inject()
|
|
jwtService: JwtService;
|
|
|
|
@Inject()
|
|
mailService: MailService;
|
|
|
|
@Inject()
|
|
authCodeService: AuthCodeService;
|
|
|
|
@Inject()
|
|
deviceWhitelistService: DeviceWhitelistService;
|
|
|
|
async requestAuthCode(deviceId: string) {
|
|
const code = await this.authCodeService.generateCode(deviceId);
|
|
|
|
await this.mailService.sendMail(
|
|
'info@canpouches.com',
|
|
'Your Login Authorization Code',
|
|
`Your authorization code is: ${code}, valid for 10 minutes.`
|
|
);
|
|
}
|
|
|
|
async login(@Body() body): Promise<LoginResDTO> {
|
|
const { username, password, deviceId, authCode } = body;
|
|
|
|
const user = await this.userModel.findOne({
|
|
where: { username, isActive: true },
|
|
});
|
|
if (!user || !(await bcrypt.compare(password, user.password))) {
|
|
throw new Error('用户名或者密码错误');
|
|
}
|
|
const isWhite = await this.deviceWhitelistService.isWhitelisted(deviceId);
|
|
if (!isWhite) {
|
|
if (!authCode) {
|
|
await this.requestAuthCode(deviceId);
|
|
const err = new httpError.BadRequestError('非白名单设备请填写验证码');
|
|
(err as any).code = 10001; // 添加业务错误码
|
|
throw err;
|
|
}
|
|
|
|
const valid = await this.authCodeService.verifyCode(deviceId, authCode);
|
|
if (!valid) {
|
|
throw new Error('验证码错误');
|
|
}
|
|
|
|
// 校验通过后,将设备加入白名单
|
|
await this.deviceWhitelistService.addToWhitelist(deviceId);
|
|
}
|
|
|
|
// 生成 JWT,包含角色和权限信息
|
|
const token = await this.jwtService.sign({
|
|
id: user.id,
|
|
deviceId,
|
|
username: user.username,
|
|
isSuper: user.isSuper,
|
|
});
|
|
|
|
return {
|
|
token, //role: user.role,
|
|
username: user.username,
|
|
userId: user.id,
|
|
permissions: user.permissions,
|
|
};
|
|
}
|
|
|
|
// 新增用户(支持可选备注)
|
|
async addUser(username: string, password: string, remark?: string) {
|
|
const existingUser = await this.userModel.findOne({
|
|
where: { username },
|
|
});
|
|
if (existingUser) {
|
|
throw new Error('用户已存在');
|
|
}
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
const user = this.userModel.create({
|
|
username,
|
|
password: hashedPassword,
|
|
// 备注字段赋值(若提供)
|
|
...(remark ? { remark } : {}),
|
|
});
|
|
return this.userModel.save(user);
|
|
}
|
|
|
|
// 用户列表支持分页与备注模糊查询(以及可选的布尔过滤)
|
|
async listUsers(
|
|
current: number,
|
|
pageSize: number,
|
|
filters: {
|
|
remark?: string;
|
|
username?: string;
|
|
isActive?: boolean;
|
|
isSuper?: boolean;
|
|
isAdmin?: boolean;
|
|
} = {},
|
|
sorter: {
|
|
field?: string;
|
|
order?: 'ASC' | 'DESC';
|
|
} = {}
|
|
) {
|
|
// 条件判断:构造 where 条件
|
|
const where: Record<string, any> = {};
|
|
if (filters.username) where.username = Like(`%${filters.username}%`); // 用户名精确匹配(如需模糊可改为 Like)
|
|
if (typeof filters.isActive === 'boolean') where.isActive = filters.isActive; // 按启用状态过滤
|
|
if (typeof filters.isSuper === 'boolean') where.isSuper = filters.isSuper; // 按超管过滤
|
|
if (typeof filters.isAdmin === 'boolean') where.isAdmin = filters.isAdmin; // 按管理员过滤
|
|
if (filters.remark) where.remark = Like(`%${filters.remark}%`); // 备注模糊搜索
|
|
|
|
const validSortFields = ['id', 'username', 'isActive', 'isSuper', 'isAdmin', 'remark'];
|
|
const sortField = validSortFields.includes(sorter.field) ? sorter.field : 'id';
|
|
const sortOrder = sorter.order === 'ASC' ? 'ASC' : 'DESC';
|
|
|
|
const [items, total] = await this.userModel.findAndCount({
|
|
where,
|
|
skip: (current - 1) * pageSize,
|
|
take: pageSize,
|
|
order: { [sortField]: sortOrder },
|
|
});
|
|
return { items, total, current, pageSize };
|
|
}
|
|
|
|
async toggleUserActive(userId: number, isActive: boolean) {
|
|
const user = await this.userModel.findOne({ where: { id: userId } });
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
user.isActive = isActive;
|
|
return this.userModel.save(user);
|
|
}
|
|
|
|
// 更新用户信息(支持用户名唯一校验与可选密码修改)
|
|
async updateUser(
|
|
userId: number,
|
|
payload: {
|
|
username?: string;
|
|
password?: string;
|
|
isSuper?: boolean;
|
|
isAdmin?: boolean;
|
|
permissions?: string[];
|
|
remark?: string;
|
|
}
|
|
) {
|
|
// 条件判断:查询用户是否存在
|
|
const user = await this.userModel.findOne({ where: { id: userId } });
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
// 条件判断:若提供了新用户名且与原用户名不同,校验唯一性
|
|
if (payload.username && payload.username !== user.username) {
|
|
const exist = await this.userModel.findOne({ where: { username: payload.username } });
|
|
if (exist) throw new Error('用户名已存在');
|
|
user.username = payload.username;
|
|
}
|
|
|
|
// 条件判断:若提供密码则进行加密存储
|
|
if (payload.password) {
|
|
user.password = await bcrypt.hash(payload.password, 10);
|
|
}
|
|
|
|
// 条件判断:更新布尔与权限字段(若提供则覆盖)
|
|
if (typeof payload.isSuper === 'boolean') user.isSuper = payload.isSuper;
|
|
if (typeof payload.isAdmin === 'boolean') user.isAdmin = payload.isAdmin;
|
|
if (Array.isArray(payload.permissions)) user.permissions = payload.permissions;
|
|
|
|
// 条件判断:更新备注(若提供则覆盖)
|
|
if (typeof payload.remark === 'string') user.remark = payload.remark;
|
|
|
|
// 保存更新
|
|
return await this.userModel.save(user);
|
|
}
|
|
|
|
async getUser(userId: number) {
|
|
return plainToInstance(
|
|
User,
|
|
await this.userModel.findOne({ where: { id: userId } })
|
|
);
|
|
}
|
|
}
|