项目结构
├── auth/ # 身份认证模块
│ ├── auth.controller.ts
│ ├── auth.module.ts
│ ├── auth.service.ts
│ └── jwt.strategy.ts
├── common/ # 公共模块 (日志、过滤器等)
│ ├── filters/
│ │ └── http-exception.filter.ts
│ ├── logger/
│ │ └── logger.service.ts
│ └── interceptors/
│ └── logging.interceptor.ts
├── database/ # 数据库模块
│ ├── entities/
│ │ ├── user.entity.ts
│ │ ├── role.entity.ts
│ │ └── permission.entity.ts
│ └── database.module.ts
├── roles/ # 角色与权限管理模块
│ ├── roles.controller.ts
│ ├── roles.module.ts
│ └── roles.service.ts
├── users/ # 用户管理模块
│ ├── users.controller.ts
│ ├── users.module.ts
│ └── users.service.ts
├── app.controller.ts # 应用控制器
├── app.module.ts # 主模块
└── main.ts # 启动文件
1. 数据库模块:配置 TypeORM 和实体
src/database/database.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { Role } from './entities/role.entity';
import { Permission } from './entities/permission.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'enterprise_db',
entities: [User, Role, Permission],
synchronize: true, // 生产环境中要关闭
}),
TypeOrmModule.forFeature([User, Role, Permission]),
],
exports: [TypeOrmModule],
})
export class DatabaseModule {}
src/database/entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm';
import { Role } from './role.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column()
password: string;
@ManyToMany(() => Role)
@JoinTable()
roles: Role[];
}
src/database/entities/role.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany } from 'typeorm';
import { Permission } from './permission.entity';
import { User } from './user.entity';
@Entity()
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Permission)
permissions: Permission[];
@ManyToMany(() => User, user => user.roles)
users: User[];
}
src/database/entities/permission.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Permission {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
2. 用户与角色管理模块
src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../database/entities/user.entity';
import { Role } from '../database/entities/role.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
@InjectRepository(Role)
private rolesRepository: Repository<Role>,
) {}
async findOne(username: string): Promise<User | undefined> {
return this.usersRepository.findOne({
where: { username },
relations: ['roles'],
});
}
async createUser(username: string, password: string, roleIds: number[]): Promise<User> {
const user = this.usersRepository.create({ username, password });
user.roles = await this.rolesRepository.findByIds(roleIds);
return this.usersRepository.save(user);
}
}
src/users/users.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post('create')
async createUser(@Body() createUserDto: { username: string, password: string, roleIds: number[] }) {
return this.usersService.createUser(createUserDto.username, createUserDto.password, createUserDto.roleIds);
}
}
3. 身份验证与权限控制
src/auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-jwt';
import { ExtractJwt } from 'passport-jwt';
import { AuthService } from './auth.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'your-secret-key', // 请替换为环境变量
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly usersService: UsersService,
) {}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
return user;
}
return null;
}
}
4. 日志与全局异常处理
src/common/logger/logger.service.ts
import { Injectable } from '@nestjs/common';
import * as winston from 'winston';
@Injectable()
export class LoggerService {
private readonly logger: winston.Logger;
constructor() {
this.logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console({ format: winston.format.simple() }),
new winston.transports.File({ filename: 'logs/app.log' }),
],
});
}
log(message: string) {
this.logger.info(message);
}
error(message: string, trace: string) {
this.logger.error(`${message} - ${trace}`);
}
warn(message: string) {
this.logger.warn(message);
}
}
src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { Response } from 'express';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.status || 500;
response.status(status).json({
statusCode: status,
message: exception.message || 'Internal server error',
});
}
}
5. 主模块配置
src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { RolesModule } from './roles/roles.module';
import { DatabaseModule } from './database/database.module';
import { LoggerService } from './common/logger/logger.service';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
@Module({
imports: [
AuthModule,
UsersModule,
RolesModule,
DatabaseModule,
],
providers: [
LoggerService,
{
provide: 'APP_FILTER',
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
1. 数据库连接配置提取
为了实现更灵活的数据库配置,可以使用 @nestjs/config 模块将数据库连接信息提取到配置文件中。
首先,安装 @nestjs/config 依赖:
npm install @nestjs/config
src/config/database.config.ts - 数据库配置
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'enterprise_db',
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: process.env.NODE_ENV !== 'production',
}));
src/app.module.ts - 导入配置模块
然后,在 app.module.ts 中引入配置模块并加载数据库配置:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { RolesModule } from './roles/roles.module';
import { DatabaseModule } from './database/database.module';
import { LoggerService } from './common/logger/logger.service';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import databaseConfig from './config/database.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [databaseConfig],
isGlobal: true, // 配置为全局
}),
AuthModule,
UsersModule,
RolesModule,
DatabaseModule,
],
providers: [
LoggerService,
{
provide: 'APP_FILTER',
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
src/database/database.module.ts - 修改数据库模块以支持动态配置
在 database.module.ts 中,我们使用 ConfigService 来获取数据库连接信息。
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';
import { User } from './entities/user.entity';
import { Role } from './entities/role.entity';
import { Permission } from './entities/permission.entity';
@Module({
imports: [
TypeOrmModule.forRootAsync({
useFactory: async (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('database.host'),
port: configService.get('database.port'),
username: configService.get('database.username'),
password: configService.get('database.password'),
database: configService.get('database.database'),
entities: [User, Role, Permission],
synchronize: configService.get('database.synchronize'),
}),
inject: [ConfigService],
}),
TypeOrmModule.forFeature([User, Role, Permission]),
],
exports: [TypeOrmModule],
})
export class DatabaseModule {}
2. 单元测试:补全 AuthService 的测试模块
在这个例子中,我们将为 AuthService 编写测试。我们会使用 Jest 模拟 UsersService 和 JwtService,来验证 AuthService 是否按预期工作。
src/auth/auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import { User } from '../database/entities/user.entity';
describe('AuthService', () => {
let authService: AuthService;
let usersService: UsersService;
let jwtService: JwtService;
beforeEach(async () => {
const mockUsersService = {
findOne: jest.fn().mockResolvedValue({
id: 1,
username: 'test',
password: 'test123',
roles: [],
} as User),
};
const mockJwtService = {
sign: jest.fn().mockReturnValue('jwt-token'),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{ provide: UsersService, useValue: mockUsersService },
{ provide: JwtService, useValue: mockJwtService },
],
}).compile();
authService = module.get<AuthService>(AuthService);
usersService = module.get<UsersService>(UsersService);
jwtService = module.get<JwtService>(JwtService);
});
it('should be defined', () => {
expect(authService).toBeDefined();
});
it('should return a JWT token when login is successful', async () => {
const result = await authService.login({ username: 'test', password: 'test123' });
expect(result.access_token).toEqual('jwt-token');
});
it('should validate user and return user info', async () => {
const user = await authService.validateUser('test', 'test123');
expect(user).toBeDefined();
expect(user.username).toBe('test');
});
it('should return null if user credentials are invalid', async () => {
const user = await authService.validateUser('test', 'wrong-password');
expect(user).toBeNull();
});
it('should call findOne method of UsersService', async () => {
await authService.validateUser('test', 'test123');
expect(usersService.findOne).toHaveBeenCalledWith('test');
});
it('should call sign method of JwtService', async () => {
await authService.login({ username: 'test', password: 'test123' });
expect(jwtService.sign).toHaveBeenCalledWith({ username: 'test', sub: 1 });
});
});
3. 环境变量配置
为了避免将敏感信息硬编码到代码中,您可以在项目根目录下创建 .env 文件,存储数据库配置信息,并确保这些值在项目中能够访问到。
.env 示例:
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=password
DB_NAME=enterprise_db
NODE_ENV=development
使 ConfigModule 加载 .env 文件
NestJS 的 ConfigModule 默认会自动加载 .env 文件。确保在 app.module.ts 中已启用 ConfigModule.forRoot():
ConfigModule.forRoot({
isGlobal: true, // 确保配置文件全局可用
envFilePath: '.env', // 加载 .env 文件
})