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:
@@ -1,8 +1,44 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
import { registerAs } from "@nestjs/config";
|
||||
|
||||
export default registerAs('jwt', () => ({
|
||||
secret: process.env.JWT_SECRET || 'default-secret',
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
||||
refreshSecret: process.env.JWT_REFRESH_SECRET || 'default-refresh-secret',
|
||||
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d',
|
||||
const validateJwtSecret = (
|
||||
secret: string | undefined,
|
||||
secretName: string,
|
||||
): secret is string => {
|
||||
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",
|
||||
}));
|
||||
|
||||
@@ -3,21 +3,32 @@ import {
|
||||
NotFoundException,
|
||||
ForbiddenException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { Asset } from '../../entities/asset.entity';
|
||||
import { AssetLog } from '../../entities/asset-log.entity';
|
||||
import { Group } from '../../entities/group.entity';
|
||||
import { GroupMember } from '../../entities/group-member.entity';
|
||||
import { CreateAssetDto, UpdateAssetDto, 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';
|
||||
} from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository, DataSource } from "typeorm";
|
||||
import { Asset } from "../../entities/asset.entity";
|
||||
import { AssetLog } from "../../entities/asset-log.entity";
|
||||
import { Group } from "../../entities/group.entity";
|
||||
import { GroupMember } from "../../entities/group-member.entity";
|
||||
import {
|
||||
CreateAssetDto,
|
||||
UpdateAssetDto,
|
||||
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()
|
||||
export class AssetsService {
|
||||
private readonly ENCRYPTION_KEY = process.env.ASSET_ENCRYPTION_KEY || 'default-key-change-in-production';
|
||||
private readonly ENCRYPTION_KEY: Buffer;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Asset)
|
||||
@@ -29,31 +40,91 @@ export class AssetsService {
|
||||
@InjectRepository(GroupMember)
|
||||
private groupMemberRepository: Repository<GroupMember>,
|
||||
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 {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(this.ENCRYPTION_KEY.padEnd(32, '0').slice(0, 32)), iv);
|
||||
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
return iv.toString('hex') + ':' + encrypted;
|
||||
// 生成随机 IV (12字节是 GCM 推荐长度)
|
||||
const iv = crypto.randomBytes(12);
|
||||
const cipher = crypto.createCipheriv(
|
||||
"aes-256-gcm",
|
||||
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 {
|
||||
const parts = encrypted.split(':');
|
||||
const ivStr = parts.shift();
|
||||
if (!ivStr) throw new Error('Invalid encrypted data');
|
||||
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);
|
||||
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
const parts = encrypted.split(":");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("Invalid encrypted data format");
|
||||
}
|
||||
|
||||
const iv = Buffer.from(parts[0], "hex");
|
||||
const authTag = Buffer.from(parts[1], "hex");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -63,7 +134,9 @@ export class AssetsService {
|
||||
async create(userId: string, createDto: CreateAssetDto) {
|
||||
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) {
|
||||
throw new NotFoundException({
|
||||
code: ErrorCode.GROUP_NOT_FOUND,
|
||||
@@ -76,17 +149,23 @@ export class AssetsService {
|
||||
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({
|
||||
code: ErrorCode.NO_PERMISSION,
|
||||
message: '需要管理员权限',
|
||||
message: "需要管理员权限",
|
||||
});
|
||||
}
|
||||
|
||||
const asset = this.assetRepository.create({
|
||||
...rest,
|
||||
groupId,
|
||||
accountCredentials: accountCredentials ? this.encrypt(accountCredentials) : undefined,
|
||||
accountCredentials: accountCredentials
|
||||
? this.encrypt(accountCredentials)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
await this.assetRepository.save(asset);
|
||||
@@ -100,8 +179,8 @@ export class AssetsService {
|
||||
async findAll(groupId: string) {
|
||||
const assets = await this.assetRepository.find({
|
||||
where: { groupId },
|
||||
relations: ['group'],
|
||||
order: { createdAt: 'DESC' },
|
||||
relations: ["group"],
|
||||
order: { createdAt: "DESC" },
|
||||
});
|
||||
|
||||
return assets.map((asset) => ({
|
||||
@@ -116,13 +195,13 @@ export class AssetsService {
|
||||
async findOne(id: string, userId?: string) {
|
||||
const asset = await this.assetRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['group'],
|
||||
relations: ["group"],
|
||||
});
|
||||
|
||||
if (!asset) {
|
||||
throw new NotFoundException({
|
||||
code: ErrorCode.ASSET_NOT_FOUND,
|
||||
message: '资产不存在',
|
||||
message: "资产不存在",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,10 +211,16 @@ export class AssetsService {
|
||||
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 {
|
||||
...asset,
|
||||
accountCredentials: asset.accountCredentials ? this.decrypt(asset.accountCredentials) : null,
|
||||
accountCredentials: asset.accountCredentials
|
||||
? this.decrypt(asset.accountCredentials)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -151,11 +236,11 @@ export class AssetsService {
|
||||
*/
|
||||
async update(userId: string, id: string, updateDto: UpdateAssetDto) {
|
||||
const asset = await this.assetRepository.findOne({ where: { id } });
|
||||
|
||||
|
||||
if (!asset) {
|
||||
throw new NotFoundException({
|
||||
code: ErrorCode.ASSET_NOT_FOUND,
|
||||
message: '资产不存在',
|
||||
message: "资产不存在",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -164,7 +249,11 @@ export class AssetsService {
|
||||
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({
|
||||
code: ErrorCode.NO_PERMISSION,
|
||||
message: ErrorMessage[ErrorCode.NO_PERMISSION],
|
||||
@@ -194,20 +283,20 @@ export class AssetsService {
|
||||
// 使用悲观锁防止并发借用
|
||||
const asset = await queryRunner.manager.findOne(Asset, {
|
||||
where: { id },
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
lock: { mode: "pessimistic_write" },
|
||||
});
|
||||
|
||||
if (!asset) {
|
||||
throw new NotFoundException({
|
||||
code: ErrorCode.ASSET_NOT_FOUND,
|
||||
message: '资产不存在',
|
||||
message: "资产不存在",
|
||||
});
|
||||
}
|
||||
|
||||
if (asset.status !== AssetStatus.AVAILABLE) {
|
||||
throw new BadRequestException({
|
||||
code: ErrorCode.INVALID_OPERATION,
|
||||
message: '资产不可用',
|
||||
message: "资产不可用",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -238,7 +327,7 @@ export class AssetsService {
|
||||
await queryRunner.manager.save(AssetLog, log);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return { message: '借用成功' };
|
||||
return { message: "借用成功" };
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
@@ -260,20 +349,20 @@ export class AssetsService {
|
||||
// 使用悲观锁防止并发问题
|
||||
const asset = await queryRunner.manager.findOne(Asset, {
|
||||
where: { id },
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
lock: { mode: "pessimistic_write" },
|
||||
});
|
||||
|
||||
if (!asset) {
|
||||
throw new NotFoundException({
|
||||
code: ErrorCode.ASSET_NOT_FOUND,
|
||||
message: '资产不存在',
|
||||
message: "资产不存在",
|
||||
});
|
||||
}
|
||||
|
||||
if (asset.currentBorrowerId !== userId) {
|
||||
throw new ForbiddenException({
|
||||
code: ErrorCode.NO_PERMISSION,
|
||||
message: '无权归还此资产',
|
||||
message: "无权归还此资产",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -292,7 +381,7 @@ export class AssetsService {
|
||||
await queryRunner.manager.save(AssetLog, log);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return { message: '归还成功' };
|
||||
return { message: "归还成功" };
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
@@ -310,14 +399,14 @@ export class AssetsService {
|
||||
if (!asset) {
|
||||
throw new NotFoundException({
|
||||
code: ErrorCode.ASSET_NOT_FOUND,
|
||||
message: '资产不存在',
|
||||
message: "资产不存在",
|
||||
});
|
||||
}
|
||||
|
||||
const logs = await this.assetLogRepository.find({
|
||||
where: { assetId: id },
|
||||
relations: ['user'],
|
||||
order: { createdAt: 'DESC' },
|
||||
relations: ["user"],
|
||||
order: { createdAt: "DESC" },
|
||||
});
|
||||
|
||||
return logs;
|
||||
@@ -332,7 +421,7 @@ export class AssetsService {
|
||||
if (!asset) {
|
||||
throw new NotFoundException({
|
||||
code: ErrorCode.ASSET_NOT_FOUND,
|
||||
message: '资产不存在',
|
||||
message: "资产不存在",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -341,7 +430,11 @@ export class AssetsService {
|
||||
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({
|
||||
code: ErrorCode.NO_PERMISSION,
|
||||
message: ErrorMessage[ErrorCode.NO_PERMISSION],
|
||||
@@ -350,6 +443,6 @@ export class AssetsService {
|
||||
|
||||
await this.assetRepository.remove(asset);
|
||||
|
||||
return { message: '删除成功' };
|
||||
return { message: "删除成功" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,58 @@
|
||||
import { Controller, Get, Put, Body, Param, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } 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';
|
||||
import { Controller, Get, Put, Body, Param, UseGuards } from "@nestjs/common";
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} 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()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('users')
|
||||
@Controller("users")
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get('me')
|
||||
@ApiOperation({ summary: '获取当前用户信息' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@Get("me")
|
||||
@ApiOperation({ summary: "获取当前用户信息" })
|
||||
@ApiResponse({ status: 200, description: "获取成功" })
|
||||
async getProfile(@CurrentUser() user: User) {
|
||||
return this.usersService.findOne(user.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '获取用户信息' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
async findOne(@Param('id') id: string) {
|
||||
return this.usersService.findOne(id);
|
||||
@Get(":id")
|
||||
@ApiOperation({
|
||||
summary: "获取用户信息",
|
||||
description: "查询自己的信息返回完整数据(包含 email、phone 等敏感字段),查询他人信息只返回公开信息(不包含敏感字段)"
|
||||
})
|
||||
@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')
|
||||
@ApiOperation({ summary: '更新当前用户信息' })
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
async update(@CurrentUser() user: User, @Body() updateUserDto: UpdateUserDto) {
|
||||
@Put("me")
|
||||
@ApiOperation({ summary: "更新当前用户信息" })
|
||||
@ApiResponse({ status: 200, description: "更新成功" })
|
||||
async update(
|
||||
@CurrentUser() user: User,
|
||||
@Body() updateUserDto: UpdateUserDto,
|
||||
) {
|
||||
return this.usersService.update(user.id, updateUserDto);
|
||||
}
|
||||
|
||||
@Put('me/password')
|
||||
@ApiOperation({ summary: '修改密码' })
|
||||
@ApiResponse({ status: 200, description: '修改成功' })
|
||||
@Put("me/password")
|
||||
@ApiOperation({ summary: "修改密码" })
|
||||
@ApiResponse({ status: 200, description: "修改成功" })
|
||||
async changePassword(
|
||||
@CurrentUser() user: User,
|
||||
@Body() changePasswordDto: ChangePasswordDto,
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
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';
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
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()
|
||||
export class UsersService {
|
||||
private readonly CACHE_PREFIX = 'user';
|
||||
private readonly CACHE_PREFIX = "user";
|
||||
private readonly CACHE_TTL = 300; // 5分钟
|
||||
|
||||
constructor(
|
||||
@@ -19,11 +26,13 @@ export class UsersService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* 获取用户完整信息(包含敏感字段)
|
||||
*/
|
||||
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) {
|
||||
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,
|
||||
ttl: this.CACHE_TTL,
|
||||
});
|
||||
@@ -80,7 +127,7 @@ export class UsersService {
|
||||
if (existingUser) {
|
||||
throw new BadRequestException({
|
||||
code: ErrorCode.USER_EXISTS,
|
||||
message: '邮箱已被使用',
|
||||
message: "邮箱已被使用",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -93,7 +140,7 @@ export class UsersService {
|
||||
if (existingUser) {
|
||||
throw new BadRequestException({
|
||||
code: ErrorCode.USER_EXISTS,
|
||||
message: '手机号已被使用',
|
||||
message: "手机号已被使用",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -101,8 +148,9 @@ export class UsersService {
|
||||
Object.assign(user, updateUserDto);
|
||||
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);
|
||||
}
|
||||
@@ -112,9 +160,9 @@ export class UsersService {
|
||||
*/
|
||||
async changePassword(id: string, changePasswordDto: ChangePasswordDto) {
|
||||
const user = await this.userRepository
|
||||
.createQueryBuilder('user')
|
||||
.where('user.id = :id', { id })
|
||||
.addSelect('user.password')
|
||||
.createQueryBuilder("user")
|
||||
.where("user.id = :id", { id })
|
||||
.addSelect("user.password")
|
||||
.getOne();
|
||||
|
||||
if (!user) {
|
||||
@@ -133,15 +181,17 @@ export class UsersService {
|
||||
if (!isPasswordValid) {
|
||||
throw new BadRequestException({
|
||||
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);
|
||||
|
||||
return { message: '密码修改成功' };
|
||||
return { message: "密码修改成功" };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,11 +199,11 @@ export class UsersService {
|
||||
*/
|
||||
async getCreatedGroupsCount(userId: string): Promise<number> {
|
||||
const user = await this.userRepository
|
||||
.createQueryBuilder('user')
|
||||
.leftJoin('user.groupMembers', 'member')
|
||||
.leftJoin('member.group', 'group')
|
||||
.where('user.id = :userId', { userId })
|
||||
.andWhere('group.ownerId = :userId', { userId })
|
||||
.createQueryBuilder("user")
|
||||
.leftJoin("user.groupMembers", "member")
|
||||
.leftJoin("member.group", "group")
|
||||
.where("user.id = :userId", { userId })
|
||||
.andWhere("group.ownerId = :userId", { userId })
|
||||
.getCount();
|
||||
|
||||
return user;
|
||||
@@ -164,9 +214,9 @@ export class UsersService {
|
||||
*/
|
||||
async getJoinedGroupsCount(userId: string): Promise<number> {
|
||||
const user = await this.userRepository
|
||||
.createQueryBuilder('user')
|
||||
.leftJoin('user.groupMembers', 'member')
|
||||
.where('user.id = :userId', { userId })
|
||||
.createQueryBuilder("user")
|
||||
.leftJoin("user.groupMembers", "member")
|
||||
.where("user.id = :userId", { userId })
|
||||
.getCount();
|
||||
|
||||
return user;
|
||||
|
||||
Reference in New Issue
Block a user