feat: 解决冲突

master
绝弹 2023-08-02 20:47:54 +08:00
commit f11b7e85e1
89 changed files with 5012 additions and 485 deletions

8
.env
View File

@ -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
# ========================================================================================
# 分页配置

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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,

File diff suppressed because it is too large Load Diff

View File

@ -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 {}

View File

@ -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;
}

View File

@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class BaseModule {}

View File

@ -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,
};
}
}

View File

@ -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 {}

View File

@ -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 {

View File

@ -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';

View File

@ -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 {

View File

@ -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()

View File

@ -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: {

View File

@ -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 {}

View File

@ -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;

View File

@ -0,0 +1 @@
export * from './serialization.module';

View File

@ -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 {}

View File

@ -0,0 +1 @@
export * from './static.module';

View File

@ -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],
});

View File

@ -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}`,
});
};

View File

@ -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;
}

View File

@ -1,3 +1,4 @@
export * from './validation.error';
export * from './validation.filter';
export * from './validation.pipe';
export * from './validation.module';

View File

@ -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 });
}
}

View File

@ -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 {}

View File

@ -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;
}

View File

@ -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 {}

View File

@ -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');
}
}

View File

@ -1,2 +1,2 @@
export * from './config.module';
export * from './config.interface';
export * from './config.service';

View File

@ -1,17 +0,0 @@
/**
*
*/
export enum envKeys {
/**
*
*/
SERVER_HOST = 'SERVER_HOST',
/**
*
*/
SERVER_PORT = 'SERVER_PORT',
/**
*
*/
UPLOAD_DIR = 'UPLOAD_DIR',
}

View File

@ -1 +0,0 @@
export * from './env';

View File

@ -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],
});

View File

@ -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],
});

View File

@ -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

3
src/database/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './datasource';
export * from './entities/base';
export * from './database.module';

View File

@ -1,8 +0,0 @@
import { Global, Module } from '@nestjs/common';
import { LoggerService } from '../logger';
@Global()
@Module({
providers: [LoggerService],
})
export class BaseModule {}

View File

@ -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,
};
}
}

View File

@ -1,2 +0,0 @@
export * from './all.filter';
export * from './http.filter';

View File

@ -1,9 +0,0 @@
import { Global, Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
@Global()
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggerModule {}

View File

@ -1,2 +0,0 @@
export * from './pagination';
export * from './pagination.dto';

View File

@ -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 };
}
}

View File

@ -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],
});

View File

@ -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}`,
});
};

View File

@ -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);

View File

@ -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],
});

View File

@ -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();

View File

@ -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 {}

View File

@ -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;
}

View File

@ -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',
},

View File

@ -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';

View File

@ -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';

View File

@ -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')

View File

@ -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);
}

View File

@ -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;

View File

@ -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 {}

View File

@ -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) {

View File

@ -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';

View File

@ -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);
}
}

View File

@ -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';

View File

@ -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')

View File

@ -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) {

View File

@ -1 +1,6 @@
export class CreateUploadDto {}
import { ApiProperty } from '@nestjs/swagger';
export class CreateUploadDto {
@ApiProperty({ type: 'string', format: 'binary' })
file: any;
}

View File

@ -1,4 +1,4 @@
import { BaseEntity } from '@/features/typeorm';
import { BaseEntity } from '@/database';
import { Column, Entity } from 'typeorm';
@Entity()

View File

@ -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);
}

View 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) => {

View File

@ -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;

View File

@ -1,2 +0,0 @@
export * from './create-user.dto';
export * from './update-user.dto';

View File

@ -1 +0,0 @@
export * from './user.entity';

View File

@ -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[];

View File

@ -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';

View File

@ -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);
}

View File

@ -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';

View File

@ -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'],
});
}

19
src/types/env.d.ts vendored
View File

@ -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;
}
}

View File

@ -5,7 +5,7 @@
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es2017",
"target": "ESNext",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",