feat: 添加用户认证系统
引入基于 Basic Auth 的用户认证系统,包括用户管理、登录界面和 API 鉴权 - 新增用户实体和管理功能 - 实现前端登录界面和凭证管理 - 重构 API 鉴权为 Basic Auth 模式 - 添加用户管理脚本工具
This commit is contained in:
@@ -12,6 +12,7 @@ import { LoggerModule } from './common/logger/logger.module';
|
||||
import { LoggingMiddleware } from './common/logger/logging.middleware';
|
||||
import { AiModule } from './ai/ai.module';
|
||||
import { AuthModule } from './common/auth/auth.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -23,6 +24,7 @@ import { AuthModule } from './common/auth/auth.module';
|
||||
}),
|
||||
LoggerModule,
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
DatabaseModule,
|
||||
BidsModule,
|
||||
KeywordsModule,
|
||||
|
||||
@@ -6,78 +6,55 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Request } from 'express';
|
||||
import { UsersService } from '../../users/users.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private configService: ConfigService) {}
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private usersService: UsersService,
|
||||
) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const clientIp = this.getClientIp(request);
|
||||
|
||||
// 检查是否为本地 IP
|
||||
if (this.isLocalIp(clientIp)) {
|
||||
return true; // 本地访问直接放行
|
||||
}
|
||||
// 检查是否启用 Basic Auth
|
||||
const enableBasicAuth =
|
||||
this.configService.get<string>('ENABLE_BASIC_AUTH') === 'true';
|
||||
|
||||
// 公网访问需要验证 API Key
|
||||
const apiKey = request.headers['x-api-key'] as string;
|
||||
const expectedApiKey = this.configService.get<string>('API_KEY');
|
||||
|
||||
if (!expectedApiKey) {
|
||||
// 如果未配置 API_KEY,允许所有访问(开发环境)
|
||||
if (!enableBasicAuth) {
|
||||
// 如果未启用 Basic Auth,允许所有访问
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!apiKey || apiKey !== expectedApiKey) {
|
||||
throw new UnauthorizedException('Invalid or missing API Key');
|
||||
// 解析 Authorization header
|
||||
const authHeader = request.headers['authorization'] as string;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
throw new UnauthorizedException('Missing or invalid Authorization header');
|
||||
}
|
||||
|
||||
// 解码 Basic Auth
|
||||
const base64Credentials = authHeader.split(' ')[1];
|
||||
const credentials = Buffer.from(base64Credentials, 'base64').toString(
|
||||
'utf-8',
|
||||
);
|
||||
const [username, password] = credentials.split(':');
|
||||
|
||||
if (!username || !password) {
|
||||
throw new UnauthorizedException('Invalid credentials format');
|
||||
}
|
||||
|
||||
// 验证用户
|
||||
const user = await this.usersService.validateUser(username, password);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid username or password');
|
||||
}
|
||||
|
||||
// 将用户信息附加到请求对象
|
||||
(request as any).user = user;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端真实 IP
|
||||
* 支持反向代理场景(X-Forwarded-For)
|
||||
*/
|
||||
private getClientIp(request: Request): string {
|
||||
const forwardedFor = request.headers['x-forwarded-for'];
|
||||
if (forwardedFor) {
|
||||
// X-Forwarded-For 可能包含多个 IP,取第一个
|
||||
const ips = (forwardedFor as string).split(',');
|
||||
return ips[0].trim();
|
||||
}
|
||||
return request.ip || request.socket.remoteAddress || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为本地 IP
|
||||
*/
|
||||
private isLocalIp(ip: string): boolean {
|
||||
if (!ip || ip === 'unknown') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// IPv4 本地地址
|
||||
if (ip === '127.0.0.1' || ip === 'localhost' || ip.startsWith('127.')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPv6 本地地址
|
||||
if (ip === '::1' || ip === '::ffff:127.0.0.1') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 内网地址(可选,根据需求决定是否允许)
|
||||
// 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
|
||||
const privateIpRegex =
|
||||
/^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/;
|
||||
if (privateIpRegex.test(ip)) {
|
||||
// 如果需要内网也免鉴权,可以返回 true
|
||||
// 这里默认返回 false,内网也需要鉴权
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { AuthGuard } from './auth.guard';
|
||||
import { UsersModule } from '../../users/users.module';
|
||||
|
||||
@Module({
|
||||
imports: [UsersModule],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
import { BidItem } from '../bids/entities/bid-item.entity';
|
||||
import { CrawlInfoAdd } from '../crawler/entities/crawl-info-add.entity';
|
||||
import { AiRecommendation } from '../ai/entities/ai-recommendation.entity';
|
||||
import { Keyword } from '../keywords/keyword.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -18,11 +23,19 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
username: configService.get<string>('DATABASE_USERNAME', 'root'),
|
||||
password: configService.get<string>('DATABASE_PASSWORD', 'root'),
|
||||
database: configService.get<string>('DATABASE_NAME', 'bidding'),
|
||||
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
|
||||
entities: [
|
||||
User,
|
||||
BidItem,
|
||||
CrawlInfoAdd,
|
||||
AiRecommendation,
|
||||
Keyword,
|
||||
__dirname + '/../**/*.entity{.ts,.js}',
|
||||
],
|
||||
synchronize: false,
|
||||
timezone: 'Z',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
exports: [TypeOrmModule],
|
||||
})
|
||||
export class DatabaseModule {}
|
||||
|
||||
100
src/scripts/create-user.ts
Normal file
100
src/scripts/create-user.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'dotenv/config';
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
// 数据库配置
|
||||
const dbConfig: DataSourceOptions = {
|
||||
type: (process.env.DATABASE_TYPE as any) || 'mysql',
|
||||
host: process.env.DATABASE_HOST || 'localhost',
|
||||
port: parseInt(process.env.DATABASE_PORT || '3306'),
|
||||
username: process.env.DATABASE_USERNAME || 'root',
|
||||
password: process.env.DATABASE_PASSWORD || 'root',
|
||||
database: process.env.DATABASE_NAME || 'bidding',
|
||||
entities: [User],
|
||||
synchronize: false,
|
||||
};
|
||||
|
||||
// 日志工具
|
||||
const logger = {
|
||||
log: (message: string, ...args: any[]) => {
|
||||
console.log(`[INFO] ${new Date().toISOString()} - ${message}`, ...args);
|
||||
},
|
||||
error: (message: string, ...args: any[]) => {
|
||||
console.error(`[ERROR] ${new Date().toISOString()} - ${message}`, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
// 主函数
|
||||
async function createUser() {
|
||||
let dataSource: DataSource | null = null;
|
||||
|
||||
try {
|
||||
// 从命令行参数获取用户名和密码
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 2) {
|
||||
console.log('用法: npm run user:create <用户名> <密码>');
|
||||
console.log('示例: npm run user:create admin password123');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const username = args[0];
|
||||
const password = args[1];
|
||||
|
||||
if (!username || username.trim().length === 0) {
|
||||
logger.error('用户名不能为空');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!password || password.length === 0) {
|
||||
logger.error('密码不能为空');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.log('开始创建用户...');
|
||||
|
||||
// 创建数据库连接
|
||||
dataSource = new DataSource(dbConfig);
|
||||
await dataSource.initialize();
|
||||
logger.log('数据库连接成功');
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const existingUser = await userRepository.findOne({
|
||||
where: { username },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
logger.error(`用户名 ${username} 已存在`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// 创建用户
|
||||
const user = userRepository.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
await userRepository.save(user);
|
||||
|
||||
logger.log(`用户 ${username} 创建成功!`);
|
||||
logger.log(`用户 ID: ${user.id}`);
|
||||
logger.log(`创建时间: ${user.createdAt}`);
|
||||
|
||||
await dataSource.destroy();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error('创建用户失败:', error);
|
||||
if (dataSource && dataSource.isInitialized) {
|
||||
await dataSource.destroy();
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行创建用户
|
||||
createUser();
|
||||
78
src/scripts/delete-user.ts
Normal file
78
src/scripts/delete-user.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'dotenv/config';
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
|
||||
// 数据库配置
|
||||
const dbConfig: DataSourceOptions = {
|
||||
type: (process.env.DATABASE_TYPE as any) || 'mysql',
|
||||
host: process.env.DATABASE_HOST || 'localhost',
|
||||
port: parseInt(process.env.DATABASE_PORT || '3306'),
|
||||
username: process.env.DATABASE_USERNAME || 'root',
|
||||
password: process.env.DATABASE_PASSWORD || 'root',
|
||||
database: process.env.DATABASE_NAME || 'bidding',
|
||||
entities: [User],
|
||||
synchronize: false,
|
||||
};
|
||||
|
||||
// 日志工具
|
||||
const logger = {
|
||||
log: (message: string, ...args: any[]) => {
|
||||
console.log(`[INFO] ${new Date().toISOString()} - ${message}`, ...args);
|
||||
},
|
||||
error: (message: string, ...args: any[]) => {
|
||||
console.error(`[ERROR] ${new Date().toISOString()} - ${message}`, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
// 主函数
|
||||
async function deleteUser() {
|
||||
let dataSource: DataSource | null = null;
|
||||
|
||||
try {
|
||||
// 获取命令行参数
|
||||
const usernameToDelete = process.argv[2];
|
||||
|
||||
if (!usernameToDelete) {
|
||||
logger.error('请提供要删除的用户名');
|
||||
console.log('用法: npm run user:delete <username>');
|
||||
console.log('示例: npm run user:delete testuser');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.log(`开始删除用户: ${usernameToDelete}...`);
|
||||
|
||||
// 创建数据库连接
|
||||
dataSource = new DataSource(dbConfig);
|
||||
await dataSource.initialize();
|
||||
logger.log('数据库连接成功');
|
||||
|
||||
// 获取用户
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const user = await userRepository.findOne({
|
||||
where: { username: usernameToDelete },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
logger.error(`用户 "${usernameToDelete}" 不存在`);
|
||||
await dataSource.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
await userRepository.delete(user.id);
|
||||
|
||||
logger.log(`用户 "${usernameToDelete}" 删除成功!`);
|
||||
|
||||
await dataSource.destroy();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error('删除用户失败:', error);
|
||||
if (dataSource && dataSource.isInitialized) {
|
||||
await dataSource.destroy();
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行删除用户
|
||||
deleteUser();
|
||||
55
src/scripts/init-users-table.ts
Normal file
55
src/scripts/init-users-table.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'dotenv/config';
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
|
||||
// 数据库配置
|
||||
const dbConfig: DataSourceOptions = {
|
||||
type: (process.env.DATABASE_TYPE as any) || 'mysql',
|
||||
host: process.env.DATABASE_HOST || 'localhost',
|
||||
port: parseInt(process.env.DATABASE_PORT || '3306'),
|
||||
username: process.env.DATABASE_USERNAME || 'root',
|
||||
password: process.env.DATABASE_PASSWORD || 'root',
|
||||
database: process.env.DATABASE_NAME || 'bidding',
|
||||
entities: [User],
|
||||
synchronize: true, // 启用自动同步以创建表
|
||||
};
|
||||
|
||||
// 日志工具
|
||||
const logger = {
|
||||
log: (message: string, ...args: any[]) => {
|
||||
console.log(`[INFO] ${new Date().toISOString()} - ${message}`, ...args);
|
||||
},
|
||||
error: (message: string, ...args: any[]) => {
|
||||
console.error(`[ERROR] ${new Date().toISOString()} - ${message}`, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
// 主函数
|
||||
async function initUsersTable() {
|
||||
let dataSource: DataSource | null = null;
|
||||
|
||||
try {
|
||||
logger.log('开始初始化 users 表...');
|
||||
|
||||
// 创建数据库连接
|
||||
dataSource = new DataSource(dbConfig);
|
||||
await dataSource.initialize();
|
||||
logger.log('数据库连接成功');
|
||||
|
||||
// 同步表结构
|
||||
await dataSource.synchronize();
|
||||
logger.log('users 表创建成功!');
|
||||
|
||||
await dataSource.destroy();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error('初始化 users 表失败:', error);
|
||||
if (dataSource && dataSource.isInitialized) {
|
||||
await dataSource.destroy();
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行初始化
|
||||
initUsersTable();
|
||||
72
src/scripts/list-users.ts
Normal file
72
src/scripts/list-users.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'dotenv/config';
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
|
||||
// 数据库配置
|
||||
const dbConfig: DataSourceOptions = {
|
||||
type: (process.env.DATABASE_TYPE as any) || 'mysql',
|
||||
host: process.env.DATABASE_HOST || 'localhost',
|
||||
port: parseInt(process.env.DATABASE_PORT || '3306'),
|
||||
username: process.env.DATABASE_USERNAME || 'root',
|
||||
password: process.env.DATABASE_PASSWORD || 'root',
|
||||
database: process.env.DATABASE_NAME || 'bidding',
|
||||
entities: [User],
|
||||
synchronize: false,
|
||||
};
|
||||
|
||||
// 日志工具
|
||||
const logger = {
|
||||
log: (message: string, ...args: any[]) => {
|
||||
console.log(`[INFO] ${new Date().toISOString()} - ${message}`, ...args);
|
||||
},
|
||||
error: (message: string, ...args: any[]) => {
|
||||
console.error(`[ERROR] ${new Date().toISOString()} - ${message}`, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
// 主函数
|
||||
async function listUsers() {
|
||||
let dataSource: DataSource | null = null;
|
||||
|
||||
try {
|
||||
logger.log('开始获取用户列表...');
|
||||
|
||||
// 创建数据库连接
|
||||
dataSource = new DataSource(dbConfig);
|
||||
await dataSource.initialize();
|
||||
logger.log('数据库连接成功');
|
||||
|
||||
// 获取所有用户
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const users = await userRepository.find();
|
||||
|
||||
if (users.length === 0) {
|
||||
logger.log('当前没有用户');
|
||||
} else {
|
||||
logger.log(`共有 ${users.length} 个用户:`);
|
||||
console.log('');
|
||||
console.log('ID | 用户名 | 创建时间 | 更新时间');
|
||||
console.log('------------------------------------|-------------|------------------------|------------------------');
|
||||
users.forEach((user) => {
|
||||
const id = user.id;
|
||||
const username = user.username.padEnd(11);
|
||||
const createdAt = user.createdAt.toISOString().replace('T', ' ').substring(0, 19);
|
||||
const updatedAt = user.updatedAt.toISOString().replace('T', ' ').substring(0, 19);
|
||||
console.log(`${id} | ${username} | ${createdAt} | ${updatedAt}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
await dataSource.destroy();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error('获取用户列表失败:', error);
|
||||
if (dataSource && dataSource.isInitialized) {
|
||||
await dataSource.destroy();
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行列出用户
|
||||
listUsers();
|
||||
25
src/users/entities/user.entity.ts
Normal file
25
src/users/entities/user.entity.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('users')
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
username: string;
|
||||
|
||||
@Column()
|
||||
password: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
11
src/users/users.module.ts
Normal file
11
src/users/users.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
112
src/users/users.service.ts
Normal file
112
src/users/users.service.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
* @param username 用户名
|
||||
* @param password 明文密码
|
||||
* @returns 创建的用户
|
||||
*/
|
||||
async createUser(username: string, password: string): Promise<User> {
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { username },
|
||||
});
|
||||
if (existingUser) {
|
||||
throw new Error(`用户名 ${username} 已存在`);
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// 创建用户
|
||||
const user = this.userRepository.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
return await this.userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户名和密码
|
||||
* @param username 用户名
|
||||
* @param password 明文密码
|
||||
* @returns 验证成功返回用户对象,失败返回 null
|
||||
*/
|
||||
async validateUser(
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<User | null> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { username },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
if (!isPasswordValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有用户(不包含密码)
|
||||
* @returns 用户列表
|
||||
*/
|
||||
async findAll(): Promise<Omit<User, 'password'>[]> {
|
||||
const users = await this.userRepository.find();
|
||||
return users.map((user) => {
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查找用户
|
||||
* @param username 用户名
|
||||
* @returns 用户对象(不包含密码)
|
||||
*/
|
||||
async findByUsername(
|
||||
username: string,
|
||||
): Promise<Omit<User, 'password'> | null> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { username },
|
||||
});
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* @param id 用户 ID
|
||||
*/
|
||||
async deleteUser(id: string): Promise<void> {
|
||||
await this.userRepository.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名删除用户
|
||||
* @param username 用户名
|
||||
*/
|
||||
async deleteUserByUsername(username: string): Promise<void> {
|
||||
await this.userRepository.delete({ username });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user