feat: 增强安全性和用户隐私保护

主要改进:

1. JWT 密钥验证增强
   - 添加启动时密钥验证(长度、格式、弱密钥检测)
   - 确保密钥符合安全要求(至少32字符)

2. 资产加密算法升级
   - 从 AES-256-CBC 升级到 AES-256-GCM
   - 提供数据完整性校验(认证加密)
   - 添加加密密钥启动时验证

3. 用户隐私保护
   - 新增 findOnePublic 方法返回公开信息
   - GET /users/:id 根据查询对象返回不同信息级别
   - 查询自己返回完整信息,查询他人只返回公开信息

4. 缓存一致性修复
   - 修复更新用户信息时缓存未正确清除的问题
   - 确保完整信息和公开信息缓存同时失效

5. API 文档改进
   - 为用户查询接口添加详细的隐私保护说明

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
UGREEN USER
2026-01-28 13:02:55 +08:00
parent b25aa5b143
commit d73a6e28b3
4 changed files with 309 additions and 114 deletions

View File

@@ -1,8 +1,44 @@
import { registerAs } from '@nestjs/config'; import { registerAs } from "@nestjs/config";
export default registerAs('jwt', () => ({ const validateJwtSecret = (
secret: process.env.JWT_SECRET || 'default-secret', secret: string | undefined,
expiresIn: process.env.JWT_EXPIRES_IN || '7d', secretName: string,
refreshSecret: process.env.JWT_REFRESH_SECRET || 'default-refresh-secret', ): secret is string => {
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d', if (!secret) {
throw new Error(
`环境变量 ${secretName} 未设置。请在 .env 文件中配置强随机密钥(至少32字符)。`,
);
}
if (secret.length < 32) {
throw new Error(
`环境变量 ${secretName} 长度不足。当前长度: ${secret.length}, 要求至少32字符。请使用强随机密钥。`,
);
}
// 检查是否使用了默认密钥(明显的弱密钥)
const weakSecrets = [
"default-secret",
"default-refresh-secret",
"secret",
"jwt-secret",
];
if (weakSecrets.includes(secret.toLowerCase())) {
throw new Error(
`检测到 ${secretName} 使用了弱密钥: "${secret}"。请立即更换为强随机密钥(至少32字符)。`,
);
}
return true;
};
// 在加载配置前进行验证
const jwtSecret = process.env.JWT_SECRET;
const jwtRefreshSecret = process.env.JWT_REFRESH_SECRET;
validateJwtSecret(jwtSecret, "JWT_SECRET");
validateJwtSecret(jwtRefreshSecret, "JWT_REFRESH_SECRET");
export default registerAs("jwt", () => ({
secret: jwtSecret!,
expiresIn: process.env.JWT_EXPIRES_IN || "7d",
refreshSecret: jwtRefreshSecret!,
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || "30d",
})); }));

View File

