diff --git a/src/config/jwt.config.ts b/src/config/jwt.config.ts index f60dd07..43080f6 100644 --- a/src/config/jwt.config.ts +++ b/src/config/jwt.config.ts @@ -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", })); diff --git a/src/modules/assets/assets.service.ts b/src/modules/assets/assets.service.ts index 2e9efa8..651f783 100644 --- a/src/modules/assets/assets.service.ts +++ b/src/modules/assets/assets.service.ts @@ -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, 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: "删除成功" }; } } diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index c243591..59beb66 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -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, diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 05e0ee2..d98f3ae 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -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(id, { prefix: this.CACHE_PREFIX }); + const cached = this.cacheService.get(`${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(`${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 { 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 { 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;