feat: 解决冲突
commit
f11b7e85e1
8
.env
8
.env
|
|
@ -12,7 +12,7 @@ SERVER_HOST = 0.0.0.0
|
|||
# 服务域名
|
||||
SERVER_URL = http://127.0.0.1
|
||||
# 接口地址
|
||||
SERVER_OPENAPI_URL = /openapi
|
||||
SERVER_OPENAPI_URL = /api
|
||||
|
||||
# ========================================================================================
|
||||
# 数据库配置
|
||||
|
|
@ -20,7 +20,7 @@ SERVER_OPENAPI_URL = /openapi
|
|||
# 数据库类型
|
||||
DB_TYPE = sqlite
|
||||
# sqlite数据库地址
|
||||
DB_SQLITE_PATH = ./database/db.sqlite
|
||||
DB_SQLITE_PATH = ./content/database/db.sqlite
|
||||
# mysql数据库地址
|
||||
DB_MYSQL_HOST = 127.0.0.1
|
||||
# mysql数据库端口
|
||||
|
|
@ -36,9 +36,9 @@ DB_MYSQL_DATABASE = test1
|
|||
# 上传和静态文件配置
|
||||
# ========================================================================================
|
||||
# 上传文件目录
|
||||
UPLOAD_DIR = ./uploads
|
||||
UPLOAD_DIR = ./content/uploads
|
||||
# 静态文件目录
|
||||
STATIC_DIR = ./public
|
||||
STATIC_DIR = ./content/html
|
||||
|
||||
# ========================================================================================
|
||||
# 分页配置
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -20,7 +20,7 @@
|
|||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"orm": "typeorm-ts-node-esm -d ./src/features/typeorm/datasource/index.ts"
|
||||
"orm": "typeorm-ts-node-esm -d ./src/database/datasource/index.ts"
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 120,
|
||||
|
|
|
|||
4334
pnpm-lock.yaml
4334
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -1,61 +1,71 @@
|
|||
import { ClassSerializerInterceptor, Global, Module } from '@nestjs/common';
|
||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PostModule } from '@/modules/post';
|
||||
import { RoleModule } from '@/modules/role';
|
||||
import { UploadModule } from '@/modules/upload';
|
||||
import { PermissionModule } from '@/modules/permission';
|
||||
import { PermissionGuard } from '@/features/permission/permission.guard';
|
||||
import { ConfigModule } from '@/config';
|
||||
import { LoggerInterceptor, LoggerModule } from '@/features/logger';
|
||||
import { ServeStaticModule } from '@/features/static';
|
||||
import { BaseModule } from '@/features/base';
|
||||
import { AllExecptionFilter, HttpExecptionFilter } from '@/features/exception';
|
||||
import { ResponseInterceptor } from '@/features/response';
|
||||
import { TypeormModule } from '@/features/typeorm';
|
||||
import { validationPipeFactory, ValidationExecptionFilter } from '@/features/validation';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { AuthModule, JwtGuard } from '@/modules/auth';
|
||||
import { LoggerModule } from '@/common/logger';
|
||||
import { ServeStaticModule } from '@/common/static';
|
||||
import { DatabaseModule } from '@/database';
|
||||
import { ValidationModule } from '@/common/validation';
|
||||
import { AuthModule } from '@/modules/auth';
|
||||
import { UserModule } from '@/modules/user';
|
||||
import { ResponseModule } from '@/common/response';
|
||||
import { SerializationModule } from '@/common/serialization';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
/**
|
||||
* 配置模块(全局),提供ConfigService类
|
||||
* 配置模块(全局)
|
||||
* @description 加载.env配置文件
|
||||
*/
|
||||
ConfigModule,
|
||||
/**
|
||||
* 日志模块(全局),提供LoggerService类
|
||||
* 日志模块(全局)
|
||||
* @description 用于记录日志
|
||||
*/
|
||||
LoggerModule,
|
||||
/**
|
||||
* 静态资源(全局),/upload和/web
|
||||
* 静态资源(全局)
|
||||
* @description 为静态页面/上传文件提供服务
|
||||
*/
|
||||
ServeStaticModule,
|
||||
/**
|
||||
* 基础模块(全局),提供基础服务
|
||||
* 序列化模块
|
||||
* @description 序列化响应结果/异常结果,移除/替换字段
|
||||
*/
|
||||
BaseModule,
|
||||
SerializationModule,
|
||||
/**
|
||||
* 响应模块
|
||||
* @description 包装响应结果/异常结果
|
||||
*/
|
||||
ResponseModule,
|
||||
/**
|
||||
* 校验模块
|
||||
* @description 校验请求参数,抛出异常时按响应模块的格式包装
|
||||
*/
|
||||
ValidationModule,
|
||||
/**
|
||||
* 数据库ORM
|
||||
* @description 用于连接数据库
|
||||
*/
|
||||
TypeormModule,
|
||||
DatabaseModule,
|
||||
/**
|
||||
* 用户模块
|
||||
*/
|
||||
UserModule,
|
||||
/**
|
||||
* 账户模块
|
||||
* 登陆模块
|
||||
*/
|
||||
AuthModule,
|
||||
/**
|
||||
* JWT模块
|
||||
*/
|
||||
JwtModule,
|
||||
/**
|
||||
* 角色模块
|
||||
*/
|
||||
RoleModule,
|
||||
/**
|
||||
* 权限模块
|
||||
*/
|
||||
PermissionModule,
|
||||
/**
|
||||
* 上传模块
|
||||
*/
|
||||
|
|
@ -64,82 +74,6 @@ import { UserModule } from '@/modules/user';
|
|||
* 文章模块
|
||||
*/
|
||||
PostModule,
|
||||
/**
|
||||
* 权限模块
|
||||
*/
|
||||
PermissionModule,
|
||||
],
|
||||
providers: [
|
||||
/**
|
||||
* 全局序列化拦截器
|
||||
* @description 由于中间件的洋葱机制,需放在响应拦截器之前,否则无法检测到实例类型
|
||||
*/
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: ClassSerializerInterceptor,
|
||||
},
|
||||
/**
|
||||
* 全局响应拦截器
|
||||
* @description 将返回值统一包装成{code, message, data, meta}格式
|
||||
*/
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: ResponseInterceptor,
|
||||
},
|
||||
/**
|
||||
* 全局日志拦截器
|
||||
* @description 将请求和响应日志打印到控制台
|
||||
*/
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: LoggerInterceptor,
|
||||
},
|
||||
/**
|
||||
* 全局异常过滤器
|
||||
* @description 将异常统一包装成{code, message, data, meta}格式
|
||||
*/
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: AllExecptionFilter,
|
||||
},
|
||||
/**
|
||||
* 全局HTTP异常过滤器
|
||||
* @description 将HTTP异常统一包装成{code, message, data, meta}格式
|
||||
*/
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: HttpExecptionFilter,
|
||||
},
|
||||
/**
|
||||
* 全局验证管道
|
||||
* @description 校验和转换输入数据
|
||||
*/
|
||||
{
|
||||
provide: APP_PIPE,
|
||||
useFactory: validationPipeFactory,
|
||||
},
|
||||
/**
|
||||
* 全局验证异常过滤器
|
||||
* @description 将验证异常统一包装成{code, message, data, meta}格式
|
||||
*/
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: ValidationExecptionFilter,
|
||||
},
|
||||
/**
|
||||
* 全局JWT守卫(校验是否登陆)
|
||||
*/
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtGuard,
|
||||
},
|
||||
/**
|
||||
* 全局权限守卫(校验是否有权限)
|
||||
*/
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: PermissionGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Inject } from '@nestjs/common';
|
||||
import { LoggerService } from '../logger';
|
||||
import { ConfigService } from '@/config';
|
||||
|
||||
/**
|
||||
* 基础控制器
|
||||
* 控制器基类
|
||||
*/
|
||||
export class BaseController {
|
||||
/**
|
||||
|
|
@ -10,4 +11,10 @@ export class BaseController {
|
|||
*/
|
||||
@Inject(LoggerService)
|
||||
readonly logger: LoggerService;
|
||||
|
||||
/**
|
||||
* 配置服务
|
||||
*/
|
||||
@Inject(ConfigService)
|
||||
readonly config: ConfigService;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({})
|
||||
export class BaseModule {}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { Inject } from '@nestjs/common';
|
||||
import { ConfigService } from '@/config';
|
||||
|
||||
/**
|
||||
* 服务基类
|
||||
*/
|
||||
export class BaseService {
|
||||
/**
|
||||
* 配置服务
|
||||
*/
|
||||
@Inject(ConfigService)
|
||||
protected readonly config: ConfigService;
|
||||
|
||||
/**
|
||||
* 格式化分页参数
|
||||
*/
|
||||
formatPagination(page = this.config.defaultPage, size = this.config.defaultPageSize) {
|
||||
return {
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Global, Module } from '@nestjs/common';
|
||||
import { LoggerService } from './logger.service';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { LoggerInterceptor } from './logger.interceptor';
|
||||
|
||||
/**
|
||||
* 日志模块
|
||||
* @description 包含全局拦截器
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
LoggerService,
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: LoggerInterceptor,
|
||||
},
|
||||
],
|
||||
exports: [LoggerService],
|
||||
})
|
||||
export class LoggerModule {}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
|
||||
import { Response as _Response } from 'express';
|
||||
import { Response } from '../response';
|
||||
import { Response } from './response';
|
||||
|
||||
@Catch(HttpException)
|
||||
export class HttpExecptionFilter implements ExceptionFilter {
|
||||
|
|
@ -2,3 +2,7 @@ export * from './response';
|
|||
export * from './response.code';
|
||||
export * from './response.decorator';
|
||||
export * from './response.interceptor';
|
||||
export * from './response.module';
|
||||
export * from './pagination.dto';
|
||||
export * from './http.filter';
|
||||
export * from './notcaptured.filter';
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';
|
||||
import { Response as _Response } from 'express';
|
||||
import { Response, ResponseCode } from '../response';
|
||||
import { Response } from './response';
|
||||
import { ResponseCode } from './response.code';
|
||||
|
||||
@Catch()
|
||||
export class AllExecptionFilter implements ExceptionFilter {
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import { Transform } from 'class-transformer';
|
||||
import { IsNumber, IsOptional, Min } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 分页 DTO
|
||||
* @example { page: 1, size: 10 }
|
||||
*/
|
||||
export class PaginationDto {
|
||||
/**
|
||||
* 页码
|
||||
* @example 1
|
||||
*/
|
||||
// @IsNumber()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
|
|
@ -14,6 +18,7 @@ export class PaginationDto {
|
|||
|
||||
/**
|
||||
* 每页条数
|
||||
* @example 10
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
|
|
@ -4,12 +4,11 @@ import { Observable, map } from 'rxjs';
|
|||
import { Response } from './response';
|
||||
import { RESPONSE_KEY, Respond } from './response.decorator';
|
||||
import { Request } from 'express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Config } from '@/config';
|
||||
import { ConfigService } from '@/config';
|
||||
|
||||
@Injectable()
|
||||
export class ResponseInterceptor implements NestInterceptor {
|
||||
constructor(private reflector: Reflector, private configService: ConfigService<Config>) {}
|
||||
constructor(private reflector: Reflector, private config: ConfigService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const controller = context.getClass();
|
||||
|
|
@ -24,8 +23,8 @@ export class ResponseInterceptor implements NestInterceptor {
|
|||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const [list, total] = data;
|
||||
if (request.query.meta) {
|
||||
const page = Number(request.query.page || this.configService.get('DEFAULT_PAGE', 1));
|
||||
const size = Number(request.query.size || this.configService.get('DEFAULT_SIZE', 10));
|
||||
const page = Number(request.query.page || this.config.defaultPage);
|
||||
const size = Number(request.query.size || this.config.defaultPageSize);
|
||||
return Response.success({
|
||||
data: list,
|
||||
meta: {
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { AllExecptionFilter } from './notcaptured.filter';
|
||||
import { HttpExecptionFilter } from './http.filter';
|
||||
import { ResponseInterceptor } from './response.interceptor';
|
||||
|
||||
/**
|
||||
* 响应模块
|
||||
* @description 包含全局异常/HTTP异常/响应结果拦截器
|
||||
*/
|
||||
@Module({
|
||||
providers: [
|
||||
/**
|
||||
* 全局异常过滤器
|
||||
* @description 将异常统一包装成{code, message, data, meta}格式
|
||||
*/
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: AllExecptionFilter,
|
||||
},
|
||||
/**
|
||||
* 全局HTTP异常过滤器
|
||||
* @description 将HTTP异常统一包装成{code, message, data, meta}格式
|
||||
*/
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: HttpExecptionFilter,
|
||||
},
|
||||
/**
|
||||
* 全局响应拦截器
|
||||
* @description 将返回值统一包装成{code, message, data, meta}格式
|
||||
*/
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: ResponseInterceptor,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class ResponseModule {}
|
||||
|
|
@ -9,20 +9,14 @@ export class Response<T = any> {
|
|||
* 状态码
|
||||
* @example 2000
|
||||
*/
|
||||
@ApiProperty({
|
||||
type: 'number',
|
||||
example: 2000,
|
||||
})
|
||||
@ApiProperty({ type: 'number', example: 2000 })
|
||||
code?: ResponseCode;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
* @example '请求成功'
|
||||
*/
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
example: '请求成功',
|
||||
})
|
||||
@ApiProperty({ type: 'string', example: '请求成功' })
|
||||
message?: string;
|
||||
|
||||
/**
|
||||
|
|
@ -40,6 +34,7 @@ export class Response<T = any> {
|
|||
|
||||
/**
|
||||
* 数据总数
|
||||
* @example 100
|
||||
*/
|
||||
total?: number;
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './serialization.module';
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { ClassSerializerInterceptor, Module } from '@nestjs/common';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
|
||||
/**
|
||||
* 序列化模块
|
||||
* @description 包含全局序列化拦截器
|
||||
*/
|
||||
@Module({
|
||||
providers: [
|
||||
/**
|
||||
* 全局序列化拦截器
|
||||
* @description 由于中间件的洋葱机制,需放在响应拦截器之前,否则无法检测到实例类型
|
||||
*/
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: ClassSerializerInterceptor,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class SerializationModule {}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './static.module';
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { ConfigService } from '@/config';
|
||||
import { ServeStaticModule as _ServeStaticModule } from '@nestjs/serve-static';
|
||||
|
||||
/**
|
||||
* 静态资源模块
|
||||
* @see https://docs.nestjs.com/techniques/mvc#serve-static
|
||||
*/
|
||||
export const ServeStaticModule = _ServeStaticModule.forRootAsync({
|
||||
useFactory: (config: ConfigService) => {
|
||||
return [
|
||||
{
|
||||
rootPath: config.uploadDir,
|
||||
serveRoot: config.uploadPrefix,
|
||||
},
|
||||
{
|
||||
rootPath: config.staticDir,
|
||||
},
|
||||
];
|
||||
},
|
||||
inject: [ConfigService],
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@/config';
|
||||
import { DocumentBuilder, SwaggerDocumentOptions, SwaggerModule } from '@nestjs/swagger';
|
||||
import { addResponseWrapper } from './util';
|
||||
|
||||
/**
|
||||
* 初始化Swagger
|
||||
* @param app 应用实例
|
||||
*/
|
||||
export const initSwagger = (app: INestApplication) => {
|
||||
const config = app.get(ConfigService);
|
||||
const docConfig = new DocumentBuilder()
|
||||
.setTitle(`${config.title}接口文档`)
|
||||
.setVersion('1.0')
|
||||
.setDescription('Openapi 3.0文档')
|
||||
.setExternalDoc('JSON数据', `${config.apiDocPrefix}.json`)
|
||||
.addTag('user', '用户管理')
|
||||
.addTag('auth', '认证管理')
|
||||
.addTag('role', '角色管理')
|
||||
.addTag('permission', '权限管理')
|
||||
.addTag('post', '文章管理')
|
||||
.addTag('upload', '文件上传')
|
||||
.build();
|
||||
const options: SwaggerDocumentOptions = {
|
||||
operationIdFactory(controllerKey, methodKey) {
|
||||
return `${controllerKey}_${methodKey}`;
|
||||
},
|
||||
};
|
||||
const document = addResponseWrapper(SwaggerModule.createDocument(app, docConfig, options));
|
||||
SwaggerModule.setup(config.apiDocPrefix, app, document, {
|
||||
jsonDocumentUrl: `${config.apiDocPrefix}.json`,
|
||||
yamlDocumentUrl: `${config.apiDocPrefix}.yaml`,
|
||||
customfavIcon: '/favicon.ico',
|
||||
customSiteTitle: `接口文档 | ${config.subtitle}`,
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { OpenAPIObject } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 为所有接口添加统一的返回数据结构
|
||||
* @param doc OPENAPI文档对象
|
||||
* @example
|
||||
* ```json
|
||||
* {
|
||||
* "code": 2000,
|
||||
* "message": "请求成功",
|
||||
* "data": []
|
||||
* }
|
||||
* ```
|
||||
* @returns
|
||||
*/
|
||||
export function addResponseWrapper(doc: OpenAPIObject) {
|
||||
for (const path of Object.keys(doc.paths)) {
|
||||
const pathItem = doc.paths[path];
|
||||
if (!pathItem) {
|
||||
continue;
|
||||
}
|
||||
for (const method of Object.keys(pathItem)) {
|
||||
const responses = doc.paths[path][method].responses;
|
||||
if (!responses) {
|
||||
continue;
|
||||
}
|
||||
for (const status of Object.keys(responses)) {
|
||||
const json = responses[status].content?.['application/json'];
|
||||
if (!json) {
|
||||
continue;
|
||||
}
|
||||
const schema = json.schema;
|
||||
json.schema = {
|
||||
allOf: [
|
||||
{
|
||||
$ref: '#/components/schemas/Response',
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: schema,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doc.components.schemas.Response = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: {
|
||||
type: 'integer',
|
||||
description: '状态码',
|
||||
example: 2000,
|
||||
format: 'int32',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: '提示信息',
|
||||
example: '请求成功',
|
||||
},
|
||||
},
|
||||
required: ['code', 'message'],
|
||||
};
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './validation.error';
|
||||
export * from './validation.filter';
|
||||
export * from './validation.pipe';
|
||||
export * from './validation.module';
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { ResponseCode } from '../response';
|
||||
import { AppValidationError } from './validation.error';
|
||||
|
|
@ -11,6 +11,6 @@ export class ValidationExecptionFilter implements ExceptionFilter {
|
|||
const code = ResponseCode.PARAM_ERROR;
|
||||
const message = exception.message;
|
||||
const data = exception.messages;
|
||||
response.status(400).json({ code, message, data });
|
||||
response.status(HttpStatus.BAD_REQUEST).json({ code, message, data });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
|
||||
import { validationPipeFactory } from './validation.pipe';
|
||||
import { ValidationExecptionFilter } from './validation.filter';
|
||||
|
||||
/**
|
||||
* 校验模块
|
||||
* @description 包含全局验证管道和全局验证异常过滤器
|
||||
*/
|
||||
@Module({
|
||||
providers: [
|
||||
/**
|
||||
* 全局验证管道
|
||||
* @description 校验和转换输入数据
|
||||
*/
|
||||
{
|
||||
provide: APP_PIPE,
|
||||
useFactory: validationPipeFactory,
|
||||
},
|
||||
/**
|
||||
* 全局验证异常过滤器
|
||||
* @description 将验证异常统一包装成{code, message, data, meta}格式
|
||||
*/
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: ValidationExecptionFilter,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class ValidationModule {}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
export interface Config {
|
||||
/**
|
||||
* 服务器端口
|
||||
*/
|
||||
SERVER_PORT: number;
|
||||
/**
|
||||
* 服务器地址
|
||||
*/
|
||||
SERVER_HOST: string;
|
||||
/**
|
||||
* OPENAPI 地址
|
||||
*/
|
||||
SERVER_OPENAPI_URL: string;
|
||||
/**
|
||||
* 应用名称
|
||||
*/
|
||||
APP_TITLE: string;
|
||||
/**
|
||||
* 应用副标题
|
||||
*/
|
||||
APP_SUBTITLE: string;
|
||||
/**
|
||||
* 数据库类型
|
||||
*/
|
||||
DB_TYPE: string;
|
||||
/**
|
||||
* SQLite 数据库文件路径
|
||||
*/
|
||||
DB_SQLITE_PATH: string;
|
||||
/**
|
||||
* 上传文件目录
|
||||
*/
|
||||
UPLOAD_DIR: string;
|
||||
/**
|
||||
* 上传文件 URL
|
||||
*/
|
||||
UPLOAD_URL: string;
|
||||
/**
|
||||
* 静态文件目录
|
||||
*/
|
||||
STATIC_DIR: string;
|
||||
/**
|
||||
* 默认分页
|
||||
*/
|
||||
DEFAULT_PAGE: number;
|
||||
/**
|
||||
* 默认分页大小
|
||||
*/
|
||||
DEFAULT_SIZE: number;
|
||||
/**
|
||||
* JWT 密钥
|
||||
*/
|
||||
JWT_SECRET: string;
|
||||
}
|
||||
|
|
@ -1,6 +1,19 @@
|
|||
import { Global, Module } from '@nestjs/common';
|
||||
import { ConfigModule as _ConfigModule } from '@nestjs/config';
|
||||
import { ConfigService } from './config.service';
|
||||
|
||||
export const ConfigModule = _ConfigModule.forRoot({
|
||||
envFilePath: ['.env.development', '.env.local', '.env'],
|
||||
isGlobal: true,
|
||||
});
|
||||
/**
|
||||
* 配置模块
|
||||
* @description 基于 `@nestjs/config` 封装,提供更便捷且类型安全的配置读取方式
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
_ConfigModule.forRoot({
|
||||
envFilePath: ['.env.development.locale', '.env.development', '.env.local', '.env'],
|
||||
}),
|
||||
],
|
||||
providers: [ConfigService],
|
||||
exports: [ConfigService],
|
||||
})
|
||||
export class ConfigModule {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService as _ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class ConfigService {
|
||||
constructor(
|
||||
/**
|
||||
* `@nestjs/config` 的 ConfigService实例
|
||||
*/
|
||||
public config: _ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 保留原有的get方法
|
||||
*/
|
||||
get(...args: [string, any]) {
|
||||
return this.config.get(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标题
|
||||
* @default 'Appnify'
|
||||
*/
|
||||
get title(): string {
|
||||
return this.config.get('TITLE', 'Appnify');
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用CORS(跨域)
|
||||
* @default true
|
||||
*/
|
||||
get cors(): boolean {
|
||||
return this.config.get('CORS', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 副标题
|
||||
* @default 'Appnify'
|
||||
*/
|
||||
get subtitle(): string {
|
||||
return this.config.get('SUBTITLE', 'Appnify');
|
||||
}
|
||||
|
||||
/**
|
||||
* API前缀
|
||||
* @default '/api'
|
||||
*/
|
||||
get apiPrefix(): string {
|
||||
return this.config.get('API_PREFIX', '/api');
|
||||
}
|
||||
|
||||
/**
|
||||
* API默认版本
|
||||
* @default '1'
|
||||
*/
|
||||
get apiVersion(): string {
|
||||
return this.config.get('API_VERSION', '1');
|
||||
}
|
||||
|
||||
/**
|
||||
* API文档前缀
|
||||
* @default '/openapi'
|
||||
*/
|
||||
get apiDocPrefix(): string {
|
||||
const prefix = this.config.get('API_DOC_PREFIX', '/openapi');
|
||||
return prefix.startsWith('/') ? prefix : `/${prefix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行端口
|
||||
* @default 3030
|
||||
*/
|
||||
get port(): number {
|
||||
return Number(this.config.get('SERVER_PORT', 3030));
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行地址
|
||||
* @default '0:0:0:0'
|
||||
*/
|
||||
get host(): string {
|
||||
return this.config.get('SERVER_HOST', '0:0:0:0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库类型
|
||||
* @default 'sqlite'
|
||||
*/
|
||||
get dbType(): 'sqlite' | 'mysql' | 'mongodb' {
|
||||
return this.config.get('DB_TYPE', 'sqlite');
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLite数据库文件路径
|
||||
* @default './content/db.sqlite'
|
||||
*/
|
||||
get dbSqlitePath(): string {
|
||||
return this.config.get('DB_SQLITE_PATH', './content/db.sqlite');
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件目录
|
||||
* @default './content/uploads'
|
||||
*/
|
||||
get uploadDir(): string {
|
||||
return this.config.get('UPLOAD_DIR', './content/uploads');
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件URL前缀
|
||||
* @default '/uploads'
|
||||
*/
|
||||
get uploadPrefix(): string {
|
||||
return this.config.get('UPLOAD_URL', '/uploads');
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态文件目录
|
||||
* @default './content/static'
|
||||
*/
|
||||
get staticDir(): string {
|
||||
return this.config.get('STATIC_DIR', './content/static');
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认页码
|
||||
* @default 1
|
||||
*/
|
||||
get defaultPage(): number {
|
||||
return Number(this.config.get('DEFAULT_PAGE_NUMBER', 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认分页大小
|
||||
* @default 10
|
||||
*/
|
||||
get defaultPageSize(): number {
|
||||
return Number(this.config.get('DEFAULT_PAGE_SIZE', 10));
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT密钥
|
||||
* @default 'todo'
|
||||
*/
|
||||
get jwtSecret(): string {
|
||||
return this.config.get('JWT_SECRET', 'todo');
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志保存目录
|
||||
* @default './content/logs'
|
||||
*/
|
||||
get logDir(): string {
|
||||
return this.config.get('LOG_DIR', './content/logs');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
export * from './config.module';
|
||||
export * from './config.interface';
|
||||
export * from './config.service';
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
/**
|
||||
* 环境变量枚举
|
||||
*/
|
||||
export enum envKeys {
|
||||
/**
|
||||
* 服务器地址
|
||||
*/
|
||||
SERVER_HOST = 'SERVER_HOST',
|
||||
/**
|
||||
* 服务器端口
|
||||
*/
|
||||
SERVER_PORT = 'SERVER_PORT',
|
||||
/**
|
||||
* 上传文件夹
|
||||
*/
|
||||
UPLOAD_DIR = 'UPLOAD_DIR',
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './env';
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigService } from '@/config';
|
||||
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
||||
|
||||
/**
|
||||
* 数据库模块
|
||||
* @description 基于 `typeorm` 封装
|
||||
*/
|
||||
export const DatabaseModule = TypeOrmModule.forRootAsync({
|
||||
useFactory: (config: ConfigService) => {
|
||||
if (config.dbType === 'sqlite') {
|
||||
return {
|
||||
type: config.dbType,
|
||||
database: config.dbSqlitePath,
|
||||
synchronize: true,
|
||||
autoLoadEntities: true,
|
||||
namingStrategy: new SnakeNamingStrategy(),
|
||||
};
|
||||
}
|
||||
if (config.dbType === 'mysql') {
|
||||
}
|
||||
},
|
||||
inject: [ConfigService],
|
||||
});
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { DataSource } from 'typeorm';
|
||||
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
||||
import { CreateUsersTable1682693329275 } from '../migrations/1682693329275-CreateUsersTable';
|
||||
import { MockPosts1685026010848 } from '../migrations/1685026010848-MockPosts';
|
||||
|
||||
/**
|
||||
* 用于生成迁移文件
|
||||
*/
|
||||
export default new DataSource({
|
||||
type: 'sqlite',
|
||||
database: 'database/db.sqlite',
|
||||
logging: false,
|
||||
namingStrategy: new SnakeNamingStrategy(),
|
||||
entities: ['src/**/*.entity.ts'],
|
||||
migrations: [CreateUsersTable1682693329275, MockPosts1685026010848],
|
||||
});
|
||||
|
|
@ -2,9 +2,10 @@ import { Exclude } from 'class-transformer';
|
|||
import { Column, CreateDateColumn, DeleteDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
/**
|
||||
* 基础实体, 查询时默认按照id倒序
|
||||
* 基础实体
|
||||
* @description 所有实体都应该继承该类
|
||||
*/
|
||||
@Entity({ orderBy: { id: 'DESC' } })
|
||||
@Entity()
|
||||
export class BaseEntity {
|
||||
/**
|
||||
* 自增ID
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './datasource';
|
||||
export * from './entities/base';
|
||||
export * from './database.module';
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { Global, Module } from '@nestjs/common';
|
||||
import { LoggerService } from '../logger';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [LoggerService],
|
||||
})
|
||||
export class BaseModule {}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { Inject } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Config } from '@/config';
|
||||
|
||||
export class BaseService {
|
||||
@Inject(ConfigService)
|
||||
protected readonly configService: ConfigService<Config>;
|
||||
|
||||
/**
|
||||
* 格式化分页参数
|
||||
*/
|
||||
formatPagination(page?: number, size?: number) {
|
||||
const _page = page || this.configService.get('DEFAULT_PAGE', 1);
|
||||
const _size = size || this.configService.get('DEFAULT_SIZE', 10);
|
||||
return {
|
||||
skip: (_page - 1) * _size,
|
||||
take: _size,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from './all.filter';
|
||||
export * from './http.filter';
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { Global, Module } from '@nestjs/common';
|
||||
import { LoggerService } from './logger.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [LoggerService],
|
||||
exports: [LoggerService],
|
||||
})
|
||||
export class LoggerModule {}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from './pagination';
|
||||
export * from './pagination.dto';
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import { Response, ResponseCode } from '../response';
|
||||
|
||||
interface Options {
|
||||
page: number;
|
||||
size: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type WrapOptions<T> = {
|
||||
page: number;
|
||||
size: number;
|
||||
total: number;
|
||||
data: T[];
|
||||
};
|
||||
|
||||
export const defaultPage = 1;
|
||||
|
||||
export const defaultSize = 10;
|
||||
|
||||
/**
|
||||
* 分页工具类
|
||||
*/
|
||||
export class Pagination {
|
||||
/**
|
||||
* 包装响应结果
|
||||
*/
|
||||
static wrap<T>(options: WrapOptions<T>) {
|
||||
const { page = defaultPage, size = defaultSize, total, data } = options;
|
||||
return Response.create({
|
||||
code: ResponseCode.SUCESS,
|
||||
message: '请求成功',
|
||||
data,
|
||||
meta: { page, size, total },
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 将分页参数转换为typeorm查询参数
|
||||
*/
|
||||
static optionize(options: Options) {
|
||||
const { page = defaultPage, size: take = defaultSize, ...where } = options || {};
|
||||
const skip = (page - 1) * take;
|
||||
return { skip, take, where };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { ConfigService } from '@nestjs/config';
|
||||
import { ServeStaticModule as _ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { Config } from '@/config';
|
||||
|
||||
export const ServeStaticModule = _ServeStaticModule.forRootAsync({
|
||||
useFactory: (configService: ConfigService<Config>) => {
|
||||
return [
|
||||
{
|
||||
rootPath: configService.get<string>('UPLOAD_DIR', 'uploads'),
|
||||
serveRoot: configService.get<string>('UPLOAD_URL', '/uploads'),
|
||||
},
|
||||
{
|
||||
rootPath: configService.get<string>('STATIC_DIR', 'public'),
|
||||
},
|
||||
];
|
||||
},
|
||||
inject: [ConfigService],
|
||||
});
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { Config } from '@/config';
|
||||
|
||||
export const initSwagger = (app: INestApplication) => {
|
||||
const configService = app.get(ConfigService<Config>);
|
||||
const openapiUrl = configService.get<string>('SERVER_OPENAPI_URL', 'openapi');
|
||||
const appTitle = configService.get<string>('APP_TITLE', 'Apptify');
|
||||
const appSubtitle = configService.get<string>('APP_SUBTITLE', 'Apptify');
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle(`${appTitle}接口文档`)
|
||||
.setVersion('1.0')
|
||||
.setDescription('Openapi 3.0文档')
|
||||
.setExternalDoc('JSON数据', `${openapiUrl}.json`)
|
||||
.addTag('user', '用户管理')
|
||||
.addTag('auth', '认证管理')
|
||||
.addTag('role', '角色管理')
|
||||
.addTag('permission', '权限管理')
|
||||
.addTag('post', '文章管理')
|
||||
.addTag('upload', '文件上传')
|
||||
.build();
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup(openapiUrl, app, document, {
|
||||
jsonDocumentUrl: `${openapiUrl}.json`,
|
||||
yamlDocumentUrl: `${openapiUrl}.yaml`,
|
||||
customfavIcon: '/favicon.ico',
|
||||
customSiteTitle: `接口文档 | ${appSubtitle}`,
|
||||
});
|
||||
};
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
||||
import { CreateUsersTable1682693329275 } from '../migrations/1682693329275-CreateUsersTable';
|
||||
import { MockPosts1685026010848 } from '../migrations/1685026010848-MockPosts';
|
||||
|
||||
/**
|
||||
* 基本配置
|
||||
*/
|
||||
export const baseConfig: DataSourceOptions = {
|
||||
type: 'sqlite',
|
||||
database: 'database/db.sqlite',
|
||||
logging: false,
|
||||
namingStrategy: new SnakeNamingStrategy(),
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于运行时连接数据库
|
||||
*/
|
||||
export const ormConfig: TypeOrmModuleOptions = {
|
||||
...baseConfig,
|
||||
synchronize: true,
|
||||
autoLoadEntities: true,
|
||||
logging: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于生成迁移文件
|
||||
*/
|
||||
export const cliConfig: DataSourceOptions = {
|
||||
...baseConfig,
|
||||
entities: ['src/**/*.entity.ts'],
|
||||
migrations: [CreateUsersTable1682693329275, MockPosts1685026010848],
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于生成迁移文件
|
||||
*/
|
||||
export default new DataSource(cliConfig);
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ormConfig } from './datasource';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
||||
import { Config } from '@/config';
|
||||
export * from './datasource';
|
||||
export * from './entities/base';
|
||||
|
||||
/**
|
||||
* 连接数据库
|
||||
*/
|
||||
export const TypeormModule = TypeOrmModule.forRootAsync({
|
||||
useFactory: (configService: ConfigService<Config>) => {
|
||||
const type = configService.get<string>('DB_TYPE', 'sqlite');
|
||||
if (type === 'sqlite') {
|
||||
const database = configService.get<string>('DB_SQLITE_PATH', 'database/db.sqlite');
|
||||
return {
|
||||
type,
|
||||
database,
|
||||
synchronize: true,
|
||||
autoLoadEntities: true,
|
||||
namingStrategy: new SnakeNamingStrategy(),
|
||||
};
|
||||
}
|
||||
if (type === 'mysql') {
|
||||
}
|
||||
return ormConfig;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
});
|
||||
20
src/main.ts
20
src/main.ts
|
|
@ -1,17 +1,21 @@
|
|||
import { VersioningType } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { initSwagger } from '@/features/swagger';
|
||||
import { LoggerService } from '@/features/logger';
|
||||
import { initSwagger } from '@/common/swagger';
|
||||
import { LoggerService } from '@/common/logger';
|
||||
import { ConfigService } from '@/config';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const { SERVER_HOST, SERVER_PORT } = process.env;
|
||||
/**
|
||||
* 创建应用
|
||||
*/
|
||||
const app = await NestFactory.create(AppModule, { bufferLogs: false });
|
||||
/**
|
||||
* 使用全局日志
|
||||
* 获取配置服务
|
||||
*/
|
||||
const config = app.get(ConfigService);
|
||||
/**
|
||||
* 获取日志服务
|
||||
*/
|
||||
const logger = app.get(LoggerService);
|
||||
/**
|
||||
|
|
@ -25,11 +29,11 @@ async function bootstrap() {
|
|||
/**
|
||||
* API前缀
|
||||
*/
|
||||
app.setGlobalPrefix('/api');
|
||||
app.setGlobalPrefix(config.apiPrefix);
|
||||
/**
|
||||
* 接口版本
|
||||
*/
|
||||
app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' });
|
||||
app.enableVersioning({ type: VersioningType.URI, defaultVersion: config.apiVersion });
|
||||
/**
|
||||
* 接口文档(swagger)
|
||||
*/
|
||||
|
|
@ -37,7 +41,7 @@ async function bootstrap() {
|
|||
/**
|
||||
* 监听端口
|
||||
*/
|
||||
await app.listen(SERVER_PORT, SERVER_HOST);
|
||||
await app.listen(config.port, config.host);
|
||||
/**
|
||||
* 输出项目运行URL
|
||||
*/
|
||||
|
|
@ -45,7 +49,7 @@ async function bootstrap() {
|
|||
/**
|
||||
* 输出接口文档URL
|
||||
*/
|
||||
logger.log(`OpenapiDocs is running at ${await app.getUrl()}/openapi`, 'NestApplication');
|
||||
logger.log(`OpenapiDocs is running at ${await app.getUrl()}${config.apiDocPrefix}`, 'NestApplication');
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
|
|
|||
|
|
@ -2,12 +2,19 @@ import { Module } from '@nestjs/common';
|
|||
import { UserModule } from '../user';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtModule } from './jwt';
|
||||
import { JwtGuard, JwtModule } from './jwt';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
imports: [UserModule, JwtModule],
|
||||
providers: [AuthService],
|
||||
providers: [
|
||||
AuthService,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtGuard,
|
||||
},
|
||||
],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ import { Reflector } from '@nestjs/core';
|
|||
import { Request } from 'express';
|
||||
import { PUBLICK_KEY } from './jwt-decorator';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ConfigService } from '@/config';
|
||||
|
||||
@Injectable()
|
||||
export class JwtGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector, private jwtService: JwtService, private configService: ConfigService) {}
|
||||
constructor(private reflector: Reflector, private jwtService: JwtService, private config: ConfigService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
if (token) {
|
||||
const secret = this.configService.get('JWT_SECRET');
|
||||
const secret = this.config.jwtSecret;
|
||||
const user = await this.jwtService.verifyAsync(token, { secret });
|
||||
request['user'] = user;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Config } from '@/config';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ConfigService } from '@/config';
|
||||
import { JwtModule as _JwtModule } from '@nestjs/jwt';
|
||||
|
||||
export const JwtModule = _JwtModule.registerAsync({
|
||||
useFactory: (configService: ConfigService<Config>) => {
|
||||
useFactory: (config: ConfigService) => {
|
||||
return {
|
||||
secret: configService.get('JWT_SECRET', 'todo'),
|
||||
secret: config.jwtSecret,
|
||||
signOptions: {
|
||||
expiresIn: '60000s',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseEntity } from '@/features/typeorm';
|
||||
import { BaseEntity } from '@/database';
|
||||
import { Role } from '@/modules/role/entities/role.entity';
|
||||
import { Column, Entity, ManyToMany } from 'typeorm';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,3 +4,5 @@ export * from './entities/permission.entity';
|
|||
export * from './permission.controller';
|
||||
export * from './permission.module';
|
||||
export * from './permission.service';
|
||||
export * from './permission.decorator';
|
||||
export * from './permission.guard';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { PermissionService } from './permission.service';
|
|||
import { CreatePermissionDto } from './dto/create-permission.dto';
|
||||
import { UpdatePermissionDto } from './dto/update-permission.dto';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Respond } from '@/features/response';
|
||||
import { Respond } from '@/common/response';
|
||||
|
||||
@ApiTags('permission')
|
||||
@Controller('permissions')
|
||||
|
|
|
|||
|
|
@ -29,6 +29,6 @@ export const enum PermissionEnum {
|
|||
* @param permissions
|
||||
* @returns
|
||||
*/
|
||||
export function Permission(...permissions: PermissionEnum[]) {
|
||||
export function NeedPermission(...permissions: PermissionEnum[]) {
|
||||
return SetMetadata(PERMISSION_KEY, permissions);
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException, forwardRef } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PERMISSION_KEY } from './permission.decorator';
|
||||
import { UserService } from '@/modules/user';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector, private userService: UserService) {}
|
||||
constructor(private reflector: Reflector, @Inject(forwardRef(() => UserService)) private userService: UserService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const controller = context.getClass();
|
||||
|
|
@ -21,7 +21,6 @@ export class PermissionGuard implements CanActivate {
|
|||
const userPermissions = await this.userService.findUserPermissions(user.id);
|
||||
const hasPermission = permissions.every((permission) => userPermissions.includes(permission));
|
||||
if (!hasPermission) {
|
||||
console.log(userPermissions, permissions);
|
||||
throw new UnauthorizedException('权限不足');
|
||||
}
|
||||
return true;
|
||||
|
|
@ -1,12 +1,21 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { PermissionService } from './permission.service';
|
||||
import { PermissionController } from './permission.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Permission } from './entities/permission.entity';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { PermissionGuard } from './permission.guard';
|
||||
import { UserModule } from '../user';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Permission])],
|
||||
imports: [TypeOrmModule.forFeature([Permission]), forwardRef(() => UserModule)],
|
||||
controllers: [PermissionController],
|
||||
providers: [PermissionService],
|
||||
providers: [
|
||||
PermissionService,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: PermissionGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class PermissionModule {}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,13 @@ export class PermissionService {
|
|||
return `This action returns a #${id} permission`;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
async update(id: number, updatePermissionDto: UpdatePermissionDto) {
|
||||
await this.permissionRepository.update(id, updatePermissionDto);
|
||||
=======
|
||||
update(id: number, updatePermissionDto: UpdatePermissionDto) {
|
||||
return this.permissionRepository.update(id, updatePermissionDto);
|
||||
>>>>>>> 1a32173fc73bbb94906f9ffde5874d47f6dfdad8
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseEntity } from '@/features/typeorm';
|
||||
import { BaseEntity } from '@/database';
|
||||
import { User } from 'src/modules/user';
|
||||
import { Column, Entity, ManyToMany } from 'typeorm';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,33 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { CreatePostDto } from './dto/create-post.dto';
|
||||
import { UpdatePostDto } from './dto/update-post.dto';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Post } from './entities/post.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class PostService {
|
||||
create(createPostDto: CreatePostDto) {
|
||||
return 'This action adds a new post';
|
||||
constructor(@InjectRepository(Post) private postRepository: Repository<Post>) {}
|
||||
|
||||
async create(createPostDto: CreatePostDto) {
|
||||
const post = this.postRepository.create(createPostDto);
|
||||
await this.postRepository.save(post);
|
||||
return post.id;
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return `This action returns all post`;
|
||||
return this.postRepository.findAndCount();
|
||||
}
|
||||
|
||||
findOne(id: number) {
|
||||
return `This action returns a #${id} post`;
|
||||
return this.postRepository.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
update(id: number, updatePostDto: UpdatePostDto) {
|
||||
return `This action updates a #${id} post`;
|
||||
return this.postRepository.update(id, updatePostDto);
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
return `This action removes a #${id} post`;
|
||||
return this.postRepository.softDelete(id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseEntity } from '@/features/typeorm';
|
||||
import { BaseEntity } from '@/database';
|
||||
import { Permission } from '@/modules/permission/entities/permission.entity';
|
||||
import { User } from 'src/modules/user';
|
||||
import { Column, Entity, JoinTable, ManyToMany } from 'typeorm';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
|||
import { CreateRoleDto } from './dto/create-role.dto';
|
||||
import { UpdateRoleDto } from './dto/update-role.dto';
|
||||
import { RoleService } from './role.service';
|
||||
import { Respond } from '@/features/response';
|
||||
import { Respond } from '@/common/response';
|
||||
|
||||
@ApiTags('role')
|
||||
@Controller('roles')
|
||||
|
|
|
|||
|
|
@ -26,11 +26,16 @@ export class RoleService {
|
|||
return `This action returns a #${id} role`;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
async update(id: number, updateRoleDto: UpdateRoleDto) {
|
||||
if (updateRoleDto.permissions) {
|
||||
delete updateRoleDto.permissions;
|
||||
}
|
||||
await this.roleRepository.update(id, updateRoleDto);
|
||||
=======
|
||||
update(id: number, updateRoleDto: UpdateRoleDto) {
|
||||
return this.roleRepository.update(id, updateRoleDto);
|
||||
>>>>>>> 1a32173fc73bbb94906f9ffde5874d47f6dfdad8
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
|
|
|
|||
|
|
@ -1 +1,6 @@
|
|||
export class CreateUploadDto {}
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateUploadDto {
|
||||
@ApiProperty({ type: 'string', format: 'binary' })
|
||||
file: any;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseEntity } from '@/features/typeorm';
|
||||
import { BaseEntity } from '@/database';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Controller, Delete, Get, Param, Patch, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { UploadService } from './upload.service';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { Respond } from '@/features/response';
|
||||
import { Respond } from '@/common/response';
|
||||
import { CreateUploadDto } from './dto/create-upload.dto';
|
||||
|
||||
@ApiTags('upload')
|
||||
@Controller('upload')
|
||||
|
|
@ -12,6 +13,8 @@ export class UploadController {
|
|||
@Post()
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ApiOperation({ summary: '上传文件', operationId: 'upload' })
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiBody({ description: '文件', type: CreateUploadDto })
|
||||
create(@UploadedFile() file: Express.Multer.File) {
|
||||
return this.uploadService.create(file);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
|
|||
import { UploadController } from './upload.controller';
|
||||
import { UploadService } from './upload.service';
|
||||
import { MulterModule } from '@nestjs/platform-express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ConfigService } from '@/config';
|
||||
import dayjs from 'dayjs';
|
||||
import { join, parse } from 'path';
|
||||
import { diskStorage } from 'multer';
|
||||
|
|
@ -13,8 +13,8 @@ import { Upload } from './entities/upload.entity';
|
|||
imports: [
|
||||
TypeOrmModule.forFeature([Upload]),
|
||||
MulterModule.registerAsync({
|
||||
useFactory: async (configService) => {
|
||||
const dest = configService.get('UPLOAD_FOLDER', './public/upload');
|
||||
useFactory: async (config: ConfigService) => {
|
||||
const dest = config.uploadDir;
|
||||
const storage = diskStorage({
|
||||
destination: join(dest),
|
||||
filename: (req, file, cb) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import { PaginationDto } from '@/features/pagination';
|
||||
import { IntersectionType } from '@nestjs/swagger';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
import { PaginationDto } from '@/common/response';
|
||||
>>>>>>> 1a32173fc73bbb94906f9ffde5874d47f6dfdad8
|
||||
|
||||
export class FindUserDto extends IntersectionType(PaginationDto) {
|
||||
/**
|
||||
* 用户昵称
|
||||
* @example '绝弹'
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nickname?: string;
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
export * from './create-user.dto';
|
||||
export * from './update-user.dto';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './user.entity';
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { ApiHideProperty } from '@nestjs/swagger';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { BaseEntity } from '@/features/typeorm';
|
||||
import { BaseEntity } from '@/database';
|
||||
import { Post } from '@/modules/post';
|
||||
import { Role } from '@/modules/role';
|
||||
import { Column, Entity, JoinTable, ManyToMany } from 'typeorm';
|
||||
|
|
@ -61,6 +61,7 @@ export class User extends BaseEntity {
|
|||
/**
|
||||
* 用户角色
|
||||
*/
|
||||
@ApiHideProperty()
|
||||
@ManyToMany(() => Role, (role) => role.user)
|
||||
@JoinTable()
|
||||
roles: Role[];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export * from './dto';
|
||||
export * from './entities';
|
||||
export * from './dto/create-user.dto';
|
||||
export * from './dto/update-user.dto';
|
||||
export * from './entities/user.entity';
|
||||
export * from './user.controller';
|
||||
export * from './user.module';
|
||||
export * from './user.service';
|
||||
|
|
|
|||
|
|
@ -2,10 +2,20 @@ import { BaseController } from '@/features/base';
|
|||
import { Respond } from '@/features/response';
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, Version } from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
<<<<<<< HEAD
|
||||
import { CreateUserDto, UpdateUserDto } from './dto';
|
||||
=======
|
||||
import { Respond } from '@/common/response';
|
||||
import { BaseController } from '@/common/base';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
>>>>>>> 1a32173fc73bbb94906f9ffde5874d47f6dfdad8
|
||||
import { FindUserDto } from './dto/find-user.dto';
|
||||
import { User } from './entities';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UserService } from './user.service';
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
>>>>>>> 1a32173fc73bbb94906f9ffde5874d47f6dfdad8
|
||||
|
||||
@ApiTags('user')
|
||||
@Controller('users')
|
||||
|
|
@ -15,7 +25,7 @@ export class UserController extends BaseController {
|
|||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: '创建用户', operationId: 'addUser' })
|
||||
@ApiOperation({ description: '创建用户', operationId: 'addUser' })
|
||||
create(@Body() createUserDto: CreateUserDto) {
|
||||
return this.userService.create(createUserDto);
|
||||
}
|
||||
|
|
@ -23,26 +33,26 @@ export class UserController extends BaseController {
|
|||
@Get()
|
||||
@Respond(Respond.PAGINATION)
|
||||
@ApiOkResponse({ isArray: true, type: User })
|
||||
@ApiOperation({ summary: '批量查询', operationId: 'getUsers' })
|
||||
@ApiOperation({ description: '批量查询用户', operationId: 'getUsers' })
|
||||
async findMany(@Query() query: FindUserDto) {
|
||||
return this.userService.findMany(query);
|
||||
}
|
||||
|
||||
@Version('2')
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '查询用户', operationId: 'getUserv2' })
|
||||
@Version('2')
|
||||
@ApiOperation({ description: '查询用户', operationId: 'getUserv2' })
|
||||
findOne(@Param('id') id: number) {
|
||||
return this.userService.findOne(+id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: '更新用户', operationId: 'setUser' })
|
||||
@ApiOperation({ description: '更新用户', operationId: 'updateUser' })
|
||||
update(@Param('id') id: number, @Body() updateUserDto: UpdateUserDto) {
|
||||
return this.userService.update(+id, updateUserDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: '删除用户', operationId: 'delUser' })
|
||||
@ApiOperation({ description: '删除用户', operationId: 'deleteUser' })
|
||||
remove(@Param('id') id: number) {
|
||||
return this.userService.remove(+id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { User } from './entities';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UserController } from './user.controller';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Like, Repository } from 'typeorm';
|
||||
import { CreateUserDto, UpdateUserDto } from './dto';
|
||||
import { FindUserDto } from './dto/find-user.dto';
|
||||
import { User } from './entities';
|
||||
import { BaseService } from '@/features/base';
|
||||
import { User } from './entities/user.entity';
|
||||
import { BaseService } from '@/common/base';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UserService extends BaseService {
|
||||
|
|
@ -36,7 +37,6 @@ export class UserService extends BaseService {
|
|||
where: {
|
||||
nickname: nickname && Like(`%${nickname}%`),
|
||||
},
|
||||
relations: ['roles'],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,5 @@
|
|||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
/**
|
||||
* 运行端口
|
||||
*/
|
||||
SERVER_PORT: number;
|
||||
/**
|
||||
* 运行IP
|
||||
*/
|
||||
SERVER_HOST: string;
|
||||
/**
|
||||
* 项目根目录路径
|
||||
*/
|
||||
ROOT_PATH: string;
|
||||
/**
|
||||
* 环境变量
|
||||
*/
|
||||
NODE_ENV: 'development' | 'production' | 'test';
|
||||
/**
|
||||
* 上传文件夹
|
||||
*/
|
||||
UPLOAD_DIR: string;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es2017",
|
||||
"target": "ESNext",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
|
|
|
|||
Loading…
Reference in New Issue