@@ -3,21 +3,32 @@ import {
NotFoundException, NotFoundException,
ForbiddenException, ForbiddenException,
BadRequestException, BadRequestException,
} from '@nestjs/common'; } from "@nestjs/common";
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from "@nestjs/typeorm";
import { Repository, DataSource } from 'typeorm'; import { Repository, DataSource } from "typeorm";
import { Asset } from '../../entities/asset.entity'; import { Asset } from "../../entities/asset.entity";
import { AssetLog } from '../../entities/asset-log.entity'; import { AssetLog } from "../../entities/asset-log.entity";
import { Group } from '../../entities/group.entity'; import { Group } from "../../entities/group.entity";
import { GroupMember } from '../../entities/group-member.entity'; import { GroupMember } from "../../entities/group-member.entity";
import { CreateAssetDto, UpdateAssetDto, BorrowAssetDto } from './dto/asset.dto'; import {
import { AssetStatus, AssetLogAction, GroupMemberRole } from '../../common/enums'; CreateAssetDto,
import { ErrorCode, ErrorMessage } from '../../common/interfaces/response.interface'; UpdateAssetDto,
import * as crypto from 'crypto'; BorrowAssetDto,
} from "./dto/asset.dto";
import {
AssetStatus,
AssetLogAction,
GroupMemberRole,
} from "../../common/enums";
import {
ErrorCode,
ErrorMessage,
} from "../../common/interfaces/response.interface";
import * as crypto from "crypto";
@Injectable() @Injectable()
export class AssetsService { export class AssetsService {
private readonly ENCRYPTION_KEY = process.env.ASSET_ENCRYPTION_KEY || 'default-key-change-in-production'; private readonly ENCRYPTION_KEY: Buffer;
constructor( constructor(
@InjectRepository(Asset) @InjectRepository(Asset)
@@ -29,31 +40,91 @@ export class AssetsService {
@InjectRepository(GroupMember) @InjectRepository(GroupMember)
private groupMemberRepository: Repository<GroupMember>, private groupMemberRepository: Repository<GroupMember>,
private dataSource: DataSource, private dataSource: DataSource,
) {} ) {
// 在构造函数中验证并初始化加密密钥
this.ENCRYPTION_KEY = this.validateAndInitEncryptionKey();
}
/** /**
* 加密账号凭据 * 验证并初始化加密密钥
*/
private validateAndInitEncryptionKey(): Buffer {
const key = process.env.ASSET_ENCRYPTION_KEY;
// 验证密钥存在
if (!key) {
throw new Error(
"环境变量 ASSET_ENCRYPTION_KEY 未设置。请在 .env 文件中配置32字节十六进制格式的强随机密钥。" +
"可以使用以下命令生成: node -e \"console.log(crypto.randomBytes(32).toString('hex'))\"",
);
}
// 验证密钥格式
let keyBuffer: Buffer;
try {
keyBuffer = Buffer.from(key, "hex");
} catch (error) {
throw new Error(
"环境变量 ASSET_ENCRYPTION_KEY 格式错误。请使用32字节十六进制格式(64个十六进制字符)。",
);
}
// 验证密钥长度
if (keyBuffer.length !== 32) {
throw new Error(
`环境变量 ASSET_ENCRYPTION_KEY 长度错误。当前: ${keyBuffer.length} 字节, 要求: 32 字节(64个十六进制字符)。`,
);
}
return keyBuffer;
}
/**
* 加密账号凭据 - 使用 AES-256-GCM(带认证的加密)
*/ */
private encrypt(text: string): string { private encrypt(text: string): string {
const iv = crypto.randomBytes(16); // 生成随机 IV (12字节是 GCM 推荐长度)
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(this.ENCRYPTION_KEY.padEnd(32, '0').slice(0, 32)), iv); const iv = crypto.randomBytes(12);
let encrypted = cipher.update(text, 'utf8', 'hex'); const cipher = crypto.createCipheriv(
encrypted += cipher.final('hex'); "aes-256-gcm",
return iv.toString('hex') + ':' + encrypted; this.ENCRYPTION_KEY,
iv,
);
// 加密
let encrypted = cipher.update(text, "utf8", "hex");
encrypted += cipher.final("hex");
// 获取认证标签(GCM 模式提供完整性校验)
const authTag = cipher.getAuthTag();
// 返回格式: iv:authTag:encrypted
return iv.toString("hex") + ":" + authTag.toString("hex") + ":" + encrypted;
} }
/** /**
* 解密账号凭据 * 解密账号凭据
*/ */
private decrypt(encrypted: string): string { private decrypt(encrypted: string): string {
const parts = encrypted.split(':'); const parts = encrypted.split(":");
const ivStr = parts.shift(); if (parts.length !== 3) {
if (!ivStr) throw new Error('Invalid encrypted data'); throw new Error("Invalid encrypted data format");
const iv = Buffer.from(ivStr, 'hex'); }
const encryptedText = parts.join(':');
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.ENCRYPTION_KEY.padEnd(32, '0').slice(0, 32)), iv); const iv = Buffer.from(parts[0], "hex");
let decrypted = decipher.update(encryptedText, 'hex', 'utf8'); const authTag = Buffer.from(parts[1], "hex");
decrypted += decipher.final('utf8'); const encryptedText = parts[2];
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
this.ENCRYPTION_KEY,
iv,
);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedText, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted; return decrypted;
} }
@@ -63,7 +134,9 @@ export class AssetsService {
async create(userId: string, createDto: CreateAssetDto) { async create(userId: string, createDto: CreateAssetDto) {
const { groupId, accountCredentials, ...rest } = createDto; const { groupId, accountCredentials, ...rest } = createDto;
const group = await this.groupRepository.findOne({ where: { id: groupId } }); const group = await this.groupRepository.findOne({
where: { id: groupId },
});
if (!group) { if (!group) {
throw new NotFoundException({ throw new NotFoundException({
code: ErrorCode.GROUP_NOT_FOUND, code: ErrorCode.GROUP_NOT_FOUND,
@@ -76,17 +149,23 @@ export class AssetsService {
where: { groupId, userId }, where: { groupId, userId },
}); });
if (!membership || (membership.role !== GroupMemberRole.ADMIN && membership.role !== GroupMemberRole.OWNER)) { if (
!membership ||
(membership.role !== GroupMemberRole.ADMIN &&
membership.role !== GroupMemberRole.OWNER)
) {
throw new ForbiddenException({ throw new ForbiddenException({
code: ErrorCode.NO_PERMISSION, code: ErrorCode.NO_PERMISSION,
message: '需要管理员权限', message: "需要管理员权限",
}); });
} }
const asset = this.assetRepository.create({ const asset = this.assetRepository.create({
...rest, ...rest,
groupId, groupId,
accountCredentials: accountCredentials ? this.encrypt(accountCredentials) : undefined, accountCredentials: accountCredentials
? this.encrypt(accountCredentials)
: undefined,
}); });
await this.assetRepository.save(asset); await this.assetRepository.save(asset);
@@ -100,8 +179,8 @@ export class AssetsService {
async findAll(groupId: string) { async findAll(groupId: string) {
const assets = await this.assetRepository.find({ const assets = await this.assetRepository.find({
where: { groupId }, where: { groupId },
relations: ['group'], relations: ["group"],
order: { createdAt: 'DESC' }, order: { createdAt: "DESC" },
}); });
return assets.map((asset) => ({ return assets.map((asset) => ({
@@ -116,13 +195,13 @@ export class AssetsService {
async findOne(id: string, userId?: string) { async findOne(id: string, userId?: string) {
const asset = await this.assetRepository.findOne({ const asset = await this.assetRepository.findOne({
where: { id }, where: { id },
relations: ['group'], relations: ["group"],
}); });
if (!asset) { if (!asset) {
throw new NotFoundException({ throw new NotFoundException({
code: ErrorCode.ASSET_NOT_FOUND, code: ErrorCode.ASSET_NOT_FOUND,
message: '资产不存在', message: "资产不存在",
}); });
} }
@@ -132,10 +211,16 @@ export class AssetsService {
where: { groupId: asset.groupId, userId }, where: { groupId: asset.groupId, userId },
}); });
if (membership && (membership.role === GroupMemberRole.ADMIN || membership.role === GroupMemberRole.OWNER)) { if (
membership &&
(membership.role === GroupMemberRole.ADMIN ||
membership.role === GroupMemberRole.OWNER)
) {
return { return {
...asset, ...asset,
accountCredentials: asset.accountCredentials ? this.decrypt(asset.accountCredentials) : null, accountCredentials: asset.accountCredentials
? this.decrypt(asset.accountCredentials)
: null,
}; };
} }
} }
@@ -155,7 +240,7 @@ export class AssetsService {
if (!asset) { if (!asset) {
throw new NotFoundException({ throw new NotFoundException({
code: ErrorCode.ASSET_NOT_FOUND, code: ErrorCode.ASSET_NOT_FOUND,
message: '资产不存在', message: "资产不存在",
}); });
} }
@@ -164,7 +249,11 @@ export class AssetsService {
where: { groupId: asset.groupId, userId }, where: { groupId: asset.groupId, userId },
}); });
if (!membership || (membership.role !== GroupMemberRole.ADMIN && membership.role !== GroupMemberRole.OWNER)) { if (
!membership ||
(membership.role !== GroupMemberRole.ADMIN &&
membership.role !== GroupMemberRole.OWNER)
) {
throw new ForbiddenException({ throw new ForbiddenException({
code: ErrorCode.NO_PERMISSION, code: ErrorCode.NO_PERMISSION,
message: ErrorMessage[ErrorCode.NO_PERMISSION], message: ErrorMessage[ErrorCode.NO_PERMISSION],
@@ -194,20 +283,20 @@ export class AssetsService {
// 使用悲观锁防止并发借用 // 使用悲观锁防止并发借用
const asset = await queryRunner.manager.findOne(Asset, { const asset = await queryRunner.manager.findOne(Asset, {
where: { id }, where: { id },
lock: { mode: 'pessimistic_write' }, lock: { mode: "pessimistic_write" },
}); });
if (!asset) { if (!asset) {
throw new NotFoundException({ throw new NotFoundException({
code: ErrorCode.ASSET_NOT_FOUND, code: ErrorCode.ASSET_NOT_FOUND,
message: '资产不存在', message: "资产不存在",
}); });
} }
if (asset.status !== AssetStatus.AVAILABLE) { if (asset.status !== AssetStatus.AVAILABLE) {
throw new BadRequestException({ throw new BadRequestException({
code: ErrorCode.INVALID_OPERATION, code: ErrorCode.INVALID_OPERATION,
message: '资产不可用', message: "资产不可用",
}); });
} }
@@ -238,7 +327,7 @@ export class AssetsService {
await queryRunner.manager.save(AssetLog, log); await queryRunner.manager.save(AssetLog, log);
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
return { message: '借用成功' }; return { message: "借用成功" };
} catch (error) { } catch (error) {
await queryRunner.rollbackTransaction(); await queryRunner.rollbackTransaction();
throw error; throw error;
@@ -260,20 +349,20 @@ export class AssetsService {
// 使用悲观锁防止并发问题 // 使用悲观锁防止并发问题
const asset = await queryRunner.manager.findOne(Asset, { const asset = await queryRunner.manager.findOne(Asset, {
where: { id }, where: { id },
lock: { mode: 'pessimistic_write' }, lock: { mode: "pessimistic_write" },
}); });
if (!asset) { if (!asset) {
throw new NotFoundException({ throw new NotFoundException({
code: ErrorCode.ASSET_NOT_FOUND, code: ErrorCode.ASSET_NOT_FOUND,
message: '资产不存在', message: "资产不存在",
}); });
} }
if (asset.currentBorrowerId !== userId) { if (asset.currentBorrowerId !== userId) {
throw new ForbiddenException({ throw new ForbiddenException({
code: ErrorCode.NO_PERMISSION, code: ErrorCode.NO_PERMISSION,
message: '无权归还此资产', message: "无权归还此资产",
}); });
} }
@@ -292,7 +381,7 @@ export class AssetsService {
await queryRunner.manager.save(AssetLog, log); await queryRunner.manager.save(AssetLog, log);
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
return { message: '归还成功' }; return { message: "归还成功" };
} catch (error) { } catch (error) {
await queryRunner.rollbackTransaction(); await queryRunner.rollbackTransaction();
throw error; throw error;
@@ -310,14 +399,14 @@ export class AssetsService {
if (!asset) { if (!asset) {
throw new NotFoundException({ throw new NotFoundException({
code: ErrorCode.ASSET_NOT_FOUND, code: ErrorCode.ASSET_NOT_FOUND,
message: '资产不存在', message: "资产不存在",
}); });
} }
const logs = await this.assetLogRepository.find({ const logs = await this.assetLogRepository.find({
where: { assetId: id }, where: { assetId: id },
relations: ['user'], relations: ["user"],
order: { createdAt: 'DESC' }, order: { createdAt: "DESC" },
}); });
return logs; return logs;
@@ -332,7 +421,7 @@ export class AssetsService {
if (!asset) { if (!asset) {
throw new NotFoundException({ throw new NotFoundException({
code: ErrorCode.ASSET_NOT_FOUND, code: ErrorCode.ASSET_NOT_FOUND,
message: '资产不存在', message: "资产不存在",
}); });
} }
@@ -341,7 +430,11 @@ export class AssetsService {
where: { groupId: asset.groupId, userId }, where: { groupId: asset.groupId, userId },
}); });
if (!membership || (membership.role !== GroupMemberRole.ADMIN && membership.role !== GroupMemberRole.OWNER)) { if (
!membership ||
(membership.role !== GroupMemberRole.ADMIN &&
membership.role !== GroupMemberRole.OWNER)
) {
throw new ForbiddenException({ throw new ForbiddenException({
code: ErrorCode.NO_PERMISSION, code: ErrorCode.NO_PERMISSION,
message: ErrorMessage[ErrorCode.NO_PERMISSION], message: ErrorMessage[ErrorCode.NO_PERMISSION],
@@ -350,6 +443,6 @@ export class AssetsService {
await this.assetRepository.remove(asset); await this.assetRepository.remove(asset);
return { message: '删除成功' }; return { message: "删除成功" };
} }
} }

View File

@@ -1,42 +1,58 @@
import { Controller, Get, Put, Body, Param, UseGuards } from '@nestjs/common'; import { Controller, Get, Put, Body, Param, UseGuards } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import {
import { UsersService } from './users.service'; ApiTags,
import { UpdateUserDto, ChangePasswordDto } from './dto/user.dto'; ApiOperation,
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; ApiResponse,
import { CurrentUser } from '../../common/decorators/current-user.decorator'; ApiBearerAuth,
import { User } from '../../entities/user.entity'; } from "@nestjs/swagger";
import { UsersService } from "./users.service";
import { UpdateUserDto, ChangePasswordDto } from "./dto/user.dto";
import { JwtAuthGuard } from "../../common/guards/jwt-auth.guard";
import { CurrentUser } from "../../common/decorators/current-user.decorator";
import { User } from "../../entities/user.entity";
@ApiTags('users') @ApiTags("users")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('users') @Controller("users")
export class UsersController { export class UsersController {
constructor(private readonly usersService: UsersService) {} constructor(private readonly usersService: UsersService) {}
@Get('me') @Get("me")
@ApiOperation({ summary: '获取当前用户信息' }) @ApiOperation({ summary: "获取当前用户信息" })
@ApiResponse({ status: 200, description: '获取成功' }) @ApiResponse({ status: 200, description: "获取成功" })
async getProfile(@CurrentUser() user: User) { async getProfile(@CurrentUser() user: User) {
return this.usersService.findOne(user.id); return this.usersService.findOne(user.id);
} }
@Get(':id') @Get(":id")
@ApiOperation({ summary: '获取用户信息' }) @ApiOperation({
@ApiResponse({ status: 200, description: '获取成功' }) summary: "获取用户信息",
async findOne(@Param('id') id: string) { description: "查询自己的信息返回完整数据(包含 email、phone 等敏感字段),查询他人信息只返回公开信息(不包含敏感字段)"
return this.usersService.findOne(id); })
@ApiResponse({ status: 200, description: "获取成功" })
async findOne(@Param("id") id: string, @CurrentUser() currentUser: User) {
// 如果查询的是自己的信息,返回完整信息
// 否则只返回公开信息(不包含 email、phone 等敏感字段)
if (currentUser.id === id) {
return this.usersService.findOne(id);
}
return this.usersService.findOnePublic(id);
} }
@Put('me') @Put("me")
@ApiOperation({ summary: '更新当前用户信息' }) @ApiOperation({ summary: "更新当前用户信息" })
@ApiResponse({ status: 200, description: '更新成功' }) @ApiResponse({ status: 200, description: "更新成功" })
async update(@CurrentUser() user: User, @Body() updateUserDto: UpdateUserDto) { async update(
@CurrentUser() user: User,
@Body() updateUserDto: UpdateUserDto,
) {
return this.usersService.update(user.id, updateUserDto); return this.usersService.update(user.id, updateUserDto);
} }
@Put('me/password') @Put("me/password")
@ApiOperation({ summary: '修改密码' }) @ApiOperation({ summary: "修改密码" })
@ApiResponse({ status: 200, description: '修改成功' }) @ApiResponse({ status: 200, description: "修改成功" })
async changePassword( async changePassword(
@CurrentUser() user: User, @CurrentUser() user: User,
@Body() changePasswordDto: ChangePasswordDto, @Body() changePasswordDto: ChangePasswordDto,

View File

@@ -1,15 +1,22 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import {
import { InjectRepository } from '@nestjs/typeorm'; Injectable,
import { Repository } from 'typeorm'; NotFoundException,
import { User } from '../../entities/user.entity'; BadRequestException,
import { UpdateUserDto, ChangePasswordDto } from './dto/user.dto'; } from "@nestjs/common";
import { CryptoUtil } from '../../common/utils/crypto.util'; import { InjectRepository } from "@nestjs/typeorm";
import { ErrorCode, ErrorMessage } from '../../common/interfaces/response.interface'; import { Repository } from "typeorm";
import { CacheService } from '../../common/services/cache.service'; import { User } from "../../entities/user.entity";
import { UpdateUserDto, ChangePasswordDto } from "./dto/user.dto";
import { CryptoUtil } from "../../common/utils/crypto.util";
import {
ErrorCode,
ErrorMessage,
} from "../../common/interfaces/response.interface";
import { CacheService } from "../../common/services/cache.service";
@Injectable() @Injectable()
export class UsersService { export class UsersService {
private readonly CACHE_PREFIX = 'user'; private readonly CACHE_PREFIX = "user";
private readonly CACHE_TTL = 300; // 5分钟 private readonly CACHE_TTL = 300; // 5分钟
constructor( constructor(
@@ -19,11 +26,13 @@ export class UsersService {
) {} ) {}
/** /**
* 获取用户信息 * 获取用户完整信息(包含敏感字段)
*/ */
async findOne(id: string) { async findOne(id: string) {
// 先查缓存 // 先查缓存
const cached = this.cacheService.get<any>(id, { prefix: this.CACHE_PREFIX }); const cached = this.cacheService.get<any>(`${id}:full`, {
prefix: this.CACHE_PREFIX,
});
if (cached) { if (cached) {
return cached; return cached;
} }
@@ -51,7 +60,45 @@ export class UsersService {
}; };
// 写入缓存 // 写入缓存
this.cacheService.set(id, result, { this.cacheService.set(`${id}:full`, result, {
prefix: this.CACHE_PREFIX,
ttl: this.CACHE_TTL,
});
return result;
}
/**
* 获取用户公开信息(不包含敏感字段)
*/
async findOnePublic(id: string) {
// 先查缓存
const cached = this.cacheService.get<any>(`${id}:public`, {
prefix: this.CACHE_PREFIX,
});
if (cached) {
return cached;
}
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException({
code: ErrorCode.USER_NOT_FOUND,
message: ErrorMessage[ErrorCode.USER_NOT_FOUND],
});
}
const result = {
id: user.id,
username: user.username,
avatar: user.avatar,
isMember: user.isMember,
createdAt: user.createdAt,
};
// 写入缓存
this.cacheService.set(`${id}:public`, result, {
prefix: this.CACHE_PREFIX, prefix: this.CACHE_PREFIX,
ttl: this.CACHE_TTL, ttl: this.CACHE_TTL,
}); });
@@ -80,7 +127,7 @@ export class UsersService {
if (existingUser) { if (existingUser) {
throw new BadRequestException({ throw new BadRequestException({
code: ErrorCode.USER_EXISTS, code: ErrorCode.USER_EXISTS,
message: '邮箱已被使用', message: "邮箱已被使用",
}); });
} }
} }
@@ -93,7 +140,7 @@ export class UsersService {
if (existingUser) { if (existingUser) {
throw new BadRequestException({ throw new BadRequestException({
code: ErrorCode.USER_EXISTS, code: ErrorCode.USER_EXISTS,
message: '手机号已被使用', message: "手机号已被使用",
}); });
} }
} }
@@ -101,8 +148,9 @@ export class UsersService {
Object.assign(user, updateUserDto); Object.assign(user, updateUserDto);
await this.userRepository.save(user); await this.userRepository.save(user);
// 清除缓存 // 清除两种缓存(完整信息和公开信息)
this.cacheService.del(id, { prefix: this.CACHE_PREFIX }); this.cacheService.del(`${id}:full`, { prefix: this.CACHE_PREFIX });
this.cacheService.del(`${id}:public`, { prefix: this.CACHE_PREFIX });
return this.findOne(id); return this.findOne(id);
} }
@@ -112,9 +160,9 @@ export class UsersService {
*/ */
async changePassword(id: string, changePasswordDto: ChangePasswordDto) { async changePassword(id: string, changePasswordDto: ChangePasswordDto) {
const user = await this.userRepository const user = await this.userRepository
.createQueryBuilder('user') .createQueryBuilder("user")
.where('user.id = :id', { id }) .where("user.id = :id", { id })
.addSelect('user.password') .addSelect("user.password")
.getOne(); .getOne();
if (!user) { if (!user) {
@@ -133,15 +181,17 @@ export class UsersService {
if (!isPasswordValid) { if (!isPasswordValid) {
throw new BadRequestException({ throw new BadRequestException({
code: ErrorCode.PASSWORD_ERROR, code: ErrorCode.PASSWORD_ERROR,
message: '原密码错误', message: "原密码错误",
}); });
} }
// 更新密码 // 更新密码
user.password = await CryptoUtil.hashPassword(changePasswordDto.newPassword); user.password = await CryptoUtil.hashPassword(
changePasswordDto.newPassword,
);
await this.userRepository.save(user); await this.userRepository.save(user);
return { message: '密码修改成功' }; return { message: "密码修改成功" };
} }
/** /**
@@ -149,11 +199,11 @@ export class UsersService {
*/ */
async getCreatedGroupsCount(userId: string): Promise<number> { async getCreatedGroupsCount(userId: string): Promise<number> {
const user = await this.userRepository const user = await this.userRepository
.createQueryBuilder('user') .createQueryBuilder("user")
.leftJoin('user.groupMembers', 'member') .leftJoin("user.groupMembers", "member")
.leftJoin('member.group', 'group') .leftJoin("member.group", "group")
.where('user.id = :userId', { userId }) .where("user.id = :userId", { userId })
.andWhere('group.ownerId = :userId', { userId }) .andWhere("group.ownerId = :userId", { userId })
.getCount(); .getCount();
return user; return user;
@@ -164,9 +214,9 @@ export class UsersService {
*/ */
async getJoinedGroupsCount(userId: string): Promise<number> { async getJoinedGroupsCount(userId: string): Promise<number> {
const user = await this.userRepository const user = await this.userRepository
.createQueryBuilder('user') .createQueryBuilder("user")
.leftJoin('user.groupMembers', 'member') .leftJoin("user.groupMembers", "member")
.where('user.id = :userId', { userId }) .where("user.id = :userId", { userId })
.getCount(); .getCount();
return user; return user;