diff --git a/apps/admin/.eslintrc.js b/apps/admin/.eslintrc.js index 870eb08..98fc3ef 100644 --- a/apps/admin/.eslintrc.js +++ b/apps/admin/.eslintrc.js @@ -1,4 +1,7 @@ module.exports = { root: true, extends: ['@vben'], + rules: { + 'no-undef': 'off', + }, }; diff --git a/apps/admin/package.json b/apps/admin/package.json index 1b3d73a..f4a7bdd 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -17,35 +17,6 @@ "test:gzip": "npx http-server dist --cors --gzip -c-1", "type:check": "vue-tsc --noEmit --skipLibCheck" }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "prettier --write", - "eslint --fix" - ], - "{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [ - "prettier --write--parser json" - ], - "package.json": [ - "prettier --write" - ], - "*.vue": [ - "prettier --write", - "eslint --fix", - "stylelint --fix" - ], - "*.{scss,less,styl,html}": [ - "prettier --write", - "stylelint --fix" - ], - "*.md": [ - "prettier --write" - ] - }, - "config": { - "commitizen": { - "path": "node_modules/cz-git" - } - }, "dependencies": { "@ant-design/icons-vue": "^7.0.1", "@iconify/iconify": "^3.1.1", diff --git a/apps/admin/src/components/Upload/index.ts b/apps/admin/src/components/Upload/index.ts index 325a396..968ad26 100644 --- a/apps/admin/src/components/Upload/index.ts +++ b/apps/admin/src/components/Upload/index.ts @@ -1,6 +1,8 @@ import { withInstall } from '/@/utils'; import basicUpload from './src/BasicUpload.vue'; import uploadImage from './src/components/ImageUpload.vue'; +import avatarUpload from './src/AvatarUpload.vue'; export const ImageUpload = withInstall(uploadImage); export const BasicUpload = withInstall(basicUpload); +export const AvatarUpload = withInstall(avatarUpload); diff --git a/apps/admin/src/components/Upload/src/AvatarUpload.vue b/apps/admin/src/components/Upload/src/AvatarUpload.vue new file mode 100644 index 0000000..0d14670 --- /dev/null +++ b/apps/admin/src/components/Upload/src/AvatarUpload.vue @@ -0,0 +1,119 @@ + + diff --git a/apps/admin/src/utils/http/axios/Axios.ts b/apps/admin/src/utils/http/axios/Axios.ts index e75dfdd..b215be9 100644 --- a/apps/admin/src/utils/http/axios/Axios.ts +++ b/apps/admin/src/utils/http/axios/Axios.ts @@ -88,8 +88,9 @@ export class VAxios { // Request interceptor configuration processing this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => { - // If cancel repeat request is turned on, then cancel repeat request is prohibited - const requestOptions = (config as unknown as any).requestOptions ?? this.options.requestOptions; + // If cancel repeat request is turned on, then cancel repeat request is prohibited + const requestOptions = + (config as unknown as any).requestOptions ?? this.options.requestOptions; const ignoreCancelToken = requestOptions?.ignoreCancelToken ?? true; !ignoreCancelToken && axiosCanceler.addPending(config); @@ -202,7 +203,7 @@ export class VAxios { if (config.cancelToken) { conf.cancelToken = config.cancelToken; } - + if (config.signal) { conf.signal = config.signal; } diff --git a/apps/admin/src/views/system/dict/dict.data.ts b/apps/admin/src/views/system/dict/dict.data.ts index de8b0bf..4edba72 100644 --- a/apps/admin/src/views/system/dict/dict.data.ts +++ b/apps/admin/src/views/system/dict/dict.data.ts @@ -27,7 +27,7 @@ export const columns: BasicColumn[] = [ }, { title: '更新时间', - width: 150, + width: 160, sorter: true, dataIndex: 'updatedAt', format: (text: string) => { diff --git a/apps/admin/src/views/system/user/user.data.ts b/apps/admin/src/views/system/user/user.data.ts index c323c3b..c5a1017 100644 --- a/apps/admin/src/views/system/user/user.data.ts +++ b/apps/admin/src/views/system/user/user.data.ts @@ -67,10 +67,8 @@ export const columns: BasicColumn[] = [ { title: '创建时间', dataIndex: 'createdAt', - width: 165, - format: (text) => { - return formatToDateTime(text); - }, + width: 160, + format: (text) => formatToDateTime(text), }, ]; diff --git a/apps/admin/src/views/tools/storage/storage.data.ts b/apps/admin/src/views/tools/storage/storage.data.ts index 21ff13d..24769e8 100644 --- a/apps/admin/src/views/tools/storage/storage.data.ts +++ b/apps/admin/src/views/tools/storage/storage.data.ts @@ -48,10 +48,8 @@ export const columns: BasicColumn[] = [ { title: '创建时间', dataIndex: 'createdAt', - width: 150, - format: (text) => { - return formatToDateTime(text); - }, + width: 160, + format: (text) => formatToDateTime(text), }, ]; diff --git a/apps/api/.env b/apps/api/.env index 6720e65..d62b577 100644 --- a/apps/api/.env +++ b/apps/api/.env @@ -1,24 +1,10 @@ +# app APP_NAME = Nest Admin - -# server port -PORT = 5001 +APP_PORT = 5001 +APP_LOCALE = zh-CN WS_PORT = 5002 WS_PATH = ws-api -# global prefix, using in router、redis -GLOBAL_PREFIX = api - -# specify id as the root administrator -ADMIN_ROLE_ID = 1 -# user password salt -USER_PWD_SALT = kz!@#123 -# user default password -USER_DEFAULT_PWD = a123456 - -# minimum internal requirements protect id -PROTECT_SYS_PERMMENU_MAX_ID = 1 -PROTECT_SYS_DICTIONARY_MAX_ID = 1 - # logger LOGGER_LEVEL = verbose LOGGER_MAX_FILES = 31 diff --git a/apps/api/.env.development b/apps/api/.env.development index 5a58b3d..34df15c 100644 --- a/apps/api/.env.development +++ b/apps/api/.env.development @@ -1,8 +1,16 @@ -# token +# logger +LOGGER_LEVEL = debug + +# security JWT_SECRET = admin!@#123 -JWT_EXPIRES = 86400 +JWT_EXPIRE = 86400 REFRESH_TOKEN_SECRET = admin!@#123 -REFRESH_TOKEN_EXPIRES = 2592000 +REFRESH_TOKEN_EXPIRE = 2592000 + +# swagger +SWAGGER_ENABLE = true +SWAGGER_PATH = api-docs +SWAGGER_VERSION = 1.0 # db DB_HOST = 127.0.0.1 @@ -19,12 +27,8 @@ REDIS_HOST = 127.0.0.1 REDIS_PASSWORD = REDIS_DB = 0 -# swagger -SWAGGER_ENABLE = true -SWAGGER_PATH = api-docs - -# email -EMAIL_HOST = smtp.qq.com -EMAIL_PORT = 465 -EMAIL_USER = -EMAIL_PASS = +# smtp +SMTP_HOST = smtp.qq.com +SMTP_PORT = 465 +SMTP_USER = +SMTP_PASS = diff --git a/apps/api/.env.production b/apps/api/.env.production index 3e940dd..71fba48 100644 --- a/apps/api/.env.production +++ b/apps/api/.env.production @@ -1,16 +1,24 @@ -# token +# logger +LOGGER_LEVEL = info + +# security JWT_SECRET = admin!@#123 -JWT_EXPIRES = 86400 +JWT_EXPIRE = 86400 REFRESH_TOKEN_SECRET = admin!@#123 -REFRESH_TOKEN_EXPIRES = 2592000 +REFRESH_TOKEN_EXPIRE = 2592000 + +# swagger +SWAGGER_ENABLE = true +SWAGGER_PATH = api-docs +SWAGGER_VERSION = 1.0 # db DB_HOST = 127.0.0.1 DB_PORT = 3306 -DB_DATABASE = kz-admin -DB_USERNAME = kz-admin +DB_DATABASE = qb360 +DB_USERNAME = root DB_PASSWORD = Aa123456 -DB_SYNCHRONIZE = false +DB_SYNCHRONIZE = true DB_LOGGING = ["error"] # redis @@ -19,14 +27,8 @@ REDIS_HOST = 127.0.0.1 REDIS_PASSWORD = REDIS_DB = 0 -# swagger -SWAGGER_ENABLE = false - -# email -EMAIL_HOST = smtp.qq.com -EMAIL_PORT = 465 -EMAIL_USER = -EMAIL_PASS = - -# logger -LOGGER_LEVEL = warn +# smtp +SMTP_HOST = smtp.qq.com +SMTP_PORT = 465 +SMTP_USER = +SMTP_PASS = diff --git a/apps/api/package.json b/apps/api/package.json index 62aadaa..2721b1b 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -31,6 +31,7 @@ "migration:revert": "typeorm -- migration:revert" }, "dependencies": { + "@fastify/cookie": "^9.1.0", "@fastify/multipart": "^8.0.0", "@fastify/static": "^6.12.0", "@liaoliaots/nestjs-redis": "^9.0.5", @@ -68,7 +69,6 @@ "helmet": "^7.1.0", "ioredis": "^5.3.2", "lodash": "^4.17.21", - "log4js": "^6.9.1", "mysql": "^2.18.1", "nanoid": "^3.3.6", "nodemailer": "^6.9.7", @@ -83,7 +83,9 @@ "svg-captcha": "^1.4.0", "systeminformation": "^5.21.16", "typeorm": "^0.3.17", - "ua-parser-js": "^1.0.37" + "ua-parser-js": "^1.0.37", + "winston": "^3.11.0", + "winston-daily-rotate-file": "^4.7.1" }, "devDependencies": { "@compodoc/compodoc": "^1.1.22", @@ -135,5 +137,9 @@ ], "coverageDirectory": "../coverage", "testEnvironment": "node" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.1.0" } } diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 87d3062..4f8d1de 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -1,27 +1,34 @@ import { Module } from '@nestjs/common'; -import { APP_GUARD } from '@nestjs/core'; +import { ConfigModule } from '@nestjs/config'; +import { APP_FILTER, APP_GUARD } from '@nestjs/core'; -import { SharedModule } from '@/modules/shared/shared.module'; +import * as config from '@/config'; +import { SharedModule } from '@/shared/shared.module'; -import { AppConfigModule } from './config/config.module'; -import { AppDatabaseModule } from './database/database.module'; +import { AllExceptionsFilter } from './common/filters/any-exception.filter'; -import { AppsModule } from './modules/apps/apps.module'; +import { TodoModule } from './modules/todo/todo.module'; import { AuthModule } from './modules/auth/auth.module'; import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard'; +import { RbacGuard } from './modules/auth/guards/rbac.guard'; import { HealthModule } from './modules/health/health.module'; -import { RbacGuard } from './modules/rbac/guards/rbac.guard'; import { SocketModule } from './modules/socket/socket.module'; import { SystemModule } from './modules/system/system.module'; import { TasksModule } from './modules/tasks/tasks.module'; import { ToolsModule } from './modules/tools/tools.module'; +import { DatabaseModule } from './shared/database/database.module'; @Module({ imports: [ - AppConfigModule, - AppDatabaseModule, + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'], + load: [...Object.values(config)], + }), SharedModule, + DatabaseModule, + AuthModule, SystemModule, TasksModule, @@ -29,17 +36,17 @@ import { ToolsModule } from './modules/tools/tools.module'; SocketModule, HealthModule, - AppsModule, + // biz + + // end biz + + TodoModule, ], providers: [ - { - provide: APP_GUARD, - useClass: JwtAuthGuard, - }, - { - provide: APP_GUARD, - useClass: RbacGuard, - }, + { provide: APP_FILTER, useClass: AllExceptionsFilter }, + + { provide: APP_GUARD, useClass: JwtAuthGuard }, + { provide: APP_GUARD, useClass: RbacGuard }, ], }) export class AppModule {} diff --git a/apps/api/src/common/adapters/fastify.adapter.ts b/apps/api/src/common/adapters/fastify.adapter.ts new file mode 100644 index 0000000..8e72f92 --- /dev/null +++ b/apps/api/src/common/adapters/fastify.adapter.ts @@ -0,0 +1,47 @@ +import FastifyCookie from '@fastify/cookie'; +import FastifyMultipart from '@fastify/multipart'; +import { FastifyAdapter } from '@nestjs/platform-fastify'; + +const app: FastifyAdapter = new FastifyAdapter({ + trustProxy: true, + logger: false, +}); +export { app as fastifyApp }; + +app.register(FastifyMultipart as any, { + limits: { + fields: 10, // Max number of non-file fields + fileSize: 1024 * 1024 * 6, // limit size 6M + files: 5, // Max number of file fields + }, +}); + +app.register(FastifyCookie as any, { + secret: 'cookie-secret', // 这个 secret 不太重要,不存鉴权相关,无关紧要 +}); + +app.getInstance().addHook('onRequest', (request, reply, done) => { + // set undefined origin + const { origin } = request.headers; + if (!origin) { + request.headers.origin = request.headers.host; + } + + // forbidden php + + const { url } = request; + + if (url.endsWith('.php')) { + reply.raw.statusMessage = + 'Eh. PHP is not support on this machine. Yep, I also think PHP is bestest programming language. But for me it is beyond my reach.'; + + return reply.code(418).send(); + } + + // skip favicon request + if (url.match(/favicon.ico$/) || url.match(/manifest.json$/)) { + return reply.code(204).send(); + } + + done(); +}); diff --git a/apps/api/src/common/adapters/socket.adapter.ts b/apps/api/src/common/adapters/socket.adapter.ts new file mode 100644 index 0000000..ba6bf60 --- /dev/null +++ b/apps/api/src/common/adapters/socket.adapter.ts @@ -0,0 +1,3 @@ +import { IoAdapter } from '@nestjs/platform-socket.io'; + +export { IoAdapter }; diff --git a/apps/api/src/decorators/api-result.decorator.ts b/apps/api/src/common/decorators/api-result.decorator.ts similarity index 100% rename from apps/api/src/decorators/api-result.decorator.ts rename to apps/api/src/common/decorators/api-result.decorator.ts diff --git a/apps/api/src/decorators/api-token.decorator.ts b/apps/api/src/common/decorators/api-token.decorator.ts similarity index 100% rename from apps/api/src/decorators/api-token.decorator.ts rename to apps/api/src/common/decorators/api-token.decorator.ts diff --git a/apps/api/src/common/decorators/cookie.decorator.ts b/apps/api/src/common/decorators/cookie.decorator.ts new file mode 100644 index 0000000..11be959 --- /dev/null +++ b/apps/api/src/common/decorators/cookie.decorator.ts @@ -0,0 +1,10 @@ +import type { ExecutionContext } from '@nestjs/common'; +import { createParamDecorator } from '@nestjs/common'; +import type { FastifyRequest } from 'fastify'; + +export const Cookies = createParamDecorator( + (data: string, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return data ? request.cookies?.[data] : request.cookies; + }, +); diff --git a/apps/api/src/decorators/field.decorator.ts b/apps/api/src/common/decorators/field.decorator.ts similarity index 100% rename from apps/api/src/decorators/field.decorator.ts rename to apps/api/src/common/decorators/field.decorator.ts diff --git a/apps/api/src/decorators/http.decorator.ts b/apps/api/src/common/decorators/http.decorator.ts similarity index 100% rename from apps/api/src/decorators/http.decorator.ts rename to apps/api/src/common/decorators/http.decorator.ts diff --git a/apps/api/src/decorators/id-param.decorator.ts b/apps/api/src/common/decorators/id-param.decorator.ts similarity index 100% rename from apps/api/src/decorators/id-param.decorator.ts rename to apps/api/src/common/decorators/id-param.decorator.ts diff --git a/apps/api/src/decorators/skip-transform.decorator.ts b/apps/api/src/common/decorators/skip-transform.decorator.ts similarity index 100% rename from apps/api/src/decorators/skip-transform.decorator.ts rename to apps/api/src/common/decorators/skip-transform.decorator.ts diff --git a/apps/api/src/decorators/swagger.decorator.ts b/apps/api/src/common/decorators/swagger.decorator.ts similarity index 100% rename from apps/api/src/decorators/swagger.decorator.ts rename to apps/api/src/common/decorators/swagger.decorator.ts diff --git a/apps/api/src/decorators/transform.decorator.ts b/apps/api/src/common/decorators/transform.decorator.ts similarity index 100% rename from apps/api/src/decorators/transform.decorator.ts rename to apps/api/src/common/decorators/transform.decorator.ts diff --git a/apps/api/src/common/dto/abstract.dto.ts b/apps/api/src/common/dto/abstract.dto.ts index d35569f..8c7ff84 100644 --- a/apps/api/src/common/dto/abstract.dto.ts +++ b/apps/api/src/common/dto/abstract.dto.ts @@ -1,7 +1,7 @@ import { Type } from 'class-transformer'; import { IsDate, IsInt, IsOptional, Min } from 'class-validator'; -export abstract class AbstractDTO { +export abstract class AbstractDto { @Type(() => Number) @IsInt() @Min(1) diff --git a/apps/api/src/common/dto/delete.dto.ts b/apps/api/src/common/dto/delete.dto.ts new file mode 100644 index 0000000..6f87b9e --- /dev/null +++ b/apps/api/src/common/dto/delete.dto.ts @@ -0,0 +1,8 @@ +import { IsDefined, IsNotEmpty, IsNumber } from 'class-validator'; + +export class BatchDeleteDto { + @IsDefined() + @IsNotEmpty() + @IsNumber({}, { each: true }) + ids: number[]; +} diff --git a/apps/api/src/common/entity/abstract.entity.ts b/apps/api/src/common/entity/abstract.entity.ts index a9ce4e6..b1b745f 100644 --- a/apps/api/src/common/entity/abstract.entity.ts +++ b/apps/api/src/common/entity/abstract.entity.ts @@ -8,9 +8,9 @@ export abstract class AbstractEntity { @PrimaryGeneratedColumn() id: number; - @CreateDateColumn({ type: 'timestamp', name: 'created_at' }) + @CreateDateColumn({ name: 'created_at' }) createdAt: Date; - @UpdateDateColumn({ type: 'timestamp', name: 'updated_at' }) + @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; } diff --git a/apps/api/src/exceptions/api.exception.ts b/apps/api/src/common/exceptions/biz.exception.ts similarity index 61% rename from apps/api/src/exceptions/api.exception.ts rename to apps/api/src/common/exceptions/biz.exception.ts index 8c4e6aa..913375b 100644 --- a/apps/api/src/exceptions/api.exception.ts +++ b/apps/api/src/common/exceptions/biz.exception.ts @@ -1,23 +1,22 @@ import { HttpException, HttpStatus } from '@nestjs/common'; -import { ErrorEnum } from '../constants/error-code.constant'; +import { ErrorEnum } from '@/constants/error-code.constant'; +import { RESPONSE_SUCCESS_CODE } from '@/constants/response.constant'; -/** - * 业务错误时可抛出该异常 - */ -export class ApiException extends HttpException { +export class BusinessException extends HttpException { private errorCode: number; constructor(error: ErrorEnum | string) { + // 如果是非 ErrorEnum if (!error.includes(':')) { super( HttpException.createBody({ - code: 0, + code: RESPONSE_SUCCESS_CODE, message: error, }), HttpStatus.OK, ); - this.errorCode = 0; + this.errorCode = RESPONSE_SUCCESS_CODE; return; } @@ -37,3 +36,5 @@ export class ApiException extends HttpException { return this.errorCode; } } + +export { BusinessException as BizException }; diff --git a/apps/api/src/common/exceptions/not-found.exception.ts b/apps/api/src/common/exceptions/not-found.exception.ts new file mode 100644 index 0000000..95b034d --- /dev/null +++ b/apps/api/src/common/exceptions/not-found.exception.ts @@ -0,0 +1,10 @@ +import { NotFoundException } from '@nestjs/common'; +import { sample } from 'lodash'; + +export const NotFoundMessage = ['404, Not Found']; + +export class CannotFindException extends NotFoundException { + constructor() { + super(sample(NotFoundMessage)); + } +} diff --git a/apps/api/src/exceptions/socket.exception.ts b/apps/api/src/common/exceptions/socket.exception.ts similarity index 92% rename from apps/api/src/exceptions/socket.exception.ts rename to apps/api/src/common/exceptions/socket.exception.ts index 3b55fe1..8d6219f 100644 --- a/apps/api/src/exceptions/socket.exception.ts +++ b/apps/api/src/common/exceptions/socket.exception.ts @@ -1,7 +1,7 @@ import { HttpException } from '@nestjs/common'; import { WsException } from '@nestjs/websockets'; -import { ErrorEnum } from '../constants/error-code.constant'; +import { ErrorEnum } from '@/constants/error-code.constant'; export class SocketException extends WsException { private errorCode: number; diff --git a/apps/api/src/common/filters/any-exception.filter.ts b/apps/api/src/common/filters/any-exception.filter.ts new file mode 100644 index 0000000..5409a7e --- /dev/null +++ b/apps/api/src/common/filters/any-exception.filter.ts @@ -0,0 +1,91 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { FastifyReply, FastifyRequest } from 'fastify'; + +import { BusinessException } from '@/common/exceptions/biz.exception'; +import { ErrorEnum } from '@/constants/error-code.constant'; + +import { isDev } from '@/global/env'; + +type myError = { + readonly status: number; + readonly statusCode?: number; + + readonly message?: string; +}; + +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + private readonly logger = new Logger(AllExceptionsFilter.name); + + constructor() { + this.registerCatchAllExceptionsHook(); + } + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const request = ctx.getRequest(); + const response = ctx.getResponse(); + + const url = request.raw.url!; + + const status = + exception instanceof HttpException + ? exception.getStatus() + : (exception as myError)?.status || + (exception as myError)?.statusCode || + HttpStatus.INTERNAL_SERVER_ERROR; + + let message = + (exception as any)?.response?.message || + (exception as myError)?.message || + `${exception}`; + + // 系统内部错误时 + if ( + status === HttpStatus.INTERNAL_SERVER_ERROR && + !(exception instanceof BusinessException) + ) { + Logger.error(exception, undefined, 'Catch'); + + // 生产环境下隐藏错误信息 + if (!isDev) { + message = ErrorEnum.SERVER_ERROR?.split(':')[1]; + } + } else { + this.logger.warn( + `错误信息:(${status}) ${message} Path: ${decodeURI(url)}`, + ); + } + + const apiErrorCode: number = + exception instanceof BusinessException + ? exception.getErrorCode() + : status; + + // 返回基础响应结果 + const resBody: IBaseResponse = { + code: apiErrorCode, + message, + data: null, + }; + + response.status(status).send(resBody); + } + + registerCatchAllExceptionsHook() { + process.on('unhandledRejection', (reason) => { + console.error('unhandledRejection: ', reason); + }); + + process.on('uncaughtException', (err) => { + console.error('uncaughtException: ', err); + }); + } +} diff --git a/apps/api/src/interceptors/logging.interceptor.ts b/apps/api/src/common/interceptors/logging.interceptor.ts similarity index 72% rename from apps/api/src/interceptors/logging.interceptor.ts rename to apps/api/src/common/interceptors/logging.interceptor.ts index 4efa6f1..8f5d1f7 100644 --- a/apps/api/src/interceptors/logging.interceptor.ts +++ b/apps/api/src/common/interceptors/logging.interceptor.ts @@ -9,11 +9,7 @@ import { Observable, tap } from 'rxjs'; @Injectable() export class LoggingInterceptor implements NestInterceptor { - private logger: Logger; - - constructor() { - this.logger = new Logger(LoggingInterceptor.name, { timestamp: false }); - } + private logger = new Logger(LoggingInterceptor.name, { timestamp: false }); intercept( context: ExecutionContext, @@ -27,9 +23,7 @@ export class LoggingInterceptor implements NestInterceptor { return call$.pipe( tap(() => - this.logger.debug( - `--- 响应请求:${content}${` +${Date.now() - now}ms`}`, - ), + this.logger.debug(`--- 响应:${content}${` +${Date.now() - now}ms`}`), ), ); } diff --git a/apps/api/src/interceptors/timeout.interceptor.ts b/apps/api/src/common/interceptors/timeout.interceptor.ts similarity index 92% rename from apps/api/src/interceptors/timeout.interceptor.ts rename to apps/api/src/common/interceptors/timeout.interceptor.ts index 5d4fde1..5387869 100644 --- a/apps/api/src/interceptors/timeout.interceptor.ts +++ b/apps/api/src/common/interceptors/timeout.interceptor.ts @@ -10,7 +10,7 @@ import { catchError, timeout } from 'rxjs/operators'; @Injectable() export class TimeoutInterceptor implements NestInterceptor { - constructor(private readonly time: number = 5000) {} + constructor(private readonly time: number = 10000) {} intercept(context: ExecutionContext, next: CallHandler): Observable { return next.handle().pipe( diff --git a/apps/api/src/interceptors/transform.interceptor.ts b/apps/api/src/common/interceptors/transform.interceptor.ts similarity index 85% rename from apps/api/src/interceptors/transform.interceptor.ts rename to apps/api/src/common/interceptors/transform.interceptor.ts index 67414e6..2492c5b 100644 --- a/apps/api/src/interceptors/transform.interceptor.ts +++ b/apps/api/src/common/interceptors/transform.interceptor.ts @@ -30,10 +30,10 @@ export class TransformInterceptor implements NestInterceptor { return next.handle().pipe( map((data) => { - if (typeof data === 'undefined') { - context.switchToHttp().getResponse().status(HttpStatus.NO_CONTENT); - return data; - } + // if (typeof data === 'undefined') { + // context.switchToHttp().getResponse().status(HttpStatus.NO_CONTENT); + // return data; + // } return new ResOp(HttpStatus.OK, data ?? null); }), diff --git a/apps/api/src/common/model/response.model.ts b/apps/api/src/common/model/response.model.ts index 791c500..b2eb2b4 100644 --- a/apps/api/src/common/model/response.model.ts +++ b/apps/api/src/common/model/response.model.ts @@ -1,23 +1,28 @@ import { ApiProperty } from '@nestjs/swagger'; +import { + RESPONSE_SUCCESS_CODE, + RESPONSE_SUCCESS_MSG, +} from '@/constants/response.constant'; + export class ResOp { @ApiProperty({ type: 'object' }) data?: T; - @ApiProperty({ type: 'number', default: 200 }) + @ApiProperty({ type: 'number', default: RESPONSE_SUCCESS_CODE }) code: number; - @ApiProperty({ type: 'string', default: 'success' }) + @ApiProperty({ type: 'string', default: RESPONSE_SUCCESS_MSG }) message: string; - constructor(code: number, data: T, message = 'success') { + constructor(code: number, data: T, message = RESPONSE_SUCCESS_MSG) { this.code = code; this.data = data; this.message = message; } static success(data?: T, message?: string) { - return new ResOp(200, data, message); + return new ResOp(RESPONSE_SUCCESS_CODE, data, message); } static error(code: number, message) { diff --git a/apps/api/src/pipes/parse-int.pipe.ts b/apps/api/src/common/pipes/parse-int.pipe.ts similarity index 100% rename from apps/api/src/pipes/parse-int.pipe.ts rename to apps/api/src/common/pipes/parse-int.pipe.ts diff --git a/apps/api/src/config/app.config.ts b/apps/api/src/config/app.config.ts new file mode 100644 index 0000000..d669411 --- /dev/null +++ b/apps/api/src/config/app.config.ts @@ -0,0 +1,20 @@ +import { ConfigType, registerAs } from '@nestjs/config'; + +import { env, envNumber } from '@/global/env'; + +export const AppConfig = registerAs('app', () => ({ + name: env('APP_NAME'), + port: envNumber('APP_PORT', 3000), + globalPrefix: env('GLOBAL_PREFIX'), + adminRoleId: envNumber('ADMIN_ROLE_ID', 1), + userPwdSalt: env('USER_PWD_SALT', ''), + userDefaultPwd: env('USER_DEFAULT_PWD', 'a123456'), + locale: env('APP_LOCALE', 'zh-CN'), + + logger: { + level: env('LOGGER_LEVEL'), + maxFiles: envNumber('LOGGER_MAX_FILES'), + }, +})); + +export type IAppConfig = ConfigType; diff --git a/apps/api/src/config/config.module.ts b/apps/api/src/config/config.module.ts deleted file mode 100644 index 3b1673c..0000000 --- a/apps/api/src/config/config.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; - -import * as config from '@/config'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'], - load: [...Object.values(config)], - }), - ], -}) -export class AppConfigModule {} diff --git a/apps/api/src/config/modules/database.config.ts b/apps/api/src/config/database.config.ts similarity index 77% rename from apps/api/src/config/modules/database.config.ts rename to apps/api/src/config/database.config.ts index 76202eb..9239b03 100644 --- a/apps/api/src/config/modules/database.config.ts +++ b/apps/api/src/config/database.config.ts @@ -1,8 +1,8 @@ -import { registerAs } from '@nestjs/config'; +import { ConfigType, registerAs } from '@nestjs/config'; import { DataSource, DataSourceOptions } from 'typeorm'; -import { env, envBoolean, envNumber } from '@/config/env'; +import { env, envBoolean, envNumber } from '@/global/env'; // eslint-disable-next-line import/order import dotenv from 'dotenv'; @@ -22,12 +22,12 @@ const dataSourceOptions: DataSourceOptions = { subscribers: ['dist/modules/**/*.subscriber{.ts,.js}'], }; -export const database = registerAs( +export const DatabaseConfig = registerAs( 'database', (): DataSourceOptions => dataSourceOptions, ); -export type IDatabaseConfig = ReturnType; +export type IDatabaseConfig = ConfigType; const dataSource = new DataSource(dataSourceOptions); diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts deleted file mode 100644 index 246a49f..0000000 --- a/apps/api/src/config/env.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * 基础类型接口 - */ -export type BaseType = boolean | number | string | undefined | null; - -/** - * 格式化环境变量 - * @param key 环境变量的键值 - * @param defaultValue 默认值 - * @param callback 格式化函数 - */ -const fromatValue = ( - key: string, - defaultValue: T, - callback?: (value: string) => T, -): T => { - const value: string | undefined = process.env[key]; - if (typeof value === 'undefined') { - return defaultValue; - } - if (!callback) { - return value as unknown as T; - } - return callback(value); -}; - -export const env = (key: string, defaultValue: string = '') => - fromatValue(key, defaultValue); - -export const envString = (key: string, defaultValue: string = '') => - fromatValue(key, defaultValue); - -export const envNumber = (key: string, defaultValue: number = 0) => - fromatValue(key, defaultValue, (value) => { - try { - return Number(value); - } catch { - throw new Error(`${key} environment variable is not a number`); - } - }); - -export const envBoolean = (key: string, defaultValue: boolean = false) => - fromatValue(key, defaultValue, (value) => { - try { - return Boolean(JSON.parse(value)); - } catch { - throw new Error(`${key} environment variable is not a boolean`); - } - }); diff --git a/apps/api/src/config/index.ts b/apps/api/src/config/index.ts index 1f6e451..2090038 100644 --- a/apps/api/src/config/index.ts +++ b/apps/api/src/config/index.ts @@ -1,7 +1,7 @@ -export * from './modules/app.config'; -export * from './modules/redis.config'; -export * from './modules/database.config'; -export * from './modules/swagger.config'; -export * from './modules/jwt.config'; -export * from './modules/mailer.config'; -export * from './modules/sms.config'; +export * from './app.config'; +export * from './redis.config'; +export * from './database.config'; +export * from './swagger.config'; +export * from './security.config'; +export * from './mailer.config'; +export * from './sms.config'; diff --git a/apps/api/src/config/mailer.config.ts b/apps/api/src/config/mailer.config.ts new file mode 100644 index 0000000..9255a60 --- /dev/null +++ b/apps/api/src/config/mailer.config.ts @@ -0,0 +1,16 @@ +import { ConfigType, registerAs } from '@nestjs/config'; + +import { env, envNumber } from '@/global/env'; + +export const MailerConfig = registerAs('mailer', () => ({ + host: env('SMTP_HOST'), + port: envNumber('SMTP_PORT'), + ignoreTLS: true, + secure: true, + auth: { + user: env('SMTP_USER'), + pass: env('SMTP_PASS'), + }, +})); + +export type IMailerConfig = ConfigType; diff --git a/apps/api/src/config/modules/app.config.ts b/apps/api/src/config/modules/app.config.ts deleted file mode 100644 index a2d3001..0000000 --- a/apps/api/src/config/modules/app.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { registerAs } from '@nestjs/config'; - -import { env, envNumber } from '@/config/env'; - -export const app = registerAs('app', () => ({ - name: env('APP_NAME'), - port: envNumber('PORT', 3000), - globalPrefix: env('GLOBAL_PREFIX'), - adminRoleId: envNumber('ADMIN_ROLE_ID'), - userPwdSalt: env('USER_PWD_SALT'), - userDefaultPwd: env('USER_DEFAULT_PWD'), - protectSysPermMenuMaxId: envNumber('PROTECT_SYS_PERMMENU_MAX_ID'), - protectSysDictionaryMaxId: envNumber('PROTECT_SYS_DICTIONARY_MAX_ID'), - locale: env('APP_LOCALE', 'zh-CN'), - - logger: { - level: env('LOGGER_LEVEL'), - maxFiles: envNumber('LOGGER_MAX_FILES'), - }, -})); - -export type IAppConfig = ReturnType; diff --git a/apps/api/src/config/modules/jwt.config.ts b/apps/api/src/config/modules/jwt.config.ts deleted file mode 100644 index 5c548ca..0000000 --- a/apps/api/src/config/modules/jwt.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { registerAs } from '@nestjs/config'; - -import { env, envNumber } from '@/config/env'; - -export const jwt = registerAs('jwt', () => ({ - secret: env('JWT_SECRET'), - expires: envNumber('JWT_EXPIRES'), - refreshSecret: env('REFRESH_TOKEN_SECRET'), - refreshExpires: envNumber('REFRESH_TOKEN_EXPIRES'), -})); - -export type IJwtConfig = ReturnType; diff --git a/apps/api/src/config/modules/mailer.config.ts b/apps/api/src/config/modules/mailer.config.ts deleted file mode 100644 index db661e4..0000000 --- a/apps/api/src/config/modules/mailer.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { registerAs } from '@nestjs/config'; - -import { env, envNumber } from '@/config/env'; - -export const mailer = registerAs('mailer', () => ({ - host: env('EMAIL_HOST'), - port: envNumber('EMAIL_PORT'), - ignoreTLS: true, - secure: true, - auth: { - user: env('EMAIL_USER'), - pass: env('EMAIL_PASS'), - }, -})); - -export type IMailerConfig = ReturnType; diff --git a/apps/api/src/config/modules/redis.config.ts b/apps/api/src/config/modules/redis.config.ts deleted file mode 100644 index 81aa02f..0000000 --- a/apps/api/src/config/modules/redis.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { registerAs } from '@nestjs/config'; - -import { env, envNumber } from '@/config/env'; - -export const redis = registerAs('redis', () => ({ - host: env('REDIS_HOST', '127.0.0.1'), - port: envNumber('REDIS_PORT', 6379), - password: env('REDIS_PASSWORD'), - db: envNumber('REDIS_DB'), -})); - -export type IRedisConfig = ReturnType; diff --git a/apps/api/src/config/modules/swagger.config.ts b/apps/api/src/config/modules/swagger.config.ts deleted file mode 100644 index 80ac24c..0000000 --- a/apps/api/src/config/modules/swagger.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { registerAs } from '@nestjs/config'; - -import { env, envBoolean } from '@/config/env'; - -export const swagger = registerAs('swagger', () => ({ - enable: envBoolean('SWAGGER_ENABLE'), - path: env('SWAGGER_PATH'), -})); - -export type ISwaggerConfig = ReturnType; diff --git a/apps/api/src/config/redis.config.ts b/apps/api/src/config/redis.config.ts new file mode 100644 index 0000000..f5b2eb8 --- /dev/null +++ b/apps/api/src/config/redis.config.ts @@ -0,0 +1,12 @@ +import { ConfigType, registerAs } from '@nestjs/config'; + +import { env, envNumber } from '@/global/env'; + +export const RedisConfig = registerAs('redis', () => ({ + host: env('REDIS_HOST', '127.0.0.1'), + port: envNumber('REDIS_PORT', 6379), + password: env('REDIS_PASSWORD'), + db: envNumber('REDIS_DB'), +})); + +export type IRedisConfig = ConfigType; diff --git a/apps/api/src/config/security.config.ts b/apps/api/src/config/security.config.ts new file mode 100644 index 0000000..3a7df7d --- /dev/null +++ b/apps/api/src/config/security.config.ts @@ -0,0 +1,12 @@ +import { ConfigType, registerAs } from '@nestjs/config'; + +import { env, envNumber } from '@/global/env'; + +export const SecurityConfig = registerAs('security', () => ({ + jwtSecret: env('JWT_SECRET'), + jwtExprire: envNumber('JWT_EXPIRE'), + refreshSecret: env('REFRESH_TOKEN_SECRET'), + refreshExpire: envNumber('REFRESH_TOKEN_EXPIRE'), +})); + +export type ISecurityConfig = ConfigType; diff --git a/apps/api/src/config/modules/sms.config.ts b/apps/api/src/config/sms.config.ts similarity index 54% rename from apps/api/src/config/modules/sms.config.ts rename to apps/api/src/config/sms.config.ts index b3140a8..2037a19 100644 --- a/apps/api/src/config/modules/sms.config.ts +++ b/apps/api/src/config/sms.config.ts @@ -1,8 +1,8 @@ -import { registerAs } from '@nestjs/config'; +import { ConfigType, registerAs } from '@nestjs/config'; -import { env } from '@/config/env'; +import { env } from '@/global/env'; -export const sms = registerAs('sms', () => ({ +export const SmsConfig = registerAs('sms', () => ({ sign: env('SMS_SING', 'Youni'), region: env('SMS_REGION', 'ap-guangzhou'), appid: env('SMS_APPID', '1400437232'), @@ -10,4 +10,4 @@ export const sms = registerAs('sms', () => ({ secretKey: env('SMS_SECRET_KEY', 'your-secret-key'), })); -export type ISmsConfig = ReturnType; +export type ISmsConfig = ConfigType; diff --git a/apps/api/src/config/swagger.config.ts b/apps/api/src/config/swagger.config.ts new file mode 100644 index 0000000..7cdd705 --- /dev/null +++ b/apps/api/src/config/swagger.config.ts @@ -0,0 +1,10 @@ +import { ConfigType, registerAs } from '@nestjs/config'; + +import { env, envBoolean } from '@/global/env'; + +export const SwaggerConfig = registerAs('swagger', () => ({ + enable: envBoolean('SWAGGER_ENABLE'), + path: env('SWAGGER_PATH'), +})); + +export type ISwaggerConfig = ConfigType; diff --git a/apps/api/src/decorators/index.ts b/apps/api/src/decorators/index.ts deleted file mode 100644 index dda21d2..0000000 --- a/apps/api/src/decorators/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './api-result.decorator'; -export * from './api-token.decorator'; -export * from './http.decorator'; -export * from './skip-transform.decorator'; diff --git a/apps/api/src/filters/app.filter.ts b/apps/api/src/filters/app.filter.ts deleted file mode 100644 index 4fe663a..0000000 --- a/apps/api/src/filters/app.filter.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - ArgumentsHost, - Catch, - ExceptionFilter, - HttpException, - HttpStatus, - Logger, -} from '@nestjs/common'; -import { FastifyReply } from 'fastify'; - -import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiException } from '@/exceptions/api.exception'; -import { isDev } from '@/global/env'; - -@Catch() -export class AppFilter implements ExceptionFilter { - private readonly logger = new Logger(AppFilter.name); - - constructor() { - this.registerCatchAllExceptionsHook(); - } - - catch(exception: unknown, host: ArgumentsHost) { - this.logger.error(exception); - - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - - // 响应结果码判断 - const httpStatus: number = - exception instanceof HttpException - ? exception.getStatus() - : HttpStatus.INTERNAL_SERVER_ERROR; - - const apiErrorCode: number = - exception instanceof ApiException ? exception.getErrorCode() : httpStatus; - - let errorMessage: string = - exception instanceof HttpException ? exception.message : `${exception}`; - - // 系统内部错误时,在生产模式下隐藏具体异常消息 - if (!isDev && httpStatus === HttpStatus.INTERNAL_SERVER_ERROR) { - errorMessage = ErrorEnum.SERVER_ERROR?.split(':')[1]; - } - - // 返回基础响应结果 - const resBody: IBaseResponse = { - code: apiErrorCode, - message: errorMessage, - data: null, - }; - - response.status(httpStatus).send(resBody); - } - - registerCatchAllExceptionsHook() { - process.on('unhandledRejection', (reason: any) => { - console.error('unhandledRejection: ', reason); - }); - - process.on('uncaughtException', (err) => { - console.error('uncaughtException: ', err); - }); - } -} diff --git a/apps/api/src/global/env.ts b/apps/api/src/global/env.ts index d5914f1..66d6f3d 100644 --- a/apps/api/src/global/env.ts +++ b/apps/api/src/global/env.ts @@ -9,3 +9,53 @@ export const isDev = process.env.NODE_ENV === 'development'; export const isTest = !!process.env.TEST; export const cwd = process.cwd(); + +/** + * 基础类型接口 + */ +export type BaseType = boolean | number | string | undefined | null; + +/** + * 格式化环境变量 + * @param key 环境变量的键值 + * @param defaultValue 默认值 + * @param callback 格式化函数 + */ +const fromatValue = ( + key: string, + defaultValue: T, + callback?: (value: string) => T, +): T => { + const value: string | undefined = process.env[key]; + if (typeof value === 'undefined') { + return defaultValue; + } + if (!callback) { + return value as unknown as T; + } + return callback(value); +}; + +export const env = (key: string, defaultValue: string = '') => + fromatValue(key, defaultValue); + +export const envString = (key: string, defaultValue: string = '') => + fromatValue(key, defaultValue); + +export const envNumber = (key: string, defaultValue: number = 0) => + fromatValue(key, defaultValue, (value) => { + try { + return Number(value); + } catch { + throw new Error(`${key} environment variable is not a number`); + } + }); + +export const envBoolean = (key: string, defaultValue: boolean = false) => + fromatValue(key, defaultValue, (value) => { + try { + return Boolean(JSON.parse(value)); + } catch { + throw new Error(`${key} environment variable is not a boolean`); + } + }); diff --git a/apps/api/src/magrations/1689457720509-UpdateTodo.ts b/apps/api/src/magrations/1689457720509-UpdateTodo.ts deleted file mode 100644 index c4c82a9..0000000 --- a/apps/api/src/magrations/1689457720509-UpdateTodo.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class UpdateTodo1689457720509 implements MigrationInterface { - name = 'UpdateTodo1689457720509'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE \`todo\` DROP COLUMN \`status\``); - await queryRunner.query( - `ALTER TABLE \`todo\` ADD \`status\` tinyint NOT NULL DEFAULT 0`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE \`todo\` DROP COLUMN \`status\``); - await queryRunner.query( - `ALTER TABLE \`todo\` ADD \`status\` varchar(255) NOT NULL DEFAULT '0'`, - ); - } -} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 5b8fb90..edd21a6 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,6 +1,5 @@ import cluster from 'cluster'; import path from 'path'; -import { performance } from 'perf_hooks'; import { ClassSerializerInterceptor, @@ -11,78 +10,55 @@ import { } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { NestFactory, Reflector } from '@nestjs/core'; -import { - NestFastifyApplication, - FastifyAdapter, -} from '@nestjs/platform-fastify'; - -import { IoAdapter } from '@nestjs/platform-socket.io'; +import { NestFastifyApplication } from '@nestjs/platform-fastify'; import { useContainer } from 'class-validator'; import { AppModule } from './app.module'; +import { fastifyApp } from './common/adapters/fastify.adapter'; +import { IoAdapter } from './common/adapters/socket.adapter'; +import { LoggingInterceptor } from './common/interceptors/logging.interceptor'; +import { TimeoutInterceptor } from './common/interceptors/timeout.interceptor'; +import { TransformInterceptor } from './common/interceptors/transform.interceptor'; import { IAppConfig } from './config'; -import { AppFilter } from './filters/app.filter'; import { isDev, isMainProcess } from './global/env'; -import { LoggingInterceptor } from './interceptors/logging.interceptor'; -import { TimeoutInterceptor } from './interceptors/timeout.interceptor'; -import { TransformInterceptor } from './interceptors/transform.interceptor'; -import { AppLoggerService } from './modules/shared/services/app-logger.service'; -import { setupSwagger } from './utils/setup-swagger'; - -// catchError(); +import { setupSwagger } from './setup-swagger'; +import { MyLogger } from './shared/logger/logger.service'; declare const module: any; async function bootstrap() { const app = await NestFactory.create( AppModule, - new FastifyAdapter(), + fastifyApp, { bufferLogs: true, snapshot: true, }, ); - // app config service const configService = app.get(ConfigService); - // reflector const reflector = app.get(Reflector); - // class-validator 的 DTO 类中注入nestjs容器的依赖 + // class-validator 的 DTO 类中注入 nest 容器的依赖 useContainer(app.select(AppModule), { fallbackOnErrors: true }); - app.useLogger(app.get(AppLoggerService)); app.enableCors({ origin: '*', credentials: true }); + app.setGlobalPrefix('api'); app.useStaticAssets({ root: path.join(__dirname, '..', 'public') }); - // https://github.com/fastify/fastify-multipart/ - // eslint-disable-next-line global-require - await app.register(require('@fastify/multipart'), { - attachFieldsToBody: true, - limits: { - fileSize: 1024 * 1024 * 10, // 10M - files: 1, - }, - }); - - // 处理异常请求 - app.useGlobalFilters(new AppFilter()); - app.useGlobalInterceptors( - // 请求超时 - new TimeoutInterceptor(30000), - // 序列化 new ClassSerializerInterceptor(reflector), - // Logging - isDev ? new LoggingInterceptor() : null, - // 返回数据转换 - new TransformInterceptor(new Reflector()), + new TransformInterceptor(reflector), + new TimeoutInterceptor(), ); - // 使用全局管道验证数据 + if (isDev) { + app.useGlobalInterceptors(new LoggingInterceptor()); + } + app.useGlobalPipes( new ValidationPipe({ transform: true, @@ -90,12 +66,12 @@ async function bootstrap() { transformOptions: { enableImplicitConversion: true }, // forbidNonWhitelisted: true, // 禁止 无装饰器验证的数据通过 errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + stopAtFirstError: true, exceptionFactory: (errors) => new UnprocessableEntityException( errors.map((e) => { - const rule = Object.keys(e.constraints)[0]; - const msg = e.constraints[rule]; - // return `property ${e.property} validation failed: ${msg}, following constraints: ${rule}`; + const rule = Object.keys(e.constraints!)[0]; + const msg = e.constraints![rule]; return msg; })[0], ), @@ -105,13 +81,12 @@ async function bootstrap() { // websocket app.useWebSocketAdapter(new IoAdapter()); - // global prefix - const { globalPrefix, port } = configService.get('app'); - app.setGlobalPrefix(globalPrefix); + const { port } = configService.get('app')!; setupSwagger(app, configService); await app.listen(port, '0.0.0.0', async () => { + app.useLogger(app.get(MyLogger)); const url = await app.getUrl(); const { pid } = process; const env = cluster.isPrimary; @@ -125,9 +100,8 @@ async function bootstrap() { logger.log(`[${prefix + pid}] Server running on ${url}`); if (isDev) { - logger.log(`[${prefix + pid}] OpenApi: ${url}/api-docs`); + logger.log(`[${prefix + pid}] OpenAPI: ${url}/api-docs`); } - logger.log(`Server is up. ${`+${performance.now() | 0}ms`}`); }); if (module.hot) { diff --git a/apps/api/src/modules/apps/apps.module.ts b/apps/api/src/modules/apps/apps.module.ts deleted file mode 100644 index 4772c43..0000000 --- a/apps/api/src/modules/apps/apps.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Module, forwardRef } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { TodoEntity } from '@/modules/apps/todo/todo.entity'; - -import { SystemModule } from '../system/system.module'; - -import { TodoController } from './todo/todo.controller'; -import { TodoService } from './todo/todo.service'; - -@Module({ - imports: [ - forwardRef(() => SystemModule), - TypeOrmModule.forFeature([TodoEntity]), - ], - controllers: [TodoController], - providers: [TodoService], - exports: [TypeOrmModule], -}) -export class AppsModule {} diff --git a/apps/api/src/modules/apps/todo/todo.permission.ts b/apps/api/src/modules/apps/todo/todo.permission.ts deleted file mode 100644 index fff85f6..0000000 --- a/apps/api/src/modules/apps/todo/todo.permission.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const PermissionTodo = { - LIST: 'todo:list', - CREATE: 'todo:create', - READ: 'todo:read', - UPDATE: 'todo:update', - DELETE: 'todo:delete', -} as const; diff --git a/apps/api/src/modules/auth/auth.controller.ts b/apps/api/src/modules/auth/auth.controller.ts index 96df6b5..ca59068 100644 --- a/apps/api/src/modules/auth/auth.controller.ts +++ b/apps/api/src/modules/auth/auth.controller.ts @@ -1,13 +1,13 @@ import { Body, Controller, Headers, Post, UseGuards } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { Ip } from '@/decorators'; -import { ApiResult } from '@/decorators/api-result.decorator'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; +import { Ip } from '@/common/decorators/http.decorator'; -import { UserService } from '../system/user/user.service'; +import { UserService } from '../user/user.service'; import { AuthService } from './auth.service'; -import { Public } from './decorators'; +import { Public } from './decorators/public.decorator'; import { LoginDto, RegisterDto } from './dto/auth.dto'; import { LocalGuard } from './guards/local.guard'; import { LoginToken } from './models/auth.model'; diff --git a/apps/api/src/modules/auth/auth.module.ts b/apps/api/src/modules/auth/auth.module.ts index 7cb81d2..eb6d921 100644 --- a/apps/api/src/modules/auth/auth.module.ts +++ b/apps/api/src/modules/auth/auth.module.ts @@ -5,13 +5,13 @@ import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { IJwtConfig } from '@/config'; +import { ISecurityConfig } from '@/config'; import { isDev } from '@/global/env'; import { LogModule } from '../system/log/log.module'; import { MenuModule } from '../system/menu/menu.module'; import { RoleModule } from '../system/role/role.module'; -import { UserModule } from '../system/user/user.module'; +import { UserModule } from '../user/user.module'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; @@ -41,11 +41,12 @@ const strategies = [LocalStrategy, JwtStrategy]; JwtModule.registerAsync({ imports: [ConfigModule], useFactory: (configService: ConfigService) => { - const { secret, expires } = configService.get('jwt'); + const { jwtSecret, jwtExprire } = + configService.get('security'); return { - secret, - expires, + secret: jwtSecret, + expires: jwtExprire, ignoreExpiration: isDev, }; }, diff --git a/apps/api/src/modules/auth/auth.service.ts b/apps/api/src/modules/auth/auth.service.ts index c846f39..44c7499 100644 --- a/apps/api/src/modules/auth/auth.service.ts +++ b/apps/api/src/modules/auth/auth.service.ts @@ -4,10 +4,10 @@ import { Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import { isEmpty } from 'lodash'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiException } from '@/exceptions/api.exception'; -import { UserService } from '@/modules/system/user/user.service'; +import { UserService } from '@/modules/user/user.service'; import { MD5 } from '@/utils'; @@ -32,12 +32,12 @@ export class AuthService { const user = await this.userService.findUserByUserName(credential); if (isEmpty(user)) { - throw new ApiException(ErrorEnum.USER_NOT_FOUND); + throw new BusinessException(ErrorEnum.USER_NOT_FOUND); } const comparePassword = MD5(`${password}${user.psalt}`); if (user.password !== comparePassword) { - throw new ApiException(ErrorEnum.PASSWORD_MISMATCH); + throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD); } if (user) { @@ -60,12 +60,12 @@ export class AuthService { ): Promise { const user = await this.userService.findUserByUserName(username); if (isEmpty(user)) { - throw new ApiException(ErrorEnum.USER_NOT_FOUND); + throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD); } const comparePassword = MD5(`${password}${user.psalt}`); if (user.password !== comparePassword) { - throw new ApiException(ErrorEnum.PASSWORD_MISMATCH); + throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD); } const roleIds = await this.roleService.getRoleIdsByUser(user.id); @@ -82,7 +82,7 @@ export class AuthService { // 设置菜单权限 const permissions = await this.menuService.getPermissions(user.id); - await this.setPermissions(user.id, permissions); + await this.setPermissionsCache(user.id, permissions); await this.loginLogService.create(user.id, ip, ua); @@ -97,7 +97,7 @@ export class AuthService { const comparePassword = MD5(`${password}${user.psalt}`); if (user.password !== comparePassword) { - throw new ApiException(ErrorEnum.PASSWORD_MISMATCH); + throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD); } } @@ -140,7 +140,12 @@ export class AuthService { return this.menuService.getPermissions(uid); } - async setPermissions(uid: number, permissions: string[]): Promise { + async getPermissionsCache(uid: number): Promise { + const permissionString = await this.redis.get(`auth:permission:${uid}`); + return permissionString ? JSON.parse(permissionString) : []; + } + + async setPermissionsCache(uid: number, permissions: string[]): Promise { await this.redis.set(`auth:permission:${uid}`, JSON.stringify(permissions)); } @@ -151,9 +156,4 @@ export class AuthService { async getTokenByUid(uid: number): Promise { return this.redis.get(`auth:token:${uid}`); } - - async getPermissionsByUid(uid: number): Promise { - const permissionString = await this.redis.get(`auth:permission:${uid}`); - return permissionString ? JSON.parse(permissionString) : []; - } } diff --git a/apps/api/src/modules/rbac/constant.ts b/apps/api/src/modules/auth/constant.ts similarity index 52% rename from apps/api/src/modules/rbac/constant.ts rename to apps/api/src/modules/auth/constant.ts index d98b5a6..fc69e65 100644 --- a/apps/api/src/modules/rbac/constant.ts +++ b/apps/api/src/modules/auth/constant.ts @@ -1,9 +1,24 @@ +export const IS_PUBLIC_KEY = 'is_public'; + export const PERMISSION_KEY = 'permission'; export const POLICY_KEY = 'policy'; export const ALLOW_ANON_KEY = 'allow_anon_permission'; +export const AuthStrategy = { + LOCAL: 'local', + LOCAL_EMAIL: 'local_email', + LOCAL_PHONE: 'local_phone', + + JWT: 'jwt', + + GITHUB: 'github', + GOOGLE: 'google', + + PDD: 'pdd', +} as const; + export const Roles = { ADMIN: 'admin', USER: 'user', diff --git a/apps/api/src/modules/auth/constants.ts b/apps/api/src/modules/auth/constants.ts deleted file mode 100644 index b72a6be..0000000 --- a/apps/api/src/modules/auth/constants.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const IS_PUBLIC_KEY = 'is_public'; - -export const AuthStrategy = { - LOCAL: 'local', - LOCAL_EMAIL: 'local_email', - LOCAL_PHONE: 'local_phone', - - JWT: 'jwt', - - GITHUB: 'github', -} as const; diff --git a/apps/api/src/modules/auth/controllers/account.controller.ts b/apps/api/src/modules/auth/controllers/account.controller.ts index 67ac5fc..3631b34 100644 --- a/apps/api/src/modules/auth/controllers/account.controller.ts +++ b/apps/api/src/modules/auth/controllers/account.controller.ts @@ -1,18 +1,18 @@ import { Body, Controller, Get, Post, Put, UseGuards } from '@nestjs/common'; import { ApiExtraModels, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { ApiResult } from '@/decorators/api-result.decorator'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; -import { AuthUser } from '@/modules/auth/decorators'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; +import { AllowAnon } from '@/modules/auth/decorators/allow-anon.decorator'; +import { AuthUser } from '@/modules/auth/decorators/auth-user.decorator'; -import { AllowAnon } from '@/modules/rbac/decorators'; import { MenuEntity } from '@/modules/system/menu/menu.entity'; -import { PasswordUpdateDto } from '@/modules/system/user/dto/password.dto'; +import { PasswordUpdateDto } from '@/modules/user/dto/password.dto'; -import { AccountInfo } from '../../system/user/user.model'; -import { UserService } from '../../system/user/user.service'; +import { AccountInfo } from '../../user/user.model'; +import { UserService } from '../../user/user.service'; import { AuthService } from '../auth.service'; import { AccountUpdateDto } from '../dto/account.dto'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; diff --git a/apps/api/src/modules/auth/controllers/captcha.controller.ts b/apps/api/src/modules/auth/controllers/captcha.controller.ts index 1e46adc..149324f 100644 --- a/apps/api/src/modules/auth/controllers/captcha.controller.ts +++ b/apps/api/src/modules/auth/controllers/captcha.controller.ts @@ -8,11 +8,11 @@ import Redis from 'ioredis'; import { isEmpty } from 'lodash'; import * as svgCaptcha from 'svg-captcha'; -import { ApiResult } from '@/decorators'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; import { generateUUID } from '@/utils'; -import { Public } from '../decorators'; +import { Public } from '../decorators/public.decorator'; import { ImageCaptchaDto } from '../dto/captcha.dto'; import { ImageCaptcha } from '../models/auth.model'; @@ -27,7 +27,7 @@ export class CaptchaController { @ApiOperation({ summary: '获取登录图片验证码' }) @ApiResult({ type: ImageCaptcha }) @Public() - @Throttle({ default: { limit: 2, ttl: 60000 } }) + @Throttle({ default: { limit: 2, ttl: 600000 } }) async captchaByImg(@Query() dto: ImageCaptchaDto): Promise { const { width, height } = dto; diff --git a/apps/api/src/modules/auth/controllers/email.controller.ts b/apps/api/src/modules/auth/controllers/email.controller.ts index 9bcda28..9eb4618 100644 --- a/apps/api/src/modules/auth/controllers/email.controller.ts +++ b/apps/api/src/modules/auth/controllers/email.controller.ts @@ -3,10 +3,11 @@ import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { Throttle, ThrottlerGuard } from '@nestjs/throttler'; -import { Ip } from '@/decorators/http.decorator'; -import { MailerService } from '@/modules/shared/mailer/mailer.service'; +import { Ip } from '@/common/decorators/http.decorator'; +import { MailerService } from '@/shared/mailer/mailer.service'; -import { AuthUser, Public } from '../decorators'; +import { AuthUser } from '../decorators/auth-user.decorator'; +import { Public } from '../decorators/public.decorator'; import { SendEmailCodeDto } from '../dto/captcha.dto'; @@ -19,7 +20,7 @@ export class EmailController { @Post('send') @ApiOperation({ summary: '发送邮箱验证码' }) @Public() - @Throttle({ default: { limit: 2, ttl: 60000 } }) + @Throttle({ default: { limit: 2, ttl: 600000 } }) async sendEmailCode( @Body() dto: SendEmailCodeDto, @Ip() ip: string, diff --git a/apps/api/src/modules/rbac/decorators/allow-anon.decorator.ts b/apps/api/src/modules/auth/decorators/allow-anon.decorator.ts similarity index 100% rename from apps/api/src/modules/rbac/decorators/allow-anon.decorator.ts rename to apps/api/src/modules/auth/decorators/allow-anon.decorator.ts diff --git a/apps/api/src/modules/auth/decorators/index.ts b/apps/api/src/modules/auth/decorators/index.ts deleted file mode 100644 index 3f71074..0000000 --- a/apps/api/src/modules/auth/decorators/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './auth-user.decorator'; -export * from './public.decorator'; diff --git a/apps/api/src/modules/rbac/decorators/permission.decorator.ts b/apps/api/src/modules/auth/decorators/permission.decorator.ts similarity index 100% rename from apps/api/src/modules/rbac/decorators/permission.decorator.ts rename to apps/api/src/modules/auth/decorators/permission.decorator.ts diff --git a/apps/api/src/modules/auth/decorators/public.decorator.ts b/apps/api/src/modules/auth/decorators/public.decorator.ts index e5cf56e..c402d38 100644 --- a/apps/api/src/modules/auth/decorators/public.decorator.ts +++ b/apps/api/src/modules/auth/decorators/public.decorator.ts @@ -1,6 +1,6 @@ import { SetMetadata } from '@nestjs/common'; -import { IS_PUBLIC_KEY } from '../constants'; +import { IS_PUBLIC_KEY } from '../constant'; /** * 当接口不需要检测用户登录时添加该装饰器 diff --git a/apps/api/src/modules/rbac/decorators/resource.decorator.ts b/apps/api/src/modules/auth/decorators/resource.decorator.ts similarity index 100% rename from apps/api/src/modules/rbac/decorators/resource.decorator.ts rename to apps/api/src/modules/auth/decorators/resource.decorator.ts diff --git a/apps/api/src/modules/auth/entities/access-token.entity.ts b/apps/api/src/modules/auth/entities/access-token.entity.ts index eafa9af..4dd0a6c 100644 --- a/apps/api/src/modules/auth/entities/access-token.entity.ts +++ b/apps/api/src/modules/auth/entities/access-token.entity.ts @@ -8,7 +8,7 @@ import { PrimaryGeneratedColumn, } from 'typeorm'; -import { UserEntity } from '@/modules/system/user/entities/user.entity'; +import { UserEntity } from '@/modules/user/entities/user.entity'; import { RefreshTokenEntity } from './refresh-token.entity'; diff --git a/apps/api/src/modules/auth/entities/index.ts b/apps/api/src/modules/auth/entities/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/apps/api/src/modules/auth/guards/jwt-auth.guard.ts b/apps/api/src/modules/auth/guards/jwt-auth.guard.ts index a645403..f222bc1 100644 --- a/apps/api/src/modules/auth/guards/jwt-auth.guard.ts +++ b/apps/api/src/modules/auth/guards/jwt-auth.guard.ts @@ -1,15 +1,19 @@ -import { ExecutionContext, Injectable } from '@nestjs/common'; +import { + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { FastifyReply, FastifyRequest } from 'fastify'; import { isEmpty, isNil } from 'lodash'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiException } from '@/exceptions/api.exception'; import { AuthService } from '@/modules/auth/auth.service'; import { TokenService } from '@/modules/auth/services/token.service'; -import { AuthStrategy, IS_PUBLIC_KEY } from '../constants'; +import { AuthStrategy, IS_PUBLIC_KEY } from '../constant'; @Injectable() export class JwtAuthGuard extends AuthGuard(AuthStrategy.JWT) { @@ -22,7 +26,6 @@ export class JwtAuthGuard extends AuthGuard(AuthStrategy.JWT) { } async canActivate(context: ExecutionContext): Promise { - // 检测是否是开放类型的,例如获取验证码类型的接口不需要校验,可以加入@Public可自动放过 const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), @@ -31,62 +34,36 @@ export class JwtAuthGuard extends AuthGuard(AuthStrategy.JWT) { const request = context.switchToHttp().getRequest(); const response = context.switchToHttp().getResponse(); - let requestToken = request.headers.authorization; + const Authorization = request.headers.authorization; let result: any = false; try { result = await super.canActivate(context); } catch (e) { + // 需要后置判断 这样携带了 token 的用户就能够解析到 request.user if (isPublic) return true; - if (isEmpty(requestToken)) { - throw new ApiException(ErrorEnum.INVALID_LOGIN); + if (isEmpty(Authorization)) { + throw new UnauthorizedException('未登录'); } - // 判断token是否存在,如果不存在则认证失败 - const accessToken = isNil(requestToken) + // 判断 token 是否存在, 如果不存在则认证失败 + const accessToken = isNil(Authorization) ? undefined - : await this.tokenService.checkAccessToken(requestToken!); - if (!accessToken) throw new ApiException(ErrorEnum.INVALID_LOGIN); + : await this.tokenService.checkAccessToken(Authorization!); - // 无法通过token校验 - // 尝试通过refreshToken刷新token - - if (!isNil(requestToken)) { - const token = await this.tokenService.refreshToken(accessToken); - if (isNil(token)) throw new ApiException(ErrorEnum.INVALID_LOGIN); - - if (token.accessToken) { - // 将新的token挂载到当前请求上 - requestToken = token.accessToken; - - response.header('new-token', token.accessToken); - } - - try { - // 刷新失败(refreshToken过期)则再次抛出认证失败的异常 - result = await super.canActivate(context); - } catch (error) { - throw new ApiException(ErrorEnum.INVALID_LOGIN); - } - } - } - - if (isPublic) return true; - - if (isEmpty(request.user)) { - throw new ApiException(ErrorEnum.INVALID_LOGIN); + if (!accessToken) throw new UnauthorizedException('令牌无效'); } const pv = await this.authService.getPasswordVersionByUid(request.user.uid); if (pv !== `${request.user.pv}`) { // 密码版本不一致,登录期间已更改过密码 - throw new ApiException(ErrorEnum.INVALID_LOGIN); + throw new BusinessException(ErrorEnum.INVALID_LOGIN); } // 不允许多端登录 // const cacheToken = await this.authService.getTokenByUid(request.user.uid); - // if (requestToken !== cacheToken) { + // if (Authorization !== cacheToken) { // // 与redis保存不一致 即二次登录 // throw new ApiException(ErrorEnum.CODE_1106); // } @@ -97,7 +74,7 @@ export class JwtAuthGuard extends AuthGuard(AuthStrategy.JWT) { handleRequest(err, user, info) { // You can throw an exception based on either "info" or "err" arguments if (err || !user) { - throw err; + throw err || new UnauthorizedException(); } return user; } diff --git a/apps/api/src/modules/auth/guards/local.guard.ts b/apps/api/src/modules/auth/guards/local.guard.ts index 898e8a4..028a4f1 100644 --- a/apps/api/src/modules/auth/guards/local.guard.ts +++ b/apps/api/src/modules/auth/guards/local.guard.ts @@ -1,7 +1,7 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { AuthStrategy } from '../constants'; +import { AuthStrategy } from '../constant'; @Injectable() export class LocalGuard extends AuthGuard(AuthStrategy.LOCAL) { diff --git a/apps/api/src/modules/rbac/guards/rbac.guard.ts b/apps/api/src/modules/auth/guards/rbac.guard.ts similarity index 66% rename from apps/api/src/modules/rbac/guards/rbac.guard.ts rename to apps/api/src/modules/auth/guards/rbac.guard.ts index 95104f8..d034c36 100644 --- a/apps/api/src/modules/rbac/guards/rbac.guard.ts +++ b/apps/api/src/modules/auth/guards/rbac.guard.ts @@ -1,22 +1,22 @@ -import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { FastifyRequest } from 'fastify'; -import { DataSource } from 'typeorm'; - +import { BusinessException } from '@/common/exceptions/biz.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiException } from '@/exceptions/api.exception'; import { AuthService } from '@/modules/auth/auth.service'; -import { IS_PUBLIC_KEY } from '../../auth/constants'; - -import { ALLOW_ANON_KEY, PERMISSION_KEY } from '../constant'; +import { ALLOW_ANON_KEY, PERMISSION_KEY, IS_PUBLIC_KEY } from '../constant'; @Injectable() export class RbacGuard implements CanActivate { constructor( private reflector: Reflector, - private dataSource: DataSource, private authService: AuthService, ) {} @@ -31,34 +31,34 @@ export class RbacGuard implements CanActivate { const request = context.switchToHttp().getRequest(); const { user } = request; + if (!user) throw new UnauthorizedException('登录无效'); - if (!user) return false; - + // allowAnon 是需要登录后可访问(无需权限), Public 则是无需登录也可访问. const allowAnon = this.reflector.get( ALLOW_ANON_KEY, context.getHandler(), ); - // Token校验身份通过,判断是否需要权限的url,不需要权限则pass if (allowAnon) return true; const payloadPermission = this.reflector.getAllAndOverride< string | string[] >(PERMISSION_KEY, [context.getHandler(), context.getClass()]); - let allPermissions = await this.authService.getPermissionsByUid(user.uid); + // 控制器没有设置接口权限,则默认通过 + if (!payloadPermission) return true; + let allPermissions = await this.authService.getPermissionsCache(user.uid); + + // 缓存失效, 则获取新的 Permission if (!allPermissions) { const res = await this.authService.getPermissions(user.uid); allPermissions = res; // set permissions into cache - await this.authService.setPermissions(user.uid, allPermissions); + await this.authService.setPermissionsCache(user.uid, allPermissions); } - // 如果没有设置接口权限,则默认通过 - // if (isEmpty(payloadPermission)) return true; - let canNext = false; // handle permission strings @@ -72,7 +72,7 @@ export class RbacGuard implements CanActivate { } if (!canNext) { - throw new ApiException(ErrorEnum.NO_PERMISSION); + throw new BusinessException(ErrorEnum.NO_PERMISSION); } return true; diff --git a/apps/api/src/modules/rbac/guards/resource.guard.ts b/apps/api/src/modules/auth/guards/resource.guard.ts similarity index 83% rename from apps/api/src/modules/rbac/guards/resource.guard.ts rename to apps/api/src/modules/auth/guards/resource.guard.ts index fc2125a..6ef51cd 100644 --- a/apps/api/src/modules/rbac/guards/resource.guard.ts +++ b/apps/api/src/modules/auth/guards/resource.guard.ts @@ -6,12 +6,11 @@ import { isNil } from 'lodash'; import { DataSource, Repository } from 'typeorm'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiException } from '@/exceptions/api.exception'; -import { IS_PUBLIC_KEY } from '../../auth/constants'; +import { POLICY_KEY, Roles, IS_PUBLIC_KEY } from '../constant'; -import { POLICY_KEY, Roles } from '../constant'; import { ResourceObject } from '../decorators/resource.decorator'; @Injectable() @@ -59,16 +58,16 @@ export class ResourceGuard implements CanActivate { const id = getRequestItemId(request); if (!id) { - throw new ApiException(ErrorEnum.REQUESTED_RESOURCE_NOT_FOUND); + throw new BusinessException(ErrorEnum.REQUESTED_RESOURCE_NOT_FOUND); } const item = await repo.findOne({ where: { id }, relations: ['user'] }); if (!item) { - throw new ApiException(ErrorEnum.REQUESTED_RESOURCE_NOT_FOUND); + throw new BusinessException(ErrorEnum.REQUESTED_RESOURCE_NOT_FOUND); } if (!item?.user) { - throw new ApiException(ErrorEnum.USER_NOT_FOUND); + throw new BusinessException(ErrorEnum.USER_NOT_FOUND); } if (condition) { @@ -77,7 +76,7 @@ export class ResourceGuard implements CanActivate { // 如果没有设置policy,则默认只能操作自己的数据 if (item.user?.id !== user.uid) { - throw new ApiException(ErrorEnum.REQUESTED_RESOURCE_NOT_FOUND); + throw new BusinessException(ErrorEnum.REQUESTED_RESOURCE_NOT_FOUND); } } diff --git a/apps/api/src/modules/auth/services/captcha.service.ts b/apps/api/src/modules/auth/services/captcha.service.ts index 2017b78..1732c72 100644 --- a/apps/api/src/modules/auth/services/captcha.service.ts +++ b/apps/api/src/modules/auth/services/captcha.service.ts @@ -4,8 +4,8 @@ import { Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import { isEmpty } from 'lodash'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiException } from '@/exceptions/api.exception'; import { CaptchaLogService } from '@/modules/system/log/services/captcha-log.service'; @Injectable() @@ -22,7 +22,7 @@ export class CaptchaService { async checkImgCaptcha(id: string, code: string): Promise { const result = await this.redis.get(`captcha:img:${id}`); if (isEmpty(result) || code.toLowerCase() !== result.toLowerCase()) { - throw new ApiException(ErrorEnum.INVALID_VERIFICATION_CODE); + throw new BusinessException(ErrorEnum.INVALID_VERIFICATION_CODE); } // 校验成功后移除验证码 await this.redis.del(`captcha:img:${id}`); diff --git a/apps/api/src/modules/auth/services/token.service.ts b/apps/api/src/modules/auth/services/token.service.ts index d68f8e4..d8dc8df 100644 --- a/apps/api/src/modules/auth/services/token.service.ts +++ b/apps/api/src/modules/auth/services/token.service.ts @@ -1,12 +1,12 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { Inject, Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import dayjs from 'dayjs'; +import { ISecurityConfig, SecurityConfig } from '@/config'; import { RoleService } from '@/modules/system/role/role.service'; -import { UserEntity } from '@/modules/system/user/entities/user.entity'; +import { UserEntity } from '@/modules/user/entities/user.entity'; -import { generateUUID } from '@/utils'; +import { generateUUID } from '@/utils/uuid'; import { AccessTokenEntity } from '../entities/access-token.entity'; import { RefreshTokenEntity } from '../entities/refresh-token.entity'; @@ -18,8 +18,8 @@ import { RefreshTokenEntity } from '../entities/refresh-token.entity'; export class TokenService { constructor( private jwtService: JwtService, - private configService: ConfigService, private roleService: RoleService, + @Inject(SecurityConfig.KEY) private securityConfig: ISecurityConfig, ) {} /** @@ -47,6 +47,12 @@ export class TokenService { return null; } + generateJwtSign(payload: any) { + const jwtSign = this.jwtService.sign(payload); + + return jwtSign; + } + async generateAccessToken(uid: number, roles: string[] = []) { const payload: IAuthUser = { uid, @@ -61,7 +67,7 @@ export class TokenService { accessToken.value = jwtSign; accessToken.user = { id: uid } as UserEntity; accessToken.expired_at = dayjs() - .add(this.configService.get('jwt.expires'), 'second') + .add(this.securityConfig.jwtExprire, 'second') .toDate(); await accessToken.save(); @@ -89,13 +95,13 @@ export class TokenService { }; const refreshTokenSign = this.jwtService.sign(refreshTokenPayload, { - secret: this.configService.get('jwt.refreshSecret'), + secret: this.securityConfig.refreshSecret, }); const refreshToken = new RefreshTokenEntity(); refreshToken.value = refreshTokenSign; refreshToken.expired_at = now - .add(this.configService.get('jwt.refreshExpires'), 'second') + .add(this.securityConfig.refreshExpire, 'second') .toDate(); refreshToken.accessToken = accessToken; diff --git a/apps/api/src/modules/auth/strategies/jwt.strategy.ts b/apps/api/src/modules/auth/strategies/jwt.strategy.ts index e1cbde1..2d9c230 100644 --- a/apps/api/src/modules/auth/strategies/jwt.strategy.ts +++ b/apps/api/src/modules/auth/strategies/jwt.strategy.ts @@ -1,19 +1,20 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { Inject, Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; -import { IJwtConfig } from '@/config'; +import { ISecurityConfig, SecurityConfig } from '@/config'; -import { AuthStrategy } from '../constants'; +import { AuthStrategy } from '../constant'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, AuthStrategy.JWT) { - constructor(private configService: ConfigService) { + constructor( + @Inject(SecurityConfig.KEY) private securityConfig: ISecurityConfig, + ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - secretOrKey: configService.get('jwt').secret, + secretOrKey: securityConfig.jwtSecret, }); } diff --git a/apps/api/src/modules/auth/strategies/local.strategy.ts b/apps/api/src/modules/auth/strategies/local.strategy.ts index 4d2566c..acfdfe1 100644 --- a/apps/api/src/modules/auth/strategies/local.strategy.ts +++ b/apps/api/src/modules/auth/strategies/local.strategy.ts @@ -3,7 +3,7 @@ import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-local'; import { AuthService } from '../auth.service'; -import { AuthStrategy } from '../constants'; +import { AuthStrategy } from '../constant'; @Injectable() export class LocalStrategy extends PassportStrategy( diff --git a/apps/api/src/modules/health/health.controller.ts b/apps/api/src/modules/health/health.controller.ts index a29a19b..2678295 100644 --- a/apps/api/src/modules/health/health.controller.ts +++ b/apps/api/src/modules/health/health.controller.ts @@ -8,7 +8,7 @@ import { TypeOrmHealthIndicator, } from '@nestjs/terminus'; -import { Permission } from '../rbac/decorators'; +import { Permission } from '../auth/decorators/permission.decorator'; export const PermissionHealth = { NETWORK: 'app:health:network', diff --git a/apps/api/src/modules/rbac/decorators/index.ts b/apps/api/src/modules/rbac/decorators/index.ts deleted file mode 100644 index f4fae24..0000000 --- a/apps/api/src/modules/rbac/decorators/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './allow-anon.decorator'; -export * from './permission.decorator'; diff --git a/apps/api/src/modules/rbac/rbac.module.ts b/apps/api/src/modules/rbac/rbac.module.ts deleted file mode 100644 index 683da98..0000000 --- a/apps/api/src/modules/rbac/rbac.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module, forwardRef } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { SystemModule } from '../system/system.module'; - -const services = []; - -@Module({ - imports: [forwardRef(() => SystemModule)], - controllers: [], - providers: [...services], - exports: [TypeOrmModule, ...services], -}) -export class RbacModule {} diff --git a/apps/api/src/modules/shared/services/app-logger.service.ts b/apps/api/src/modules/shared/services/app-logger.service.ts deleted file mode 100644 index b8bcf23..0000000 --- a/apps/api/src/modules/shared/services/app-logger.service.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { - ConsoleLogger, - ConsoleLoggerOptions, - Injectable, - LogLevel, -} from '@nestjs/common'; - -import { ConfigService } from '@nestjs/config'; -import { Appender, configure, getLogger, levels } from 'log4js'; - -/** - * 将Nestjs内置日志等级进行等级排序, 并将内置log等级调整为info - */ -const LogLevelOrder: LogLevel[] = ['verbose', 'debug', 'log', 'warn', 'error']; - -@Injectable() -export class AppLoggerService extends ConsoleLogger { - constructor( - context: string, - options: ConsoleLoggerOptions, - private readonly configService: ConfigService, - ) { - // 设置日志等级 - const level = configService.get('app.logger.level'); - const levelIndex = LogLevelOrder.findIndex((e) => e === level); - if (levelIndex === -1) { - throw new Error( - `Invalid logger level, configurable level ${LogLevelOrder.join(',')}`, - ); - } - - super(context, { - ...options, - timestamp: true, - logLevels: LogLevelOrder.slice(levelIndex), - }); - - // 初始化log4js - this.initLog4js(); - } - - verbose(message: any, context?: string): void { - super.verbose.apply(this, [message, context]); - - if (this.isLevelEnabled('verbose')) { - getLogger('verbose').log('verbose', message); - } - } - - debug(message: any, context?: string): void { - super.debug.apply(this, [message, context]); - - if (this.isLevelEnabled('debug')) { - getLogger('debug').log('debug', message); - } - } - - log(message: any, context?: string): void { - super.log.apply(this, [message, context]); - - if (this.isLevelEnabled('log')) { - getLogger('info').log('info', message); - } - } - - warn(message: any, context?: string): void { - super.warn.apply(this, [message, context]); - - if (this.isLevelEnabled('warn')) { - getLogger('warn').log('warn', message); - } - } - - error(message: any, stack?: string, context?: string): void { - super.error.apply(this, [message, stack, context]); - - if (this.isLevelEnabled('error')) { - getLogger('error').log('error', message); - } - } - - private initLog4js() { - // 增加日志等级 - levels.addLevels({ - VERBOSE: { value: 5000, colour: 'blue' }, - DEBUG: { value: 10000, colour: 'cyan' }, - INFO: { value: 20000, colour: 'green' }, - WARN: { value: 30000, colour: 'yellow' }, - ERROR: { value: 40000, colour: 'red' }, - }); - - configure({ - appenders: { - verbose: this.createAppenders('verbose'), - debug: this.createAppenders('debug'), - info: this.createAppenders('info'), - warn: this.createAppenders('warn'), - error: this.createAppenders('error'), - console: { - type: 'console', - }, - }, - categories: { - default: { - appenders: ['console'], - level: 'all', - }, - verbose: { - appenders: ['verbose'], - level: 'all', - }, - debug: { - appenders: ['debug'], - level: 'all', - }, - info: { - appenders: ['info'], - level: 'all', - }, - warn: { - appenders: ['warn'], - level: 'all', - }, - error: { - appenders: ['error'], - level: 'all', - enableCallStack: true, - }, - }, - }); - } - - private createAppenders(level: LogLevel | 'info'): Appender { - const enableCallStack = level === 'error'; - - return { - type: 'dateFile', - filename: `logs/${level}`, - pattern: 'yyyy-MM-dd.log', - alwaysIncludePattern: true, - keepFileExt: true, - numBackups: this.configService.get('app.logger.maxFiles'), - layout: { - type: 'pattern', - pattern: - '[%h] %z %d{yyyy-MM-dd hh:mm:ss.SSS} %p %n%m' + - `${enableCallStack ? ' %n%s' : ''}` + - ' %n%x{divider}', - tokens: { - divider: '-'.repeat(150), - }, - }, - }; - } -} diff --git a/apps/api/src/modules/socket/admin-ws.guard.ts b/apps/api/src/modules/socket/admin-ws.guard.ts index db8d76b..90d383d 100644 --- a/apps/api/src/modules/socket/admin-ws.guard.ts +++ b/apps/api/src/modules/socket/admin-ws.guard.ts @@ -2,8 +2,8 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Observable } from 'rxjs'; import { Socket } from 'socket.io'; +import { SocketException } from '@/common/exceptions/socket.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { SocketException } from '@/exceptions/socket.exception'; import { AuthService } from './auth.service'; diff --git a/apps/api/src/modules/socket/admin-ws.service.ts b/apps/api/src/modules/socket/admin-ws.service.ts index ebe0947..cfbc77e 100644 --- a/apps/api/src/modules/socket/admin-ws.service.ts +++ b/apps/api/src/modules/socket/admin-ws.service.ts @@ -7,7 +7,7 @@ import { In, Repository } from 'typeorm'; import { AdminWSGateway } from '@/modules/socket/admin-ws.gateway'; import { RoleEntity } from '../system/role/role.entity'; -import { UserEntity } from '../system/user/entities/user.entity'; +import { UserEntity } from '../user/entities/user.entity'; import { EVENT_UPDATE_MENU } from './socket.event'; diff --git a/apps/api/src/modules/socket/auth.service.ts b/apps/api/src/modules/socket/auth.service.ts index ed464ef..75290e9 100644 --- a/apps/api/src/modules/socket/auth.service.ts +++ b/apps/api/src/modules/socket/auth.service.ts @@ -2,10 +2,9 @@ import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { isEmpty } from 'lodash'; +import { SocketException } from '@/common/exceptions/socket.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { SocketException } from '@/exceptions/socket.exception'; - @Injectable() export class AuthService { constructor(private jwtService: JwtService) {} diff --git a/apps/api/src/modules/system/dept/dept.controller.ts b/apps/api/src/modules/system/dept/dept.controller.ts index 7ca2fe8..c148866 100644 --- a/apps/api/src/modules/system/dept/dept.controller.ts +++ b/apps/api/src/modules/system/dept/dept.controller.ts @@ -9,18 +9,25 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; +import { IdParam } from '@/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiResult } from '@/decorators/api-result.decorator'; -import { IdParam } from '@/decorators/id-param.decorator'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; -import { ApiException } from '@/exceptions/api.exception'; -import { AuthUser } from '@/modules/auth/decorators'; -import { Permission } from '@/modules/rbac/decorators'; +import { AuthUser } from '@/modules/auth/decorators/auth-user.decorator'; +import { Permission } from '@/modules/auth/decorators/permission.decorator'; import { DeptEntity } from '@/modules/system/dept/dept.entity'; import { DeptDto, DeptQueryDto } from './dept.dto'; import { DeptService } from './dept.service'; -import { PermissionDept } from './permission'; + +export const Permissions = { + LIST: 'system:dept:list', + CREATE: 'system:dept:create', + READ: 'system:dept:read', + UPDATE: 'system:dept:update', + DELETE: 'system:dept:delete', +} as const; @ApiSecurityAuth() @ApiTags('System - 部门模块') @@ -31,7 +38,7 @@ export class DeptController { @Get() @ApiOperation({ summary: '获取部门列表' }) @ApiResult({ type: [DeptEntity] }) - @Permission(PermissionDept.LIST) + @Permission(Permissions.LIST) async list( @Query() dto: DeptQueryDto, @AuthUser('uid') uid: number, @@ -41,21 +48,21 @@ export class DeptController { @Post() @ApiOperation({ summary: '创建部门' }) - @Permission(PermissionDept.CREATE) + @Permission(Permissions.CREATE) async create(@Body() dto: DeptDto): Promise { await this.deptService.create(dto); } @Get(':id') @ApiOperation({ summary: '查询部门信息' }) - @Permission(PermissionDept.READ) + @Permission(Permissions.READ) async info(@IdParam() id: number) { return this.deptService.info(id); } @Put(':id') @ApiOperation({ summary: '更新部门' }) - @Permission(PermissionDept.UPDATE) + @Permission(Permissions.UPDATE) async update( @IdParam() id: number, @Body() updateDeptDto: DeptDto, @@ -65,17 +72,17 @@ export class DeptController { @Delete(':id') @ApiOperation({ summary: '删除部门' }) - @Permission(PermissionDept.DELETE) + @Permission(Permissions.DELETE) async delete(@IdParam() id: number): Promise { // 查询是否有关联用户或者部门,如果含有则无法删除 const count = await this.deptService.countUserByDeptId(id); if (count > 0) - throw new ApiException(ErrorEnum.DEPARTMENT_HAS_ASSOCIATED_USERS); + throw new BusinessException(ErrorEnum.DEPARTMENT_HAS_ASSOCIATED_USERS); const count2 = await this.deptService.countChildDept(id); if (count2 > 0) - throw new ApiException(ErrorEnum.DEPARTMENT_HAS_CHILD_DEPARTMENTS); + throw new BusinessException(ErrorEnum.DEPARTMENT_HAS_CHILD_DEPARTMENTS); await this.deptService.delete(id); } diff --git a/apps/api/src/modules/system/dept/dept.entity.ts b/apps/api/src/modules/system/dept/dept.entity.ts index 11406c2..a47913d 100644 --- a/apps/api/src/modules/system/dept/dept.entity.ts +++ b/apps/api/src/modules/system/dept/dept.entity.ts @@ -10,7 +10,7 @@ import { import { AbstractEntity } from '@/common/entity/abstract.entity'; -import { UserEntity } from '../user/entities/user.entity'; +import { UserEntity } from '../../user/entities/user.entity'; @Entity({ name: 'sys_dept' }) @Tree('materialized-path') diff --git a/apps/api/src/modules/system/dept/dept.module.ts b/apps/api/src/modules/system/dept/dept.module.ts index 9d03567..aeade9c 100644 --- a/apps/api/src/modules/system/dept/dept.module.ts +++ b/apps/api/src/modules/system/dept/dept.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserModule } from '../../user/user.module'; import { RoleModule } from '../role/role.module'; -import { UserModule } from '../user/user.module'; import { DeptController } from './dept.controller'; import { DeptEntity } from './dept.entity'; diff --git a/apps/api/src/modules/system/dept/dept.service.ts b/apps/api/src/modules/system/dept/dept.service.ts index 5c47354..ff5d7a5 100644 --- a/apps/api/src/modules/system/dept/dept.service.ts +++ b/apps/api/src/modules/system/dept/dept.service.ts @@ -3,10 +3,10 @@ import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; import { isEmpty } from 'lodash'; import { EntityManager, Repository, TreeRepository } from 'typeorm'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiException } from '@/exceptions/api.exception'; import { DeptEntity } from '@/modules/system/dept/dept.entity'; -import { UserEntity } from '@/modules/system/user/entities/user.entity'; +import { UserEntity } from '@/modules/user/entities/user.entity'; import { deleteEmptyChildren } from '@/utils/list2tree'; @@ -37,7 +37,7 @@ export class DeptService { .getOne(); if (isEmpty(dept)) { - throw new ApiException(ErrorEnum.DEPARTMENT_NOT_FOUND); + throw new BusinessException(ErrorEnum.DEPARTMENT_NOT_FOUND); } return dept; } diff --git a/apps/api/src/modules/system/dept/permission.ts b/apps/api/src/modules/system/dept/permission.ts deleted file mode 100644 index 59539a4..0000000 --- a/apps/api/src/modules/system/dept/permission.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const PermissionDept = { - LIST: 'system:dept:list', - CREATE: 'system:dept:create', - READ: 'system:dept:read', - UPDATE: 'system:dept:update', - DELETE: 'system:dept:delete', -} as const; diff --git a/apps/api/src/modules/system/dict/dict.controller.ts b/apps/api/src/modules/system/dict/dict.controller.ts index e01c52c..d4b6d5f 100644 --- a/apps/api/src/modules/system/dict/dict.controller.ts +++ b/apps/api/src/modules/system/dict/dict.controller.ts @@ -1,16 +1,23 @@ import { Body, Controller, Delete, Get, Post, Query } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { ApiResult } from '@/decorators/api-result.decorator'; -import { IdParam } from '@/decorators/id-param.decorator'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; +import { IdParam } from '@/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; import { Pagination } from '@/helper/paginate/pagination'; -import { Permission } from '@/modules/rbac/decorators'; +import { Permission } from '@/modules/auth/decorators/permission.decorator'; import { DictEntity } from '@/modules/system/dict/dict.entity'; import { DictDto, DictQueryDto } from './dict.dto'; import { DictService } from './dict.service'; -import { PermissionDict } from './permission'; + +export const Permissions = { + LIST: 'system:dict:list', + CREATE: 'system:dict:create', + READ: 'system:dict:read', + UPDATE: 'system:dict:update', + DELETE: 'system:dict:delete', +} as const; @ApiTags('System - 字典配置模块') @ApiSecurityAuth() @@ -21,14 +28,14 @@ export class DictController { @Get() @ApiOperation({ summary: '获取字典配置列表' }) @ApiResult({ type: [DictEntity] }) - @Permission(PermissionDict.LIST) + @Permission(Permissions.LIST) async list(@Query() dto: DictQueryDto): Promise> { return this.dictService.page(dto); } @Post() @ApiOperation({ summary: '新增字典配置' }) - @Permission(PermissionDict.CREATE) + @Permission(Permissions.CREATE) async create(@Body() dto: DictDto): Promise { await this.dictService.isExistKey(dto.key); await this.dictService.create(dto); @@ -37,21 +44,21 @@ export class DictController { @Get(':id') @ApiOperation({ summary: '查询字典配置信息' }) @ApiResult({ type: DictEntity }) - @Permission(PermissionDict.READ) + @Permission(Permissions.READ) async info(@IdParam() id: number): Promise { return this.dictService.findOne(id); } @Post(':id') @ApiOperation({ summary: '更新字典配置' }) - @Permission(PermissionDict.UPDATE) + @Permission(Permissions.UPDATE) async update(@IdParam() id: number, @Body() dto: DictDto): Promise { await this.dictService.update(id, dto); } @Delete(':id') @ApiOperation({ summary: '删除指定的字典配置' }) - @Permission(PermissionDict.DELETE) + @Permission(Permissions.DELETE) async delete(@IdParam() id: number): Promise { await this.dictService.delete(id); } diff --git a/apps/api/src/modules/system/dict/dict.service.ts b/apps/api/src/modules/system/dict/dict.service.ts index 15e4d5c..a8abbf7 100644 --- a/apps/api/src/modules/system/dict/dict.service.ts +++ b/apps/api/src/modules/system/dict/dict.service.ts @@ -3,8 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiException } from '@/exceptions/api.exception'; import { paginate } from '@/helper/paginate'; import { Pagination } from '@/helper/paginate/pagination'; import { DictEntity } from '@/modules/system/dict/dict.entity'; @@ -75,7 +75,7 @@ export class DictService { async isExistKey(key: string): Promise { const result = await this.dictRepository.findOneBy({ key }); if (result) { - throw new ApiException(ErrorEnum.PARAMETER_CONFIG_KEY_EXISTS); + throw new BusinessException(ErrorEnum.PARAMETER_CONFIG_KEY_EXISTS); } } diff --git a/apps/api/src/modules/system/dict/permission.ts b/apps/api/src/modules/system/dict/permission.ts deleted file mode 100644 index 238f6e0..0000000 --- a/apps/api/src/modules/system/dict/permission.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const PermissionDict = { - LIST: 'system:dict:list', - CREATE: 'system:dict:create', - READ: 'system:dict:read', - UPDATE: 'system:dict:update', - DELETE: 'system:dict:delete', -} as const; diff --git a/apps/api/src/modules/system/log/entities/login-log.entity.ts b/apps/api/src/modules/system/log/entities/login-log.entity.ts index bd8d3ce..3c7955d 100644 --- a/apps/api/src/modules/system/log/entities/login-log.entity.ts +++ b/apps/api/src/modules/system/log/entities/login-log.entity.ts @@ -3,7 +3,7 @@ import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; import { AbstractEntity } from '@/common/entity/abstract.entity'; -import { UserEntity } from '../../user/entities/user.entity'; +import { UserEntity } from '../../../user/entities/user.entity'; @Entity({ name: 'sys_login_log' }) export class LoginLogEntity extends AbstractEntity { diff --git a/apps/api/src/modules/system/log/log.controller.ts b/apps/api/src/modules/system/log/log.controller.ts index fbfa4a8..34a0c20 100644 --- a/apps/api/src/modules/system/log/log.controller.ts +++ b/apps/api/src/modules/system/log/log.controller.ts @@ -1,11 +1,11 @@ import { Controller, Get, Query } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { ApiResult } from '@/decorators'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; import { Pagination } from '@/helper/paginate/pagination'; -import { Permission } from '@/modules/rbac/decorators'; +import { Permission } from '@/modules/auth/decorators/permission.decorator'; import { CaptchaLogQueryDto, @@ -14,11 +14,17 @@ import { } from './dto/log.dto'; import { CaptchaLogEntity } from './entities/captcha-log.entity'; import { TaskLogEntity } from './entities/task-log.entity'; -import { LoginLogInfo } from './log.modal'; +import { LoginLogInfo } from './models/log.model'; import { CaptchaLogService } from './services/captcha-log.service'; import { LoginLogService } from './services/login-log.service'; import { TaskLogService } from './services/task-log.service'; +export const Permissions = { + TaskList: 'system:log:task:list', + LogList: 'system:log:login:list', + CaptchaList: 'system:log:captcha:list', +}; + @ApiSecurityAuth() @ApiTags('System - 日志模块') @Controller('log') @@ -32,7 +38,7 @@ export class LogController { @Get('login/list') @ApiOperation({ summary: '查询登录日志列表' }) @ApiResult({ type: [LoginLogInfo], isPage: true }) - @Permission('system:log:task:list') + @Permission(Permissions.TaskList) async loginLogPage( @Query() dto: LoginLogQueryDto, ): Promise> { @@ -42,7 +48,7 @@ export class LogController { @Get('task/list') @ApiOperation({ summary: '查询任务日志列表' }) @ApiResult({ type: [TaskLogEntity], isPage: true }) - @Permission('system:log:task:list') + @Permission(Permissions.LogList) async taskList(@Query() dto: TaskLogQueryDto) { return this.taskService.list(dto); } @@ -50,7 +56,7 @@ export class LogController { @Get('captcha/list') @ApiOperation({ summary: '查询验证码日志列表' }) @ApiResult({ type: [CaptchaLogEntity], isPage: true }) - @Permission('system:log:captcha:list') + @Permission(Permissions.CaptchaList) async captchaList( @Query() dto: CaptchaLogQueryDto, ): Promise> { diff --git a/apps/api/src/modules/system/log/log.modal.ts b/apps/api/src/modules/system/log/log.modal.ts deleted file mode 100644 index a962a2f..0000000 --- a/apps/api/src/modules/system/log/log.modal.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class LoginLogInfo { - @ApiProperty({ description: '日志编号' }) - id: number; - - @ApiProperty({ description: '登录ip', example: '1.1.1.1' }) - ip: string; - - @ApiProperty({ description: '登录地址' }) - address: string; - - @ApiProperty({ description: '系统', example: 'Windows 10' }) - os: string; - - @ApiProperty({ description: '浏览器', example: 'Chrome' }) - browser: string; - - @ApiProperty({ description: '时间', example: '2022-01-01 00:00:00' }) - time: string; - - @ApiProperty({ description: '登录用户名', example: 'admin' }) - username: string; -} diff --git a/apps/api/src/modules/system/log/log.module.ts b/apps/api/src/modules/system/log/log.module.ts index 42782e0..7f1b5b8 100644 --- a/apps/api/src/modules/system/log/log.module.ts +++ b/apps/api/src/modules/system/log/log.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { UserModule } from '../user/user.module'; +import { UserModule } from '../../user/user.module'; import { CaptchaLogEntity } from './entities/captcha-log.entity'; import { LoginLogEntity } from './entities/login-log.entity'; diff --git a/apps/api/src/modules/system/log/services/login-log.service.ts b/apps/api/src/modules/system/log/services/login-log.service.ts index 450a171..2338cc5 100644 --- a/apps/api/src/modules/system/log/services/login-log.service.ts +++ b/apps/api/src/modules/system/log/services/login-log.service.ts @@ -7,11 +7,11 @@ import UAParser from 'ua-parser-js'; import { paginateRaw } from '@/helper/paginate'; -import { IpService } from '@/modules/shared/ip/ip.service'; +import { IpService } from '@/shared/ip/ip.service'; import { LoginLogQueryDto } from '../dto/log.dto'; import { LoginLogEntity } from '../entities/login-log.entity'; -import { LoginLogInfo } from '../log.modal'; +import { LoginLogInfo } from '../models/log.model'; async function parseLoginLog(e: any, parser: UAParser): Promise { const uaResult = parser.setUA(e.login_log_ua).getResult(); diff --git a/apps/api/src/modules/system/menu/menu.controller.ts b/apps/api/src/modules/system/menu/menu.controller.ts index ca1f789..67c9504 100644 --- a/apps/api/src/modules/system/menu/menu.controller.ts +++ b/apps/api/src/modules/system/menu/menu.controller.ts @@ -1,4 +1,5 @@ import { + BadRequestException, Body, Controller, Delete, @@ -7,50 +8,50 @@ import { Put, Query, } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { flattenDeep } from 'lodash'; -import { IAppConfig } from '@/config'; -import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiResult } from '@/decorators/api-result.decorator'; -import { IdParam } from '@/decorators/id-param.decorator'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; -import { ApiException } from '@/exceptions/api.exception'; -import { Permission } from '@/modules/rbac/decorators'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; +import { IdParam } from '@/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; +import { Permission } from '@/modules/auth/decorators/permission.decorator'; import { MenuEntity } from '@/modules/system/menu/menu.entity'; import { MenuDto, MenuQueryDto } from './menu.dto'; import { MenuService } from './menu.service'; -import { PermissionMenu } from './permission'; + +export const Permissions = { + LIST: 'system:menu:list', + CREATE: 'system:menu:create', + READ: 'system:menu:read', + UPDATE: 'system:menu:update', + DELETE: 'system:menu:delete', +} as const; @ApiTags('System - 菜单权限模块') @ApiSecurityAuth() @Controller('menus') export class MenuController { - constructor( - private menuService: MenuService, - private configService: ConfigService, - ) {} + constructor(private menuService: MenuService) {} @Get() @ApiOperation({ summary: '获取所有菜单列表' }) @ApiResult({ type: [MenuEntity] }) - @Permission(PermissionMenu.LIST) + @Permission(Permissions.LIST) async list(@Query() dto: MenuQueryDto) { return this.menuService.list(dto); } @Get(':id') @ApiOperation({ summary: '获取菜单或权限信息' }) - @Permission(PermissionMenu.READ) + @Permission(Permissions.READ) async info(@IdParam() id: number) { return this.menuService.getMenuItemAndParentInfo(id); } @Post() @ApiOperation({ summary: '新增菜单或权限' }) - @Permission(PermissionMenu.CREATE) + @Permission(Permissions.CREATE) async create(@Body() dto: MenuDto): Promise { // check await this.menuService.check(dto); @@ -70,14 +71,11 @@ export class MenuController { @Put(':id') @ApiOperation({ summary: '更新菜单或权限' }) - @Permission(PermissionMenu.UPDATE) + @Permission(Permissions.UPDATE) async update( @IdParam() id: number, @Body() dto: Partial, ): Promise { - if (id <= this.configService.get('app').protectSysPermMenuMaxId) - throw new ApiException(ErrorEnum.SYSTEM_BUILTIN_FUNCTION_NOT_ALLOWED); - // check await this.menuService.check(dto); if (dto.parent === -1 || !dto.parent) { @@ -93,15 +91,12 @@ export class MenuController { @Delete(':id') @ApiOperation({ summary: '删除菜单或权限' }) - @Permission(PermissionMenu.DELETE) + @Permission(Permissions.DELETE) async delete(@IdParam() id: number): Promise { - // 68为内置init.sql中插入最后的索引编号 - if ( - id <= this.configService.get('app').protectSysPermMenuMaxId - ) { - // 系统内置功能不提供删除 - throw new ApiException(ErrorEnum.SYSTEM_BUILTIN_FUNCTION_NOT_ALLOWED); + if (await this.menuService.checkRoleByMenuId(id)) { + throw new BadRequestException('该菜单存在关联角色,无法删除'); } + // 如果有子目录,一并删除 const childMenus = await this.menuService.findChildMenus(id); await this.menuService.deleteMenuItem(flattenDeep([id, childMenus])); diff --git a/apps/api/src/modules/system/menu/menu.service.ts b/apps/api/src/modules/system/menu/menu.service.ts index fe0a943..ae5765e 100644 --- a/apps/api/src/modules/system/menu/menu.service.ts +++ b/apps/api/src/modules/system/menu/menu.service.ts @@ -6,8 +6,8 @@ import { concat, isEmpty, uniq } from 'lodash'; import { In, IsNull, Like, Not, Repository } from 'typeorm'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiException } from '@/exceptions/api.exception'; import { MenuEntity } from '@/modules/system/menu/menu.entity'; import { deleteEmptyChildren } from '@/utils'; @@ -92,16 +92,18 @@ export class MenuService { async check(dto: Partial): Promise { if (dto.type === 2 && !dto.parent) { // 无法直接创建权限,必须有parent - throw new ApiException(ErrorEnum.PERMISSION_REQUIRES_PARENT); + throw new BusinessException(ErrorEnum.PERMISSION_REQUIRES_PARENT); } if (dto.type === 1 && dto.parent) { const parent = await this.getMenuItemInfo(dto.parent); if (isEmpty(parent)) { - throw new ApiException(ErrorEnum.PARENT_MENU_NOT_FOUND); + throw new BusinessException(ErrorEnum.PARENT_MENU_NOT_FOUND); } if (parent && parent.type === 1) { // 当前新增为菜单但父节点也为菜单时为非法操作 - throw new ApiException(ErrorEnum.ILLEGAL_OPERATION_DIRECTORY_PARENT); + throw new BusinessException( + ErrorEnum.ILLEGAL_OPERATION_DIRECTORY_PARENT, + ); } } } @@ -225,4 +227,17 @@ export class MenuService { }); } } + + /** + * 根据菜单ID查找是否有关联角色 + */ + async checkRoleByMenuId(id: number): Promise { + return !!(await this.menuRepository.findOne({ + where: { + roles: { + id, + }, + }, + })); + } } diff --git a/apps/api/src/modules/system/menu/permission.ts b/apps/api/src/modules/system/menu/permission.ts deleted file mode 100644 index 7df4e62..0000000 --- a/apps/api/src/modules/system/menu/permission.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const PermissionMenu = { - LIST: 'system:menu:list', - CREATE: 'system:menu:create', - READ: 'system:menu:read', - UPDATE: 'system:menu:update', - DELETE: 'system:menu:delete', -} as const; diff --git a/apps/api/src/modules/system/online/online.controller.ts b/apps/api/src/modules/system/online/online.controller.ts index d9b46d7..a248a29 100644 --- a/apps/api/src/modules/system/online/online.controller.ts +++ b/apps/api/src/modules/system/online/online.controller.ts @@ -1,14 +1,14 @@ import { Body, Controller, Get, Post } from '@nestjs/common'; import { ApiExtraModels, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiResult } from '@/decorators/api-result.decorator'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; -import { ApiException } from '@/exceptions/api.exception'; -import { AuthUser } from '@/modules/auth/decorators'; +import { AuthUser } from '@/modules/auth/decorators/auth-user.decorator'; -import { Permission } from '@/modules/rbac/decorators'; +import { Permission } from '@/modules/auth/decorators/permission.decorator'; import { KickDto } from './online.dto'; import { OnlineUserInfo } from './online.model'; @@ -34,7 +34,7 @@ export class OnlineController { @Permission('system:online:kick') async kick(@Body() dto: KickDto, @AuthUser() user: IAuthUser): Promise { if (dto.id === user.uid) { - throw new ApiException(ErrorEnum.NOT_ALLOWED_TO_LOGOUT_USER); + throw new BusinessException(ErrorEnum.NOT_ALLOWED_TO_LOGOUT_USER); } await this.onlineService.kickUser(dto.id, user.uid); } diff --git a/apps/api/src/modules/system/online/online.module.ts b/apps/api/src/modules/system/online/online.module.ts index d68ce63..e11fe6e 100644 --- a/apps/api/src/modules/system/online/online.module.ts +++ b/apps/api/src/modules/system/online/online.module.ts @@ -3,11 +3,10 @@ import { Module, forwardRef } from '@nestjs/common'; import { AuthModule } from '@/modules/auth/auth.module'; import { SocketModule } from '@/modules/socket/socket.module'; +import { UserModule } from '../../user/user.module'; import { RoleModule } from '../role/role.module'; import { SystemModule } from '../system.module'; -import { UserModule } from '../user/user.module'; - import { OnlineController } from './online.controller'; import { OnlineService } from './online.service'; diff --git a/apps/api/src/modules/system/online/online.service.ts b/apps/api/src/modules/system/online/online.service.ts index da8a736..928c0a9 100644 --- a/apps/api/src/modules/system/online/online.service.ts +++ b/apps/api/src/modules/system/online/online.service.ts @@ -6,13 +6,13 @@ import { EntityManager } from 'typeorm'; import { UAParser } from 'ua-parser-js'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiException } from '@/exceptions/api.exception'; import { AdminWSGateway } from '@/modules/socket/admin-ws.gateway'; import { AdminWSService } from '@/modules/socket/admin-ws.service'; import { EVENT_KICK } from '@/modules/socket/socket.event'; -import { UserService } from '../user/user.service'; +import { UserService } from '../../user/user.service'; import { OnlineUserInfo } from './online.model'; @@ -48,7 +48,7 @@ export class OnlineService { const rootUserId = await this.userService.findRootUserId(); const currentUserInfo = await this.userService.getAccountInfo(currentUid); if (uid === rootUserId) { - throw new ApiException(ErrorEnum.NOT_ALLOWED_TO_LOGOUT_USER); + throw new BusinessException(ErrorEnum.NOT_ALLOWED_TO_LOGOUT_USER); } // reset redis keys await this.userService.forbidden(uid); diff --git a/apps/api/src/modules/system/role/permission.ts b/apps/api/src/modules/system/role/permission.ts deleted file mode 100644 index 02a4963..0000000 --- a/apps/api/src/modules/system/role/permission.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const PermissionRole = { - LIST: 'system:role:list', - CREATE: 'system:role:create', - READ: 'system:role:read', - UPDATE: 'system:role:update', - DELETE: 'system:role:delete', -} as const; diff --git a/apps/api/src/modules/system/role/role.controller.ts b/apps/api/src/modules/system/role/role.controller.ts index 426c5a0..36e048f 100644 --- a/apps/api/src/modules/system/role/role.controller.ts +++ b/apps/api/src/modules/system/role/role.controller.ts @@ -1,4 +1,5 @@ import { + BadRequestException, Body, Controller, Delete, @@ -9,19 +10,26 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; +import { IdParam } from '@/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; import { PageOptionsDto } from '@/common/dto/page-options.dto'; -import { ApiResult } from '@/decorators/api-result.decorator'; -import { IdParam } from '@/decorators/id-param.decorator'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; -import { Permission } from '@/modules/rbac/decorators'; +import { Permission } from '@/modules/auth/decorators/permission.decorator'; import { RoleEntity } from '@/modules/system/role/role.entity'; import { MenuService } from '../menu/menu.service'; -import { PermissionRole } from './permission'; import { RoleDto } from './role.dto'; import { RoleService } from './role.service'; +export const Permissions = { + LIST: 'system:role:list', + CREATE: 'system:role:create', + READ: 'system:role:read', + UPDATE: 'system:role:update', + DELETE: 'system:role:delete', +} as const; + @ApiTags('System - 角色模块') @ApiSecurityAuth() @Controller('roles') @@ -34,7 +42,7 @@ export class RoleController { @Get() @ApiOperation({ summary: '获取角色列表' }) @ApiResult({ type: [RoleEntity] }) - @Permission(PermissionRole.LIST) + @Permission(Permissions.LIST) async list(@Query() dto: PageOptionsDto) { return this.roleService.findAll(dto); } @@ -42,21 +50,21 @@ export class RoleController { @Get(':id') @ApiOperation({ summary: '获取角色信息' }) @ApiResult({ type: RoleEntity }) - @Permission(PermissionRole.READ) + @Permission(Permissions.READ) async info(@IdParam() id: number) { return this.roleService.info(id); } @Post() @ApiOperation({ summary: '新增角色' }) - @Permission(PermissionRole.CREATE) + @Permission(Permissions.CREATE) async create(@Body() dto: RoleDto): Promise { await this.roleService.create(dto); } @Put(':id') @ApiOperation({ summary: '更新角色' }) - @Permission(PermissionRole.UPDATE) + @Permission(Permissions.UPDATE) async update( @IdParam() id: number, @Body() dto: Partial, @@ -67,8 +75,12 @@ export class RoleController { @Delete(':id') @ApiOperation({ summary: '删除角色' }) - @Permission(PermissionRole.DELETE) + @Permission(Permissions.DELETE) async delete(@IdParam() id: number): Promise { + if (await this.roleService.checkUserByRoleId(id)) { + throw new BadRequestException('该角色存在关联用户,无法删除'); + } + await this.roleService.delete(id); await this.menuService.refreshOnlineUserPerms(); } diff --git a/apps/api/src/modules/system/role/role.entity.ts b/apps/api/src/modules/system/role/role.entity.ts index d6ed580..a5941e8 100644 --- a/apps/api/src/modules/system/role/role.entity.ts +++ b/apps/api/src/modules/system/role/role.entity.ts @@ -9,8 +9,8 @@ import { import { AbstractEntity } from '@/common/entity/abstract.entity'; +import { UserEntity } from '../../user/entities/user.entity'; import { MenuEntity } from '../menu/menu.entity'; -import { UserEntity } from '../user/entities/user.entity'; @Entity({ name: 'sys_role' }) export class RoleEntity extends AbstractEntity { diff --git a/apps/api/src/modules/system/role/role.service.ts b/apps/api/src/modules/system/role/role.service.ts index c9c0918..0f197cb 100644 --- a/apps/api/src/modules/system/role/role.service.ts +++ b/apps/api/src/modules/system/role/role.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; -import { difference, isEmpty } from 'lodash'; +import { isEmpty } from 'lodash'; import { EntityManager, In, Repository } from 'typeorm'; import { PageOptionsDto } from '@/common/dto/page-options.dto'; @@ -38,21 +38,21 @@ export class RoleService { /** * 根据角色获取角色信息 */ - async info(rid: number): Promise { + async info(id: number): Promise { const info = await this.roleRepository .createQueryBuilder('role') .where({ - id: rid, + id, }) .getOne(); - if (rid === this.configService.get('app').adminRoleId) { - const menus = await this.menuRepository.find({ select: ['id'] }); - return { ...info, menuIds: menus.map((m) => m.id) }; - } + // if (id === this.configService.get('app').adminRoleId) { + // const menus = await this.menuRepository.find({ select: ['id'] }); + // return { ...info, menuIds: menus.map((m) => m.id) }; + // } const menus = await this.menuRepository.find({ - where: { roles: { id: rid } }, + where: { roles: { id } }, select: ['id'], }); @@ -87,24 +87,18 @@ export class RoleService { async update(id, { menuIds, ...data }: Partial): Promise { await this.roleRepository.update(id, data); - // 对比 menu 差异 - const originMenus = await this.menuRepository.find({ - where: { roles: { id } }, - }); - const originMenuIds = originMenus.map((m) => m.id); - const insertMenusRowIds = difference(menuIds, originMenuIds); - const deleteMenusRowIds = difference(originMenuIds, menuIds); - - // using transaction - await this.entityManager.transaction(async (manager) => { - if (!isEmpty(menuIds)) { - await manager - .createQueryBuilder() - .relation(RoleEntity, 'menus') - .of(id) - .addAndRemove(insertMenusRowIds, deleteMenusRowIds); - } - }); + if (!isEmpty(menuIds)) { + // using transaction + await this.entityManager.transaction(async (manager) => { + const menus = await this.menuRepository.find({ + where: { id: In(menuIds) }, + }); + + const role = await this.roleRepository.findOne({ where: { id } }); + role.menus = menus; + await manager.save(role); + }); + } } /** @@ -151,4 +145,17 @@ export class RoleService { (r) => r === this.configService.get('app').adminRoleId, ); } + + /** + * 根据角色ID查找是否有关联用户 + */ + async checkUserByRoleId(id: number): Promise { + return !!(await this.roleRepository.findOne({ + where: { + users: { + id, + }, + }, + })); + } } diff --git a/apps/api/src/modules/system/serve/serve.controller.ts b/apps/api/src/modules/system/serve/serve.controller.ts index 589842a..9c92f8c 100644 --- a/apps/api/src/modules/system/serve/serve.controller.ts +++ b/apps/api/src/modules/system/serve/serve.controller.ts @@ -2,11 +2,11 @@ import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager'; import { Controller, Get, UseInterceptors } from '@nestjs/common'; import { ApiExtraModels, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { ApiResult } from '@/decorators/api-result.decorator'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; -import { AllowAnon } from '@/modules/rbac/decorators'; +import { AllowAnon } from '@/modules/auth/decorators/allow-anon.decorator'; import { ServeStatInfo } from './serve.model'; import { ServeService } from './serve.service'; diff --git a/apps/api/src/modules/system/system.module.ts b/apps/api/src/modules/system/system.module.ts index 6614f20..e573cdf 100644 --- a/apps/api/src/modules/system/system.module.ts +++ b/apps/api/src/modules/system/system.module.ts @@ -2,6 +2,8 @@ import { Module } from '@nestjs/common'; import { RouterModule } from '@nestjs/core'; +import { UserModule } from '../user/user.module'; + import { DeptModule } from './dept/dept.module'; import { DictModule } from './dict/dict.module'; import { LogModule } from './log/log.module'; @@ -10,7 +12,6 @@ import { OnlineModule } from './online/online.module'; import { RoleModule } from './role/role.module'; import { ServeModule } from './serve/serve.module'; import { TaskModule } from './task/task.module'; -import { UserModule } from './user/user.module'; const modules = [ UserModule, @@ -26,8 +27,6 @@ const modules = [ @Module({ imports: [ - // forwardRef(() => WSModule), - // forwardRef(() => AuthModule), ...modules, RouterModule.register([ { diff --git a/apps/api/src/modules/system/task/constant.ts b/apps/api/src/modules/system/task/constant.ts index 82f2312..e32bf7a 100644 --- a/apps/api/src/modules/system/task/constant.ts +++ b/apps/api/src/modules/system/task/constant.ts @@ -1,15 +1,3 @@ -export const PermissionTask = { - LIST: 'system:task:list', - CREATE: 'system:task:create', - READ: 'system:task:read', - UPDATE: 'system:task:update', - DELETE: 'system:task:delete', - - ONCE: 'system:task:once', - START: 'system:task:start', - STOP: 'system:task:stop', -} as const; - export enum TaskStatus { Disabled = 0, Activited = 1, diff --git a/apps/api/src/modules/system/task/task.controller.ts b/apps/api/src/modules/system/task/task.controller.ts index bbc8d7e..3349cce 100644 --- a/apps/api/src/modules/system/task/task.controller.ts +++ b/apps/api/src/modules/system/task/task.controller.ts @@ -9,17 +9,28 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { ApiResult } from '@/decorators/api-result.decorator'; -import { IdParam } from '@/decorators/id-param.decorator'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; +import { IdParam } from '@/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; import { Pagination } from '@/helper/paginate/pagination'; -import { Permission } from '@/modules/rbac/decorators'; +import { Permission } from '@/modules/auth/decorators/permission.decorator'; import { TaskEntity } from '@/modules/system/task/task.entity'; -import { PermissionTask } from './constant'; import { TaskDto, TaskQueryDto } from './task.dto'; import { TaskService } from './task.service'; +export const Permissions = { + LIST: 'system:task:list', + CREATE: 'system:task:create', + READ: 'system:task:read', + UPDATE: 'system:task:update', + DELETE: 'system:task:delete', + + ONCE: 'system:task:once', + START: 'system:task:start', + STOP: 'system:task:stop', +} as const; + @ApiTags('System - 任务调度模块') @ApiSecurityAuth() @Controller('tasks') @@ -29,14 +40,14 @@ export class TaskController { @Get() @ApiOperation({ summary: '获取任务列表' }) @ApiResult({ type: [TaskEntity] }) - @Permission(PermissionTask.LIST) + @Permission(Permissions.LIST) async list(@Query() dto: TaskQueryDto): Promise> { return this.taskService.list(dto); } @Post() @ApiOperation({ summary: '添加任务' }) - @Permission(PermissionTask.CREATE) + @Permission(Permissions.CREATE) async create(@Body() dto: TaskDto): Promise { const serviceCall = dto.service.split('.'); await this.taskService.checkHasMissionMeta(serviceCall[0], serviceCall[1]); @@ -45,7 +56,7 @@ export class TaskController { @Put(':id') @ApiOperation({ summary: '更新任务' }) - @Permission(PermissionTask.UPDATE) + @Permission(Permissions.UPDATE) async update( @IdParam() id: number, @Body() dto: Partial, @@ -58,14 +69,14 @@ export class TaskController { @Get(':id') @ApiOperation({ summary: '查询任务详细信息' }) @ApiResult({ type: TaskEntity }) - @Permission(PermissionTask.READ) + @Permission(Permissions.READ) async info(@IdParam() id: number): Promise { return this.taskService.info(id); } @Delete(':id') @ApiOperation({ summary: '删除任务' }) - @Permission(PermissionTask.DELETE) + @Permission(Permissions.DELETE) async delete(@IdParam() id: number): Promise { const task = await this.taskService.info(id); await this.taskService.delete(task); @@ -73,7 +84,7 @@ export class TaskController { @Put(':id/once') @ApiOperation({ summary: '手动执行一次任务' }) - @Permission(PermissionTask.ONCE) + @Permission(Permissions.ONCE) async once(@IdParam() id: number): Promise { const task = await this.taskService.info(id); await this.taskService.once(task); @@ -81,7 +92,7 @@ export class TaskController { @Put(':id/stop') @ApiOperation({ summary: '停止任务' }) - @Permission(PermissionTask.STOP) + @Permission(Permissions.STOP) async stop(@IdParam() id: number): Promise { const task = await this.taskService.info(id); await this.taskService.stop(task); @@ -89,7 +100,7 @@ export class TaskController { @Put(':id/start') @ApiOperation({ summary: '启动任务' }) - @Permission(PermissionTask.START) + @Permission(Permissions.START) async start(@IdParam() id: number): Promise { const task = await this.taskService.info(id); diff --git a/apps/api/src/modules/system/task/task.service.ts b/apps/api/src/modules/system/task/task.service.ts index 8a19b0c..9c2e974 100644 --- a/apps/api/src/modules/system/task/task.service.ts +++ b/apps/api/src/modules/system/task/task.service.ts @@ -3,6 +3,7 @@ import { InjectQueue } from '@nestjs/bull'; import { BadRequestException, Injectable, + Logger, NotFoundException, OnModuleInit, } from '@nestjs/common'; @@ -14,12 +15,11 @@ import Redis from 'ioredis'; import { isEmpty } from 'lodash'; import { Like, Repository } from 'typeorm'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiException } from '@/exceptions/api.exception'; import { paginate } from '@/helper/paginate'; import { Pagination } from '@/helper/paginate/pagination'; -import { AppLoggerService } from '@/modules/shared/services/app-logger.service'; import { TaskEntity } from '@/modules/system/task/task.entity'; import { MISSION_DECORATOR_KEY } from '@/modules/tasks/mission.decorator'; @@ -34,6 +34,8 @@ import { TaskDto, TaskQueryDto } from './task.dto'; @Injectable() export class TaskService implements OnModuleInit { + private logger = new Logger(TaskService.name); + constructor( @InjectRepository(TaskEntity) private taskRepository: Repository, @@ -41,8 +43,6 @@ export class TaskService implements OnModuleInit { private moduleRef: ModuleRef, private reflector: Reflector, @InjectRedis() private redis: Redis, - - private logger: AppLoggerService, ) {} /** @@ -313,7 +313,7 @@ export class TaskService implements OnModuleInit { ); // 如果没有,则抛出错误 if (!hasMission) { - throw new ApiException(ErrorEnum.INSECURE_MISSION); + throw new BusinessException(ErrorEnum.INSECURE_MISSION); } } catch (e) { if (e instanceof UnknownElementException) { diff --git a/apps/api/src/modules/system/user/dto/index.ts b/apps/api/src/modules/system/user/dto/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/apps/api/src/modules/system/user/entities/index.ts b/apps/api/src/modules/system/user/entities/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/apps/api/src/modules/system/user/permission.ts b/apps/api/src/modules/system/user/permission.ts deleted file mode 100644 index 6643e0d..0000000 --- a/apps/api/src/modules/system/user/permission.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const PermissionUser = { - LIST: 'system:user:list', - CREATE: 'system:user:create', - READ: 'system:user:read', - UPDATE: 'system:user:update', - DELETE: 'system:user:delete', - - PASSWORD_UPDATE: 'system:user:password:update', - PASSWORD_RESET: 'system:user:pass:reset', -} as const; diff --git a/apps/api/src/modules/tasks/jobs/email.job.ts b/apps/api/src/modules/tasks/jobs/email.job.ts index 2413031..a71c21a 100644 --- a/apps/api/src/modules/tasks/jobs/email.job.ts +++ b/apps/api/src/modules/tasks/jobs/email.job.ts @@ -1,8 +1,7 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; -import { MailerService } from '@/modules/shared/mailer/mailer.service'; +import { MailerService } from '@/shared/mailer/mailer.service'; -import { AppLoggerService } from '../../shared/services/app-logger.service'; import { Mission } from '../mission.decorator'; /** @@ -13,7 +12,7 @@ import { Mission } from '../mission.decorator'; export class EmailJob { constructor( private readonly emailService: MailerService, - private readonly logger: AppLoggerService, + private readonly logger: Logger, ) {} async send(config: any): Promise { diff --git a/apps/api/src/modules/tasks/jobs/http-request.job.ts b/apps/api/src/modules/tasks/jobs/http-request.job.ts index c18f3ea..c0a649d 100644 --- a/apps/api/src/modules/tasks/jobs/http-request.job.ts +++ b/apps/api/src/modules/tasks/jobs/http-request.job.ts @@ -1,7 +1,6 @@ import { HttpService } from '@nestjs/axios'; -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; -import { AppLoggerService } from '../../shared/services/app-logger.service'; import { Mission } from '../mission.decorator'; /** @@ -12,7 +11,7 @@ import { Mission } from '../mission.decorator'; export class HttpRequestJob { constructor( private readonly httpService: HttpService, - private readonly logger: AppLoggerService, + private readonly logger: Logger, ) {} /** diff --git a/apps/api/src/modules/apps/todo/todo.controller.ts b/apps/api/src/modules/todo/todo.controller.ts similarity index 61% rename from apps/api/src/modules/apps/todo/todo.controller.ts rename to apps/api/src/modules/todo/todo.controller.ts index c0eea39..7c53409 100644 --- a/apps/api/src/modules/apps/todo/todo.controller.ts +++ b/apps/api/src/modules/todo/todo.controller.ts @@ -7,23 +7,29 @@ import { Delete, UseGuards, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiExtraModels } from '@nestjs/swagger'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; -import { ApiResult } from '@/decorators/api-result.decorator'; -import { IdParam } from '@/decorators/id-param.decorator'; -import { TodoEntity } from '@/modules/apps/todo/todo.entity'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; +import { IdParam } from '@/common/decorators/id-param.decorator'; -import { Permission } from '@/modules/rbac/decorators'; -import { Resource } from '@/modules/rbac/decorators/resource.decorator'; +import { Permission } from '@/modules/auth/decorators/permission.decorator'; +import { Resource } from '@/modules/auth/decorators/resource.decorator'; -import { ResourceGuard } from '@/modules/rbac/guards/resource.guard'; +import { ResourceGuard } from '@/modules/auth/guards/resource.guard'; +import { TodoEntity } from '@/modules/todo/todo.entity'; import { TodoDto } from './todo.dto'; -import { PermissionTodo } from './todo.permission'; import { TodoService } from './todo.service'; +export const Permissions = { + LIST: 'todo:list', + CREATE: 'todo:create', + READ: 'todo:read', + UPDATE: 'todo:update', + DELETE: 'todo:delete', +} as const; + @ApiTags('Business - Todo模块') -@ApiExtraModels(TodoEntity) @UseGuards(ResourceGuard) @Controller('todo') export class TodoController { @@ -32,7 +38,7 @@ export class TodoController { @Get() @ApiOperation({ summary: '获取Todo列表' }) @ApiResult({ type: [TodoEntity] }) - @Permission(PermissionTodo.LIST) + @Permission(Permissions.LIST) async list(): Promise { return this.todoService.list(); } @@ -40,21 +46,22 @@ export class TodoController { @Get(':id') @ApiOperation({ summary: '获取Todo详情' }) @ApiResult({ type: TodoEntity }) - @Permission(PermissionTodo.READ) + @Permission(Permissions.READ) async info(@IdParam() id: number): Promise { return this.todoService.detail(id); } @Post() @ApiOperation({ summary: '创建Todo' }) - @Permission(PermissionTodo.CREATE) + @Permission(Permissions.CREATE) async create(@Body() dto: TodoDto): Promise { await this.todoService.create(dto); } @Put(':id') @ApiOperation({ summary: '更新Todo' }) - @Permission(PermissionTodo.UPDATE) + @Permission(Permissions.UPDATE) + @Resource(TodoEntity) async update( @IdParam() id: number, @Body() dto: Partial, @@ -64,7 +71,7 @@ export class TodoController { @Delete(':id') @ApiOperation({ summary: '删除Todo' }) - @Permission(PermissionTodo.DELETE) + @Permission(Permissions.DELETE) @Resource(TodoEntity) async delete(@IdParam() id: number): Promise { await this.todoService.delete(id); diff --git a/apps/api/src/modules/apps/todo/todo.dto.ts b/apps/api/src/modules/todo/todo.dto.ts similarity index 80% rename from apps/api/src/modules/apps/todo/todo.dto.ts rename to apps/api/src/modules/todo/todo.dto.ts index 490f017..e7de581 100644 --- a/apps/api/src/modules/apps/todo/todo.dto.ts +++ b/apps/api/src/modules/todo/todo.dto.ts @@ -9,4 +9,4 @@ export class TodoDto { value: string; } -export class TodoPageDto extends PageOptionsDto {} +export class TodoQueryDto extends PageOptionsDto {} diff --git a/apps/api/src/modules/apps/todo/todo.entity.ts b/apps/api/src/modules/todo/todo.entity.ts similarity index 86% rename from apps/api/src/modules/apps/todo/todo.entity.ts rename to apps/api/src/modules/todo/todo.entity.ts index 8f794bb..9a0c50c 100644 --- a/apps/api/src/modules/apps/todo/todo.entity.ts +++ b/apps/api/src/modules/todo/todo.entity.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; import { AbstractEntity } from '@/common/entity/abstract.entity'; -import { UserEntity } from '@/modules/system/user/entities/user.entity'; +import { UserEntity } from '@/modules/user/entities/user.entity'; @Entity('todo') export class TodoEntity extends AbstractEntity { diff --git a/apps/api/src/modules/apps/todo/todo.module.ts b/apps/api/src/modules/todo/todo.module.ts similarity index 100% rename from apps/api/src/modules/apps/todo/todo.module.ts rename to apps/api/src/modules/todo/todo.module.ts diff --git a/apps/api/src/modules/apps/todo/todo.service.ts b/apps/api/src/modules/todo/todo.service.ts similarity index 77% rename from apps/api/src/modules/apps/todo/todo.service.ts rename to apps/api/src/modules/todo/todo.service.ts index 430a050..8bdfada 100644 --- a/apps/api/src/modules/apps/todo/todo.service.ts +++ b/apps/api/src/modules/todo/todo.service.ts @@ -4,9 +4,9 @@ import { Repository } from 'typeorm'; import { paginate } from '@/helper/paginate'; import { Pagination } from '@/helper/paginate/pagination'; -import { TodoEntity } from '@/modules/apps/todo/todo.entity'; +import { TodoEntity } from '@/modules/todo/todo.entity'; -import { TodoDto, TodoPageDto } from './todo.dto'; +import { TodoDto, TodoQueryDto } from './todo.dto'; @Injectable() export class TodoService { @@ -19,7 +19,10 @@ export class TodoService { return this.todoRepository.find(); } - async page({ page, pageSize }: TodoPageDto): Promise> { + async page({ + page, + pageSize, + }: TodoQueryDto): Promise> { return paginate(this.todoRepository, { page, pageSize }); } @@ -37,8 +40,8 @@ export class TodoService { await this.todoRepository.save(test); } - async update(id: number, data: Partial) { - await this.todoRepository.update(id, data); + async update(id: number, dto: Partial) { + await this.todoRepository.update(id, dto); } async delete(id: number) { diff --git a/apps/api/src/modules/tools/email/email.controller.ts b/apps/api/src/modules/tools/email/email.controller.ts index 251e512..405a740 100644 --- a/apps/api/src/modules/tools/email/email.controller.ts +++ b/apps/api/src/modules/tools/email/email.controller.ts @@ -2,8 +2,8 @@ import { Body, Controller, Post } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; -import { MailerService } from '@/modules/shared/mailer/mailer.service'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; +import { MailerService } from '@/shared/mailer/mailer.service'; import { EmailSendDto } from './email.dto'; diff --git a/apps/api/src/modules/tools/storage/storage.controller.ts b/apps/api/src/modules/tools/storage/storage.controller.ts index cde62e7..7e732d1 100644 --- a/apps/api/src/modules/tools/storage/storage.controller.ts +++ b/apps/api/src/modules/tools/storage/storage.controller.ts @@ -2,12 +2,12 @@ import { Body, Controller, Get, Post, Query } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { ApiResult } from '@/decorators/api-result.decorator'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; +import { ApiResult } from '@/common/decorators/api-result.decorator'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; import { Pagination } from '@/helper/paginate/pagination'; -import { Permission } from '@/modules/rbac/decorators'; +import { Permission } from '@/modules/auth/decorators/permission.decorator'; import { StorageDeleteDto, StoragePageDto } from './storage.dto'; import { StorageInfo } from './storage.modal'; diff --git a/apps/api/src/modules/tools/storage/storage.entity.ts b/apps/api/src/modules/tools/storage/storage.entity.ts index 1313c71..075e6d5 100644 --- a/apps/api/src/modules/tools/storage/storage.entity.ts +++ b/apps/api/src/modules/tools/storage/storage.entity.ts @@ -3,7 +3,7 @@ import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm'; import { AbstractEntity } from '@/common/entity/abstract.entity'; -@Entity({ name: 'tool-storage' }) +@Entity({ name: 'tool_storage' }) export class Storage extends AbstractEntity { @PrimaryGeneratedColumn() @ApiProperty() diff --git a/apps/api/src/modules/tools/storage/storage.service.ts b/apps/api/src/modules/tools/storage/storage.service.ts index 075bdea..e0ece1c 100644 --- a/apps/api/src/modules/tools/storage/storage.service.ts +++ b/apps/api/src/modules/tools/storage/storage.service.ts @@ -5,8 +5,8 @@ import { Between, Like, Repository } from 'typeorm'; import { paginateRaw } from '@/helper/paginate'; import { PaginationTypeEnum } from '@/helper/paginate/interface'; import { Pagination } from '@/helper/paginate/pagination'; -import { UserEntity } from '@/modules/system/user/entities/user.entity'; import { Storage } from '@/modules/tools/storage/storage.entity'; +import { UserEntity } from '@/modules/user/entities/user.entity'; import { deleteFile } from '@/utils/file'; diff --git a/apps/api/src/modules/tools/tools.module.ts b/apps/api/src/modules/tools/tools.module.ts index 6d9d88d..1a58a51 100644 --- a/apps/api/src/modules/tools/tools.module.ts +++ b/apps/api/src/modules/tools/tools.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { UserEntity } from '../system/user/entities/user.entity'; +import { UserEntity } from '../user/entities/user.entity'; import { EmailController } from './email/email.controller'; import { StorageController } from './storage/storage.controller'; diff --git a/apps/api/src/modules/tools/upload/upload.controller.ts b/apps/api/src/modules/tools/upload/upload.controller.ts index 3f2a62f..5b35cf0 100644 --- a/apps/api/src/modules/tools/upload/upload.controller.ts +++ b/apps/api/src/modules/tools/upload/upload.controller.ts @@ -2,10 +2,10 @@ import { MultipartFile } from '@fastify/multipart'; import { BadRequestException, Body, Controller, Post } from '@nestjs/common'; import { ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; import { AuthUser } from '@/modules/auth/decorators/auth-user.decorator'; -import { Permission } from '@/modules/rbac/decorators'; +import { Permission } from '@/modules/auth/decorators/permission.decorator'; import { UploadService } from './upload.service'; diff --git a/apps/api/src/modules/system/user/constant.ts b/apps/api/src/modules/user/constant.ts similarity index 100% rename from apps/api/src/modules/system/user/constant.ts rename to apps/api/src/modules/user/constant.ts diff --git a/apps/api/src/modules/auth/controllers/index.ts b/apps/api/src/modules/user/dto/index.ts similarity index 100% rename from apps/api/src/modules/auth/controllers/index.ts rename to apps/api/src/modules/user/dto/index.ts diff --git a/apps/api/src/modules/system/user/dto/password.dto.ts b/apps/api/src/modules/user/dto/password.dto.ts similarity index 87% rename from apps/api/src/modules/system/user/dto/password.dto.ts rename to apps/api/src/modules/user/dto/password.dto.ts index fff5dce..ba3d895 100644 --- a/apps/api/src/modules/system/user/dto/password.dto.ts +++ b/apps/api/src/modules/user/dto/password.dto.ts @@ -7,8 +7,8 @@ import { MinLength, } from 'class-validator'; -import { IsEntityExist } from '@/database/constraints/entity-exist.constraint'; -import { UserEntity } from '@/modules/system/user/entities/user.entity'; +import { UserEntity } from '@/modules/user/entities/user.entity'; +import { IsEntityExist } from '@/shared/database/constraints/entity-exist.constraint'; export class PasswordUpdateDto { @ApiProperty({ description: '旧密码' }) diff --git a/apps/api/src/modules/system/user/dto/user.dto.ts b/apps/api/src/modules/user/dto/user.dto.ts similarity index 94% rename from apps/api/src/modules/system/user/dto/user.dto.ts rename to apps/api/src/modules/user/dto/user.dto.ts index feb155e..cbb696b 100644 --- a/apps/api/src/modules/system/user/dto/user.dto.ts +++ b/apps/api/src/modules/user/dto/user.dto.ts @@ -17,8 +17,8 @@ import { import { isEmpty } from 'lodash'; import { PageOptionsDto } from '@/common/dto/page-options.dto'; -import { IsUnique } from '@/database/constraints/unique.constraint'; -import { UserEntity } from '@/modules/system/user/entities/user.entity'; +import { UserEntity } from '@/modules/user/entities/user.entity'; +import { IsUnique } from '@/shared/database/constraints/unique.constraint'; export class UserDto { @ApiProperty({ description: '登录账号', example: 'kz-admin' }) diff --git a/apps/api/src/modules/auth/dto/index.ts b/apps/api/src/modules/user/entities/index.ts similarity index 100% rename from apps/api/src/modules/auth/dto/index.ts rename to apps/api/src/modules/user/entities/index.ts diff --git a/apps/api/src/modules/system/user/entities/user.entity.ts b/apps/api/src/modules/user/entities/user.entity.ts similarity index 91% rename from apps/api/src/modules/system/user/entities/user.entity.ts rename to apps/api/src/modules/user/entities/user.entity.ts index 44118c3..bd8ca57 100644 --- a/apps/api/src/modules/system/user/entities/user.entity.ts +++ b/apps/api/src/modules/user/entities/user.entity.ts @@ -14,8 +14,8 @@ import { AbstractEntity } from '@/common/entity/abstract.entity'; import { AccessTokenEntity } from '@/modules/auth/entities/access-token.entity'; -import { DeptEntity } from '../../dept/dept.entity'; -import { RoleEntity } from '../../role/role.entity'; +import { DeptEntity } from '@/modules/system/dept/dept.entity'; +import { RoleEntity } from '@/modules/system/role/role.entity'; @Entity({ name: 'sys_user' }) export class UserEntity extends AbstractEntity { diff --git a/apps/api/src/modules/system/user/user.controller.ts b/apps/api/src/modules/user/user.controller.ts similarity index 70% rename from apps/api/src/modules/system/user/user.controller.ts rename to apps/api/src/modules/user/user.controller.ts index 33800f3..7e4d930 100644 --- a/apps/api/src/modules/system/user/user.controller.ts +++ b/apps/api/src/modules/user/user.controller.ts @@ -5,21 +5,30 @@ import { Get, Post, Put, - Patch, Query, } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { IdParam } from '@/decorators/id-param.decorator'; -import { ApiSecurityAuth } from '@/decorators/swagger.decorator'; -import { Permission } from '@/modules/rbac/decorators'; +import { IdParam } from '@/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '@/common/decorators/swagger.decorator'; +import { Permission } from '@/modules/auth/decorators/permission.decorator'; import { MenuService } from '@/modules/system/menu/menu.service'; import { UserPasswordDto } from './dto/password.dto'; import { UserDto, UserListDto } from './dto/user.dto'; -import { PermissionUser } from './permission'; import { UserService } from './user.service'; +export const Permissions = { + LIST: 'system:user:list', + CREATE: 'system:user:create', + READ: 'system:user:read', + UPDATE: 'system:user:update', + DELETE: 'system:user:delete', + + PASSWORD_UPDATE: 'system:user:password:update', + PASSWORD_RESET: 'system:user:pass:reset', +} as const; + @ApiTags('System - 用户模块') @ApiSecurityAuth() @Controller('users') @@ -31,28 +40,28 @@ export class UserController { @Get() @ApiOperation({ summary: '获取用户列表' }) - @Permission(PermissionUser.LIST) + @Permission(Permissions.LIST) async list(@Query() dto: UserListDto) { return this.userService.findAll(dto); } @Get(':id') @ApiOperation({ summary: '查询用户' }) - @Permission(PermissionUser.READ) + @Permission(Permissions.READ) async read(@IdParam() id: number) { return this.userService.info(id); } @Post() @ApiOperation({ summary: '新增用户' }) - @Permission(PermissionUser.CREATE) + @Permission(Permissions.CREATE) async create(@Body() dto: UserDto): Promise { await this.userService.create(dto); } @Put(':id') @ApiOperation({ summary: '更新用户' }) - @Permission(PermissionUser.UPDATE) + @Permission(Permissions.UPDATE) async update( @IdParam() id: number, @Body() dto: Partial, @@ -63,7 +72,7 @@ export class UserController { @Delete(':id') @ApiOperation({ summary: '删除用户' }) - @Permission(PermissionUser.DELETE) + @Permission(Permissions.DELETE) async delete(@IdParam() id: number): Promise { await this.userService.delete([id]); await this.userService.multiForbidden([id]); @@ -71,7 +80,7 @@ export class UserController { @Post(':id/password') @ApiOperation({ summary: '更改用户密码' }) - @Patch(PermissionUser.PASSWORD_UPDATE) + @Permission(Permissions.PASSWORD_UPDATE) async password(@Body() dto: UserPasswordDto): Promise { await this.userService.forceUpdatePassword(dto.id, dto.password); } diff --git a/apps/api/src/modules/system/user/user.model.ts b/apps/api/src/modules/user/user.model.ts similarity index 100% rename from apps/api/src/modules/system/user/user.model.ts rename to apps/api/src/modules/user/user.model.ts diff --git a/apps/api/src/modules/system/user/user.module.ts b/apps/api/src/modules/user/user.module.ts similarity index 75% rename from apps/api/src/modules/system/user/user.module.ts rename to apps/api/src/modules/user/user.module.ts index fb08f6f..338f5d5 100644 --- a/apps/api/src/modules/system/user/user.module.ts +++ b/apps/api/src/modules/user/user.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { DictModule } from '../dict/dict.module'; +import { DictModule } from '../system/dict/dict.module'; -import { MenuModule } from '../menu/menu.module'; -import { RoleModule } from '../role/role.module'; +import { MenuModule } from '../system/menu/menu.module'; +import { RoleModule } from '../system/role/role.module'; import { UserEntity } from './entities/user.entity'; import { UserController } from './user.controller'; diff --git a/apps/api/src/modules/system/user/user.service.ts b/apps/api/src/modules/user/user.service.ts similarity index 88% rename from apps/api/src/modules/system/user/user.service.ts rename to apps/api/src/modules/user/user.service.ts index 46168d5..0bba5f1 100644 --- a/apps/api/src/modules/system/user/user.service.ts +++ b/apps/api/src/modules/user/user.service.ts @@ -7,22 +7,22 @@ import { isEmpty, isNil } from 'lodash'; import { EntityManager, Like, Repository } from 'typeorm'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { IAppConfig } from '@/config'; import { ErrorEnum } from '@/constants/error-code.constant'; import { SYS_USER_INITPASSWORD } from '@/constants/system.constant'; -import { ApiException } from '@/exceptions/api.exception'; import { paginate } from '@/helper/paginate'; import { Pagination } from '@/helper/paginate/pagination'; import { AccountUpdateDto } from '@/modules/auth/dto/account.dto'; import { RegisterDto } from '@/modules/auth/dto/auth.dto'; -import { QQService } from '@/modules/shared/qq/qq.service'; +import { QQService } from '@/shared/qq/qq.service'; import { MD5, randomValue } from '@/utils'; -import { DictService } from '../dict/dict.service'; +import { DictService } from '../system/dict/dict.service'; -import { RoleEntity } from '../role/role.entity'; +import { RoleEntity } from '../system/role/role.entity'; import { UserStatus } from './constant'; import { PasswordUpdateDto } from './dto/password.dto'; @@ -45,9 +45,16 @@ export class UserService { private readonly configService: ConfigService, ) {} - /** - * 根据用户名查找已经启用的用户 - */ + async findUserById(id: number): Promise { + return this.userRepository + .createQueryBuilder('user') + .where({ + id, + status: UserStatus.Enabled, + }) + .getOne(); + } + async findUserByUserName(username: string): Promise { return this.userRepository .createQueryBuilder('user') @@ -63,8 +70,15 @@ export class UserService { * @param uid user id */ async getAccountInfo(uid: number): Promise { - const user: UserEntity = await this.userRepository.findOneBy({ id: uid }); - if (isEmpty(user)) throw new ApiException(ErrorEnum.USER_NOT_FOUND); + const user: UserEntity = await this.userRepository + .createQueryBuilder('user') + .leftJoinAndSelect('user.roles', 'role') + .where(`user.id = :uid`, { uid }) + .getOne(); + + if (isEmpty(user)) throw new BusinessException(ErrorEnum.USER_NOT_FOUND); + + delete user?.psalt; return user; } @@ -74,7 +88,7 @@ export class UserService { */ async updateAccountInfo(uid: number, info: AccountUpdateDto): Promise { const user = await this.userRepository.findOneBy({ id: uid }); - if (isEmpty(user)) throw new ApiException(ErrorEnum.USER_NOT_FOUND); + if (isEmpty(user)) throw new BusinessException(ErrorEnum.USER_NOT_FOUND); const data = { ...(info.nickname ? { nickname: info.nickname } : null), @@ -101,12 +115,12 @@ export class UserService { async updatePassword(uid: number, dto: PasswordUpdateDto): Promise { const user = await this.userRepository.findOneBy({ id: uid }); if (isEmpty(user)) { - throw new ApiException(ErrorEnum.USER_NOT_FOUND); + throw new BusinessException(ErrorEnum.USER_NOT_FOUND); } const comparePassword = MD5(`${dto.oldPassword}${user.psalt}`); // 原密码不一致,不允许更改 if (user.password !== comparePassword) { - throw new ApiException(ErrorEnum.PASSWORD_MISMATCH); + throw new BusinessException(ErrorEnum.PASSWORD_MISMATCH); } const password = MD5(`${dto.newPassword}${user.psalt}`); await this.userRepository.update({ id: uid }, { password }); @@ -138,7 +152,7 @@ export class UserService { username, }); if (!isEmpty(exists)) { - throw new ApiException(ErrorEnum.SYSTEM_USER_EXISTS); + throw new BusinessException(ErrorEnum.SYSTEM_USER_EXISTS); } await this.entityManager.transaction(async (manager) => { @@ -329,7 +343,7 @@ export class UserService { async exist(username: string) { const user = await this.userRepository.findOneBy({ username }); if (isNil(user)) { - throw new ApiException(ErrorEnum.SYSTEM_USER_EXISTS); + throw new BusinessException(ErrorEnum.SYSTEM_USER_EXISTS); } return true; } @@ -341,7 +355,8 @@ export class UserService { const exists = await this.userRepository.findOneBy({ username, }); - if (!isEmpty(exists)) throw new ApiException(ErrorEnum.SYSTEM_USER_EXISTS); + if (!isEmpty(exists)) + throw new BusinessException(ErrorEnum.SYSTEM_USER_EXISTS); await this.entityManager.transaction(async (manager) => { const salt = randomValue(32); diff --git a/apps/api/src/utils/setup-swagger.ts b/apps/api/src/setup-swagger.ts similarity index 50% rename from apps/api/src/utils/setup-swagger.ts rename to apps/api/src/setup-swagger.ts index db9c5db..902c851 100644 --- a/apps/api/src/utils/setup-swagger.ts +++ b/apps/api/src/setup-swagger.ts @@ -2,23 +2,24 @@ import { INestApplication, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { AbstractEntity } from '../common/entity/abstract.entity'; -import { ResOp, TreeResult } from '../common/model/response.model'; -import { IAppConfig, ISwaggerConfig } from '../config'; -import { API_SECURITY_AUTH } from '../decorators/swagger.decorator'; -import { Pagination } from '../helper/paginate/pagination'; +import { API_SECURITY_AUTH } from './common/decorators/swagger.decorator'; +import { AbstractEntity } from './common/entity/abstract.entity'; +import { ResOp, TreeResult } from './common/model/response.model'; +import { IAppConfig, ISwaggerConfig } from './config'; +import { Pagination } from './helper/paginate/pagination'; export function setupSwagger( app: INestApplication, configService: ConfigService, ): void { + const { name, port } = configService.get('app'); const { enable, path } = configService.get('swagger'); if (!enable) return; const documentBuilder = new DocumentBuilder() - .setTitle(`${configService.get('app').name}`) - .setDescription(`${configService.get('app').name} API document`) + .setTitle(name) + .setDescription(`${name} API document`) .setVersion('1.0'); // auth security @@ -36,23 +37,7 @@ export function setupSwagger( SwaggerModule.setup(path, app, document); - // 导入 ApiFox 忽略全局前缀 - const documentApiFox = SwaggerModule.createDocument( - app, - documentBuilder.build(), - { - ignoreGlobalPrefix: true, - extraModels: [AbstractEntity, ResOp, Pagination, TreeResult], - }, - ); - - SwaggerModule.setup(`${path}-fox`, app, documentApiFox); - // started log const logger = new Logger('SwaggerModule'); - logger.log( - `Document running on http://127.0.0.1:${ - configService.get('app').port - }/${path}`, - ); + logger.log(`Document running on http://127.0.0.1:${port}/${path}`); } diff --git a/apps/api/src/database/constraints/entity-exist.constraint.ts b/apps/api/src/shared/database/constraints/entity-exist.constraint.ts similarity index 100% rename from apps/api/src/database/constraints/entity-exist.constraint.ts rename to apps/api/src/shared/database/constraints/entity-exist.constraint.ts diff --git a/apps/api/src/database/constraints/unique.constraint.ts b/apps/api/src/shared/database/constraints/unique.constraint.ts similarity index 94% rename from apps/api/src/database/constraints/unique.constraint.ts rename to apps/api/src/shared/database/constraints/unique.constraint.ts index b031732..3714cee 100644 --- a/apps/api/src/database/constraints/unique.constraint.ts +++ b/apps/api/src/shared/database/constraints/unique.constraint.ts @@ -21,7 +21,7 @@ type Condition = { @ValidatorConstraint({ name: 'entityItemUnique', async: true }) @Injectable() export class UniqueConstraint implements ValidatorConstraintInterface { - constructor(private dataSource: DataSource) { } + constructor(private dataSource: DataSource) {} async validate(value: any, args: ValidationArguments) { // 获取要验证的模型和字段 @@ -31,9 +31,9 @@ export class UniqueConstraint implements ValidatorConstraintInterface { const condition = ('entity' in args.constraints[0] ? merge(config, args.constraints[0]) : { - ...config, - entity: args.constraints[0], - }) as unknown as Required; + ...config, + entity: args.constraints[0], + }) as unknown as Required; if (!condition.entity) return false; try { // 查询是否存在数据,如果已经存在则验证失败 diff --git a/apps/api/src/database/database.module.ts b/apps/api/src/shared/database/database.module.ts similarity index 90% rename from apps/api/src/database/database.module.ts rename to apps/api/src/shared/database/database.module.ts index ddec799..c24527c 100644 --- a/apps/api/src/database/database.module.ts +++ b/apps/api/src/shared/database/database.module.ts @@ -6,7 +6,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { LoggerOptions } from 'typeorm'; import { IDatabaseConfig } from '@/config'; -import { env } from '@/config/env'; +import { env } from '@/global/env'; import { EntityExistConstraint } from './constraints/entity-exist.constraint'; import { UniqueConstraint } from './constraints/unique.constraint'; @@ -21,6 +21,7 @@ const providers = [EntityExistConstraint, UniqueConstraint]; let loggerOptions: LoggerOptions = env('DB_LOGGING') as 'all'; try { + // 解析成 js 数组 ['error'] loggerOptions = JSON.parse(loggerOptions); } catch { // ignore @@ -39,4 +40,4 @@ const providers = [EntityExistConstraint, UniqueConstraint]; providers, exports: providers, }) -export class AppDatabaseModule {} +export class DatabaseModule {} diff --git a/apps/api/src/database/typeorm-logger.ts b/apps/api/src/shared/database/typeorm-logger.ts similarity index 96% rename from apps/api/src/database/typeorm-logger.ts rename to apps/api/src/shared/database/typeorm-logger.ts index fc675b1..5b65822 100644 --- a/apps/api/src/database/typeorm-logger.ts +++ b/apps/api/src/shared/database/typeorm-logger.ts @@ -2,7 +2,7 @@ import { Logger } from '@nestjs/common'; import { Logger as ITypeORMLogger, LoggerOptions, QueryRunner } from 'typeorm'; export class TypeORMLogger implements ITypeORMLogger { - private readonly logger = new Logger(TypeORMLogger.name); + private logger = new Logger(TypeORMLogger.name); constructor(private options: LoggerOptions) {} @@ -79,6 +79,8 @@ export class TypeORMLogger implements ITypeORMLogger { case 'warn': this.logger.warn(message); break; + default: + break; } } diff --git a/apps/api/src/modules/shared/ip/ip.service.ts b/apps/api/src/shared/ip/ip.service.ts similarity index 100% rename from apps/api/src/modules/shared/ip/ip.service.ts rename to apps/api/src/shared/ip/ip.service.ts diff --git a/apps/api/src/shared/logger/logger.module.ts b/apps/api/src/shared/logger/logger.module.ts new file mode 100644 index 0000000..664d4fc --- /dev/null +++ b/apps/api/src/shared/logger/logger.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { MyLogger } from './logger.service'; + +@Module({ + providers: [MyLogger], + exports: [MyLogger], +}) +export class LoggerModule {} diff --git a/apps/api/src/shared/logger/logger.service.ts b/apps/api/src/shared/logger/logger.service.ts new file mode 100644 index 0000000..e1f2d6a --- /dev/null +++ b/apps/api/src/shared/logger/logger.service.ts @@ -0,0 +1,117 @@ +import { + ConsoleLogger, + ConsoleLoggerOptions, + Injectable, +} from '@nestjs/common'; + +import { ConfigService } from '@nestjs/config'; +import type { Logger as WinstonLogger } from 'winston'; + +import { createLogger, format, transports, config } from 'winston'; + +import 'winston-daily-rotate-file'; + +export enum LogLevel { + ERROR = 'error', + WARN = 'warn', + INFO = 'info', + DEBUG = 'debug', + VERBOSE = 'verbose', +} + +@Injectable() +export class MyLogger extends ConsoleLogger { + private winstonLogger: WinstonLogger; + + constructor( + context: string, + options: ConsoleLoggerOptions, + private configService: ConfigService, + ) { + super(context, options); + this.initWinston(); + } + + protected get level(): LogLevel { + return this.configService.get('app.logger.level') as LogLevel; + } + + protected get maxFiles(): number { + return this.configService.get('app.logger.maxFiles'); + } + + protected initWinston(): void { + this.winstonLogger = createLogger({ + levels: config.npm.levels, + format: format.combine( + format.errors({ stack: true }), + format.timestamp(), + format.json(), + ), + transports: [ + new transports.DailyRotateFile({ + level: this.level, + filename: 'logs/app.%DATE%.log', + datePattern: 'YYYY-MM-DD', + maxFiles: this.maxFiles, + format: format.combine(format.timestamp(), format.json()), + auditFile: 'logs/.audit/app.json', + }), + new transports.DailyRotateFile({ + level: LogLevel.ERROR, + filename: 'logs/app-error.%DATE%.log', + datePattern: 'YYYY-MM-DD', + maxFiles: this.maxFiles, + format: format.combine(format.timestamp(), format.json()), + auditFile: 'logs/.audit/app-error.json', + }), + ], + }); + + // if (isDev) { + // this.winstonLogger.add( + // new transports.Console({ + // level: this.level, + // format: format.combine( + // format.simple(), + // format.colorize({ all: true }), + // ), + // }), + // ); + // } + } + + verbose(message: any, context?: string): void { + super.verbose.apply(this, [message, context]); + + this.winstonLogger.log(LogLevel.VERBOSE, message, { context }); + } + + debug(message: any, context?: string): void { + super.debug.apply(this, [message, context]); + + this.winstonLogger.log(LogLevel.DEBUG, message, { context }); + } + + log(message: any, context?: string): void { + super.log.apply(this, [message, context]); + + this.winstonLogger.log(LogLevel.INFO, message, { context }); + } + + warn(message: any, context?: string): void { + super.warn.apply(this, [message, context]); + + this.winstonLogger.log(LogLevel.WARN, message); + } + + error(message: any, stack?: string, context?: string): void { + super.error.apply(this, [message, stack, context]); + + const hasStack = !!context; + this.winstonLogger.log(LogLevel.ERROR, { + context: hasStack ? context : stack, + message: hasStack ? new Error(message) : message, + }); + } +} diff --git a/apps/api/src/modules/shared/mailer/mailer.service.ts b/apps/api/src/shared/mailer/mailer.service.ts similarity index 85% rename from apps/api/src/modules/shared/mailer/mailer.service.ts rename to apps/api/src/shared/mailer/mailer.service.ts index 57ed755..430b8aa 100644 --- a/apps/api/src/modules/shared/mailer/mailer.service.ts +++ b/apps/api/src/shared/mailer/mailer.service.ts @@ -7,9 +7,9 @@ import dayjs from 'dayjs'; import Redis from 'ioredis'; +import { BusinessException } from '@/common/exceptions/biz.exception'; import { IAppConfig } from '@/config'; import { ErrorEnum } from '@/constants/error-code.constant'; -import { ApiException } from '@/exceptions/api.exception'; import { randomValue } from '@/utils'; @@ -58,7 +58,7 @@ export class MailerService { ); } catch (error) { console.log(error); - throw new ApiException(ErrorEnum.VERIFICATION_CODE_SEND_FAILED); + throw new BusinessException(ErrorEnum.VERIFICATION_CODE_SEND_FAILED); } return { @@ -70,7 +70,7 @@ export class MailerService { async checkCode(to, code) { const ret = await this.redis.get(`captcha:${to}`); if (ret !== code) { - throw new ApiException(ErrorEnum.INVALID_VERIFICATION_CODE); + throw new BusinessException(ErrorEnum.INVALID_VERIFICATION_CODE); } await this.redis.del(`captcha:${to}`); } @@ -80,11 +80,11 @@ export class MailerService { // ip限制 const ipLimit = await this.redis.get(`ip:${ip}:send:limit`); - if (ipLimit) throw new ApiException(ErrorEnum.TOO_MANY_REQUESTS); + if (ipLimit) throw new BusinessException(ErrorEnum.TOO_MANY_REQUESTS); // 1分钟最多接收1条 const limit = await this.redis.get(`captcha:${to}:limit`); - if (limit) throw new ApiException(ErrorEnum.TOO_MANY_REQUESTS); + if (limit) throw new BusinessException(ErrorEnum.TOO_MANY_REQUESTS); // 1天一个邮箱最多接收5条 let limitCountOfDay: string | number = await this.redis.get( @@ -92,7 +92,9 @@ export class MailerService { ); limitCountOfDay = limitCountOfDay ? Number(limitCountOfDay) : 0; if (limitCountOfDay > LIMIT_TIME) - throw new ApiException(ErrorEnum.MAXIMUM_FIVE_VERIFICATION_CODES_PER_DAY); + throw new BusinessException( + ErrorEnum.MAXIMUM_FIVE_VERIFICATION_CODES_PER_DAY, + ); // 1天一个ip最多发送5条 let ipLimitCountOfDay: string | number = await this.redis.get( @@ -100,7 +102,9 @@ export class MailerService { ); ipLimitCountOfDay = ipLimitCountOfDay ? Number(ipLimitCountOfDay) : 0; if (ipLimitCountOfDay > LIMIT_TIME) - throw new ApiException(ErrorEnum.MAXIMUM_FIVE_VERIFICATION_CODES_PER_DAY); + throw new BusinessException( + ErrorEnum.MAXIMUM_FIVE_VERIFICATION_CODES_PER_DAY, + ); } async log(to: string, code: string, ip: string) { diff --git a/apps/api/src/modules/shared/qq/qq.service.ts b/apps/api/src/shared/qq/qq.service.ts similarity index 100% rename from apps/api/src/modules/shared/qq/qq.service.ts rename to apps/api/src/shared/qq/qq.service.ts diff --git a/apps/api/src/modules/shared/shared.module.ts b/apps/api/src/shared/shared.module.ts similarity index 85% rename from apps/api/src/modules/shared/shared.module.ts rename to apps/api/src/shared/shared.module.ts index a1acf73..0f6c982 100644 --- a/apps/api/src/modules/shared/shared.module.ts +++ b/apps/api/src/shared/shared.module.ts @@ -9,22 +9,22 @@ import { MailerModule } from '@nestjs-modules/mailer'; import { IMailerConfig, IRedisConfig } from '@/config'; import { IpService } from './ip/ip.service'; +import { LoggerModule } from './logger/logger.module'; import { MailerService } from './mailer/mailer.service'; import { QQService } from './qq/qq.service'; -import { AppLoggerService } from './services/app-logger.service'; -const providers = [AppLoggerService, MailerService, IpService, QQService]; +const providers = [MailerService, IpService, QQService]; @Global() @Module({ imports: [ + // logger + LoggerModule, // http HttpModule, // redis cache CacheModule.register({ isGlobal: true, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore // store: redisStore, // host: 'localhost', @@ -33,8 +33,8 @@ const providers = [AppLoggerService, MailerService, IpService, QQService]; // rate limit ThrottlerModule.forRoot([ { + limit: 3, ttl: 60000, - limit: 5, }, ]), // redis diff --git a/package.json b/package.json index b83793c..de7c4c1 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "reinstall": "rimraf pnpm-lock.yaml && rimraf package.lock.json && rimraf node_modules && npm run bootstrap" }, "devDependencies": { - "@kuizuo/eslint-config-ts": "^1.0.1", "cross-env": "7.0.3", "prettier": "^3.0.3", "prettier-plugin-packagejson": "^2.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4684842..af428bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: devDependencies: - '@kuizuo/eslint-config-ts': - specifier: ^1.0.1 - version: 1.0.1(eslint@8.53.0)(typescript@5.2.2) cross-env: specifier: 7.0.3 version: 7.0.3 @@ -249,6 +246,9 @@ importers: apps/api: dependencies: + '@fastify/cookie': + specifier: ^9.1.0 + version: 9.1.0 '@fastify/multipart': specifier: ^8.0.0 version: 8.0.0 @@ -360,9 +360,6 @@ importers: lodash: specifier: ^4.17.21 version: 4.17.21 - log4js: - specifier: ^6.9.1 - version: 6.9.1 mysql: specifier: ^2.18.1 version: 2.18.1 @@ -408,6 +405,12 @@ importers: ua-parser-js: specifier: ^1.0.37 version: 1.0.37 + winston: + specifier: ^3.11.0 + version: 3.11.0 + winston-daily-rotate-file: + specifier: ^4.7.1 + version: 4.7.1(winston@3.11.0) devDependencies: '@compodoc/compodoc': specifier: ^1.1.22 @@ -2137,6 +2140,11 @@ packages: dev: true optional: true + /@colors/colors@1.6.0: + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + dev: false + /@commitlint/cli@18.2.0(typescript@5.2.2): resolution: {integrity: sha512-F/DCG791kMFmWg5eIdogakuGeg4OiI2kD430ed1a1Hh3epvrJdeIAgcGADAMIOmF+m0S1+VlIYUKG2dvQQ1Izw==} engines: {node: '>=v18'} @@ -2434,6 +2442,14 @@ packages: resolution: {integrity: sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==} engines: {node: '>=10'} + /@dabh/diagnostics@2.0.3: + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + dev: false + /@emotion/hash@0.9.1: resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} @@ -2903,6 +2919,13 @@ packages: text-decoding: 1.0.0 dev: false + /@fastify/cookie@9.1.0: + resolution: {integrity: sha512-w/LlQjj7cmYlQNhEKNm4jQoLkFXCL73kFu1Jy3aL7IFbYEojEKur0f7ieCKUxBBaU65tpaWC83UM8xW7AzY6uw==} + dependencies: + cookie: 0.5.0 + fastify-plugin: 4.5.0 + dev: false + /@fastify/cors@8.4.0: resolution: {integrity: sha512-MlVvMTenltToByTpLwlWtO+7dQ3l2J+1OpmGrx9JpSNWo1d+dhfNCOi23zHhxdFhtpDzfwGwCsKu9DTeG7k7nQ==} dependencies: @@ -3617,49 +3640,6 @@ packages: resolution: {integrity: sha512-CGZ9HGmHGTcGnU8CDQm7RR7hZgxLyRHTIFpS1FDCQkVaipdL/poq72ibpKqqQflomgKRCYV6GReP7ZXEZeDx1w==} dev: true - /@kuizuo/eslint-config-basic@1.0.1(@typescript-eslint/parser@5.57.1)(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-fFzBKBQtkew18JKhnSCVtwIv0oOI7D3pOgapo71MiZ9VUzSReVD7OkhTaUxRx3n/Iv4oPVO//lXAUEk6xy4aCA==} - peerDependencies: - eslint: '>=7.4.0' - dependencies: - eslint: 8.53.0 - eslint-plugin-eslint-comments: 3.2.0(eslint@8.53.0) - eslint-plugin-html: 7.1.0 - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@5.57.1)(eslint@8.53.0) - eslint-plugin-jsonc: 2.9.0(eslint@8.53.0) - eslint-plugin-kuizuo: 1.0.1(eslint@8.53.0)(typescript@5.2.2) - eslint-plugin-markdown: 3.0.0(eslint@8.53.0) - eslint-plugin-n: 15.7.0(eslint@8.53.0) - eslint-plugin-promise: 6.1.1(eslint@8.53.0) - eslint-plugin-unicorn: 43.0.2(eslint@8.53.0) - eslint-plugin-yml: 1.8.0(eslint@8.53.0) - jsonc-eslint-parser: 2.3.0 - yaml-eslint-parser: 1.2.2 - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - typescript - dev: true - - /@kuizuo/eslint-config-ts@1.0.1(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-2hROWJ2k7wBlRJrXQSSYKjFbLACMkE7lFRJGMY9MOHt4sNJECQnRhz8WboS4JQrlW5pbdS8hA0qnvyoda5hpYg==} - peerDependencies: - eslint: '>=7.4.0' - typescript: '>=3.9' - dependencies: - '@kuizuo/eslint-config-basic': 1.0.1(@typescript-eslint/parser@5.57.1)(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/eslint-plugin': 5.57.1(@typescript-eslint/parser@5.57.1)(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/parser': 5.57.1(eslint@8.53.0)(typescript@5.2.2) - eslint: 8.53.0 - typescript: 5.2.2 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - dev: true - /@liaoliaots/nestjs-redis@9.0.5(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(ioredis@5.3.2): resolution: {integrity: sha512-nPcGLj0zW4mEsYtQYfWx3o7PmrMjuzFk6+t/g2IRopAeWWUZZ/5nIJ4KTKiz/3DJEUkbX8PZqB+dOhklGF0SVA==} engines: {node: '>=12.22.0'} @@ -4746,12 +4726,6 @@ packages: resolution: {integrity: sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg==} dev: false - /@types/mdast@3.0.12: - resolution: {integrity: sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==} - dependencies: - '@types/unist': 2.0.7 - dev: true - /@types/mime@1.3.2: resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} dev: true @@ -4883,14 +4857,14 @@ packages: '@types/estree': 1.0.1 dev: true + /@types/triple-beam@1.3.5: + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + dev: false + /@types/ua-parser-js@0.7.39: resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} dev: true - /@types/unist@2.0.7: - resolution: {integrity: sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==} - dev: true - /@types/validator@13.7.17: resolution: {integrity: sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==} @@ -4917,34 +4891,6 @@ packages: '@types/yargs-parser': 21.0.0 dev: true - /@typescript-eslint/eslint-plugin@5.57.1(@typescript-eslint/parser@5.57.1)(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-1MeobQkQ9tztuleT3v72XmY0XuKXVXusAhryoLuU5YZ+mXoYKZP9SQ7Flulh1NX4DTjpGTc2b/eMu4u7M7dhnQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 5.57.1(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/scope-manager': 5.57.1 - '@typescript-eslint/type-utils': 5.57.1(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/utils': 5.57.1(eslint@8.53.0)(typescript@5.2.2) - debug: 4.3.4 - eslint: 8.53.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.4 - natural-compare-lite: 1.4.0 - semver: 7.5.4 - tsutils: 3.21.0(typescript@5.2.2) - typescript: 5.2.2 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -4974,26 +4920,6 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@5.57.1(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-hlA0BLeVSA/wBPKdPGxoVr9Pp6GutGoY380FEhbVi0Ph4WNe8kLvqIRx76RSQt1lynZKfrXKs0/XeEk4zZycuA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/scope-manager': 5.57.1 - '@typescript-eslint/types': 5.57.1 - '@typescript-eslint/typescript-estree': 5.57.1(typescript@5.2.2) - debug: 4.3.4 - eslint: 8.53.0 - typescript: 5.2.2 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5015,22 +4941,6 @@ packages: - supports-color dev: true - /@typescript-eslint/scope-manager@5.57.1: - resolution: {integrity: sha512-N/RrBwEUKMIYxSKl0oDK5sFVHd6VI7p9K5MyUlVYAY6dyNb/wHUqndkTd3XhpGlXgnQsBkRZuu4f9kAHghvgPw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.57.1 - '@typescript-eslint/visitor-keys': 5.57.1 - dev: true - - /@typescript-eslint/scope-manager@5.62.0: - resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 - dev: true - /@typescript-eslint/scope-manager@6.10.0: resolution: {integrity: sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5039,26 +4949,6 @@ packages: '@typescript-eslint/visitor-keys': 6.10.0 dev: true - /@typescript-eslint/type-utils@5.57.1(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-/RIPQyx60Pt6ga86hKXesXkJ2WOS4UemFrmmq/7eOyiYjYv/MUSHPlkhU6k9T9W1ytnTJueqASW+wOmW4KrViw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 5.57.1(typescript@5.2.2) - '@typescript-eslint/utils': 5.57.1(eslint@8.53.0)(typescript@5.2.2) - debug: 4.3.4 - eslint: 8.53.0 - tsutils: 3.21.0(typescript@5.2.2) - typescript: 5.2.2 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/type-utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5079,63 +4969,11 @@ packages: - supports-color dev: true - /@typescript-eslint/types@5.57.1: - resolution: {integrity: sha512-bSs4LOgyV3bJ08F5RDqO2KXqg3WAdwHCu06zOqcQ6vqbTJizyBhuh1o1ImC69X4bV2g1OJxbH71PJqiO7Y1RuA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /@typescript-eslint/types@5.62.0: - resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /@typescript-eslint/types@6.10.0: resolution: {integrity: sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@5.57.1(typescript@5.2.2): - resolution: {integrity: sha512-A2MZqD8gNT0qHKbk2wRspg7cHbCDCk2tcqt6ScCFLr5Ru8cn+TCfM786DjPhqwseiS+PrYwcXht5ztpEQ6TFTw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 5.57.1 - '@typescript-eslint/visitor-keys': 5.57.1 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - tsutils: 3.21.0(typescript@5.2.2) - typescript: 5.2.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/typescript-estree@5.62.0(typescript@5.2.2): - resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - tsutils: 3.21.0(typescript@5.2.2) - typescript: 5.2.2 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/typescript-estree@6.10.0(typescript@5.2.2): resolution: {integrity: sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5157,46 +4995,6 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@5.57.1(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-kN6vzzf9NkEtawECqze6v99LtmDiUJCVpvieTFA1uL7/jDghiJGubGZ5csicYHU1Xoqb3oH/R5cN5df6W41Nfg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 5.57.1 - '@typescript-eslint/types': 5.57.1 - '@typescript-eslint/typescript-estree': 5.57.1(typescript@5.2.2) - eslint: 8.53.0 - eslint-scope: 5.1.1 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@typescript-eslint/utils@5.62.0(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2) - eslint: 8.53.0 - eslint-scope: 5.1.1 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /@typescript-eslint/utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5216,22 +5014,6 @@ packages: - typescript dev: true - /@typescript-eslint/visitor-keys@5.57.1: - resolution: {integrity: sha512-RjQrAniDU0CEk5r7iphkm731zKlFiUjvcBS2yHAg8WWqFMCaCrD0rKEVOMUyMMcbGPZ0bPp56srkGWrgfZqLRA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.57.1 - eslint-visitor-keys: 3.4.3 - dev: true - - /@typescript-eslint/visitor-keys@5.62.0: - resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.62.0 - eslint-visitor-keys: 3.4.3 - dev: true - /@typescript-eslint/visitor-keys@6.10.0: resolution: {integrity: sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -6789,12 +6571,6 @@ packages: engines: {node: '>=6'} dev: true - /builtins@5.0.1: - resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} - dependencies: - semver: 7.5.4 - dev: true - /bull@4.11.4: resolution: {integrity: sha512-6rPnFkUbN/eWhzGF65mcYM2HWDl2rp0fTidZ8en64Zwplioe/QxpdiWfLLtXX4Yy25piPly4f96wHR0NquiyyQ==} engines: {node: '>=12'} @@ -6985,14 +6761,6 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} - /character-entities-legacy@1.1.4: - resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} - dev: true - - /character-entities@1.2.4: - resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} - dev: true - /character-parser@2.2.0: resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} requiresBuild: true @@ -7000,10 +6768,6 @@ packages: is-regex: 1.1.4 dev: false - /character-reference-invalid@1.1.4: - resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} - dev: true - /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true @@ -7105,13 +6869,6 @@ packages: source-map: 0.6.1 dev: true - /clean-regexp@1.0.0: - resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} - engines: {node: '>=4'} - dependencies: - escape-string-regexp: 1.0.5 - dev: true - /cli-boxes@2.2.1: resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} engines: {node: '>=6'} @@ -7240,11 +6997,25 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: false + /color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true dev: true + /color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + dev: false + /colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} dev: true @@ -7263,6 +7034,13 @@ packages: engines: {node: '>=0.1.90'} dev: true + /colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + dev: false + /combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -7783,11 +7561,6 @@ packages: '@babel/runtime': 7.22.6 dev: false - /date-format@4.0.14: - resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==} - engines: {node: '>=4.0'} - dev: false - /dayjs@1.11.10: resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} @@ -8320,6 +8093,10 @@ packages: engines: {node: '>= 4'} dev: true + /enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + dev: false + /encode-utf8@1.0.3: resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} dev: false @@ -8979,35 +8756,6 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.57.1)(eslint-import-resolver-node@0.3.9)(eslint@8.53.0): - resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - dependencies: - '@typescript-eslint/parser': 5.57.1(eslint@8.53.0)(typescript@5.2.2) - debug: 3.2.7 - eslint: 8.53.0 - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint@8.53.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} @@ -9037,69 +8785,6 @@ packages: - supports-color dev: true - /eslint-plugin-es@4.1.0(eslint@8.53.0): - resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=4.19.1' - dependencies: - eslint: 8.53.0 - eslint-utils: 2.1.0 - regexpp: 3.2.0 - dev: true - - /eslint-plugin-eslint-comments@3.2.0(eslint@8.53.0): - resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} - engines: {node: '>=6.5.0'} - peerDependencies: - eslint: '>=4.19.1' - dependencies: - escape-string-regexp: 1.0.5 - eslint: 8.53.0 - ignore: 5.2.4 - dev: true - - /eslint-plugin-html@7.1.0: - resolution: {integrity: sha512-fNLRraV/e6j8e3XYOC9xgND4j+U7b1Rq+OygMlLcMg+wI/IpVbF+ubQa3R78EjKB9njT6TQOlcK5rFKBVVtdfg==} - dependencies: - htmlparser2: 8.0.2 - dev: true - - /eslint-plugin-import@2.29.0(@typescript-eslint/parser@5.57.1)(eslint@8.53.0): - resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - dependencies: - '@typescript-eslint/parser': 5.57.1(eslint@8.53.0)(typescript@5.2.2) - array-includes: 3.1.7 - array.prototype.findlastindex: 1.2.3 - array.prototype.flat: 1.3.2 - array.prototype.flatmap: 1.3.2 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.53.0 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.57.1)(eslint-import-resolver-node@0.3.9)(eslint@8.53.0) - hasown: 2.0.0 - is-core-module: 2.13.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.7 - object.groupby: 1.0.1 - object.values: 1.1.7 - semver: 6.3.1 - tsconfig-paths: 3.14.2 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - dev: true - /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0): resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} engines: {node: '>=4'} @@ -9135,18 +8820,6 @@ packages: - supports-color dev: true - /eslint-plugin-jsonc@2.9.0(eslint@8.53.0): - resolution: {integrity: sha512-RK+LeONVukbLwT2+t7/OY54NJRccTXh/QbnXzPuTLpFMVZhPuq1C9E07+qWenGx7rrQl0kAalAWl7EmB+RjpGA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '>=6.0.0' - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) - eslint: 8.53.0 - jsonc-eslint-parser: 2.3.0 - natural-compare: 1.4.0 - dev: true - /eslint-plugin-jsx-a11y@6.7.1(eslint@8.53.0): resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} engines: {node: '>=4.0'} @@ -9172,45 +8845,6 @@ packages: semver: 6.3.1 dev: true - /eslint-plugin-kuizuo@1.0.1(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-YPX2tUpDn47mfpu+dRyOBi7kkn9zsQGWzGgKKmSfmLMBSht7tote0mtsvmbrPYNxHDaqDoxr2Y4DwtUV16Z/+Q==} - dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@8.53.0)(typescript@5.2.2) - transitivePeerDependencies: - - eslint - - supports-color - - typescript - dev: true - - /eslint-plugin-markdown@3.0.0(eslint@8.53.0): - resolution: {integrity: sha512-hRs5RUJGbeHDLfS7ELanT0e29Ocyssf/7kBM+p7KluY5AwngGkDf8Oyu4658/NZSGTTq05FZeWbkxXtbVyHPwg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - eslint: 8.53.0 - mdast-util-from-markdown: 0.8.5 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint-plugin-n@15.7.0(eslint@8.53.0): - resolution: {integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==} - engines: {node: '>=12.22.0'} - peerDependencies: - eslint: '>=7.0.0' - dependencies: - builtins: 5.0.1 - eslint: 8.53.0 - eslint-plugin-es: 4.1.0(eslint@8.53.0) - eslint-utils: 3.0.0(eslint@8.53.0) - ignore: 5.2.4 - is-core-module: 2.13.1 - minimatch: 3.1.2 - resolve: 1.22.2 - semver: 7.5.4 - dev: true - /eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.0.3): resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -9232,15 +8866,6 @@ packages: synckit: 0.8.5 dev: true - /eslint-plugin-promise@6.1.1(eslint@8.53.0): - resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - dependencies: - eslint: 8.53.0 - dev: true - /eslint-plugin-react-hooks@4.6.0(eslint@8.53.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} @@ -9282,29 +8907,6 @@ packages: eslint: 8.53.0 dev: true - /eslint-plugin-unicorn@43.0.2(eslint@8.53.0): - resolution: {integrity: sha512-DtqZ5mf/GMlfWoz1abIjq5jZfaFuHzGBZYIeuJfEoKKGWRHr2JiJR+ea+BF7Wx2N1PPRoT/2fwgiK1NnmNE3Hg==} - engines: {node: '>=14.18'} - peerDependencies: - eslint: '>=8.18.0' - dependencies: - '@babel/helper-validator-identifier': 7.22.5 - ci-info: 3.8.0 - clean-regexp: 1.0.0 - eslint: 8.53.0 - eslint-utils: 3.0.0(eslint@8.53.0) - esquery: 1.5.0 - indent-string: 4.0.0 - is-builtin-module: 3.2.1 - lodash: 4.17.21 - pluralize: 8.0.0 - read-pkg-up: 7.0.1 - regexp-tree: 0.1.27 - safe-regex: 2.1.1 - semver: 7.5.4 - strip-indent: 3.0.0 - dev: true - /eslint-plugin-unused-imports@3.0.0(@typescript-eslint/eslint-plugin@6.10.0)(eslint@8.53.0): resolution: {integrity: sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -9338,21 +8940,6 @@ packages: - supports-color dev: true - /eslint-plugin-yml@1.8.0(eslint@8.53.0): - resolution: {integrity: sha512-fgBiJvXD0P2IN7SARDJ2J7mx8t0bLdG6Zcig4ufOqW5hOvSiFxeUyc2g5I1uIm8AExbo26NNYCcTGZT0MXTsyg==} - engines: {node: ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '>=6.0.0' - dependencies: - debug: 4.3.4 - eslint: 8.53.0 - lodash: 4.17.21 - natural-compare: 1.4.0 - yaml-eslint-parser: 1.2.2 - transitivePeerDependencies: - - supports-color - dev: true - /eslint-rule-composer@0.3.0: resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} engines: {node: '>=4.0.0'} @@ -9374,33 +8961,6 @@ packages: estraverse: 5.3.0 dev: true - /eslint-utils@2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} - dependencies: - eslint-visitor-keys: 1.3.0 - dev: true - - /eslint-utils@3.0.0(eslint@8.53.0): - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - dependencies: - eslint: 8.53.0 - eslint-visitor-keys: 2.1.0 - dev: true - - /eslint-visitor-keys@1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - dev: true - - /eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - dev: true - /eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -9882,6 +9442,10 @@ packages: dependencies: bser: 2.1.1 + /fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + dev: false + /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -9903,6 +9467,12 @@ packages: flat-cache: 3.1.1 dev: true + /file-stream-rotator@0.6.1: + resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} + dependencies: + moment: 2.29.4 + dev: false + /file-uri-to-path@2.0.0: resolution: {integrity: sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==} engines: {node: '>= 6'} @@ -10003,11 +9573,16 @@ packages: /flatted@3.2.7: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + dev: true /flatted@3.2.9: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true + /fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + dev: false + /follow-redirects@1.15.2(debug@4.3.4): resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} @@ -10462,10 +10037,6 @@ packages: /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - /grapheme-splitter@1.0.4: - resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - dev: true - /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true @@ -11015,17 +10586,6 @@ packages: kind-of: 6.0.3 dev: true - /is-alphabetical@1.0.4: - resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} - dev: true - - /is-alphanumerical@1.0.4: - resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} - dependencies: - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - dev: true - /is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} @@ -11045,6 +10605,10 @@ packages: /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: false + /is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} dependencies: @@ -11111,10 +10675,6 @@ packages: has-tostringtag: 1.0.0 dev: true - /is-decimal@1.0.4: - resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} - dev: true - /is-descriptor@0.1.6: resolution: {integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==} engines: {node: '>=0.10.0'} @@ -11182,10 +10742,6 @@ packages: dependencies: is-extglob: 2.1.1 - /is-hexadecimal@1.0.4: - resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} - dev: true - /is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -12565,16 +12121,6 @@ packages: engines: {node: '>=6'} hasBin: true - /jsonc-eslint-parser@2.3.0: - resolution: {integrity: sha512-9xZPKVYp9DxnM3sd1yAsh/d59iIaswDkai8oTxbursfKYbg/ibjX0IzFt35+VZ8iEW453TVTXztnRvYUQlAfUQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.10.0 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - semver: 7.5.4 - dev: true - /jsonc-parser@3.1.0: resolution: {integrity: sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==} dev: true @@ -12725,6 +12271,10 @@ packages: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} dev: true + /kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + dev: false + /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} dev: true @@ -13018,17 +12568,16 @@ packages: is-unicode-supported: 0.1.0 dev: true - /log4js@6.9.1: - resolution: {integrity: sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==} - engines: {node: '>=8.0'} + /logform@2.6.0: + resolution: {integrity: sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==} + engines: {node: '>= 12.0.0'} dependencies: - date-format: 4.0.14 - debug: 4.3.4 - flatted: 3.2.7 - rfdc: 1.3.0 - streamroller: 3.1.5 - transitivePeerDependencies: - - supports-color + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.4.3 + triple-beam: 1.4.1 dev: false /loglevel-plugin-prefix@0.8.4: @@ -13231,22 +12780,6 @@ packages: is-buffer: 1.1.6 dev: false - /mdast-util-from-markdown@0.8.5: - resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} - dependencies: - '@types/mdast': 3.0.12 - mdast-util-to-string: 2.0.0 - micromark: 2.11.4 - parse-entities: 2.0.0 - unist-util-stringify-position: 2.0.3 - transitivePeerDependencies: - - supports-color - dev: true - - /mdast-util-to-string@2.0.0: - resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} - dev: true - /mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} dev: true @@ -13342,15 +12875,6 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} - /micromark@2.11.4: - resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} - dependencies: - debug: 4.3.4 - parse-entities: 2.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /micromatch@3.1.0: resolution: {integrity: sha512-3StSelAE+hnRvMs8IdVW7Uhk8CVed5tp+kLLGlBP6WiRAXS21GPGu/Nat4WNPXj2Eoc24B02SaeoyozPMfj0/g==} engines: {node: '>=0.10.0'} @@ -13921,6 +13445,10 @@ packages: dependencies: commander: 11.0.0 + /moment@2.29.4: + resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} + dev: false + /morgan@1.10.0: resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} engines: {node: '>= 0.8.0'} @@ -14033,10 +13561,6 @@ packages: /nanopop@2.3.0: resolution: {integrity: sha512-fzN+T2K7/Ah25XU02MJkPZ5q4Tj5FpjmIYq4rvoHX4yb16HzFdCO6JxFFn5Y/oBhQ8no8fUZavnyIv9/+xkBBw==} - /natural-compare-lite@1.4.0: - resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} - dev: true - /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -14216,6 +13740,11 @@ packages: kind-of: 3.2.2 dev: true + /object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + dev: false + /object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} @@ -14346,6 +13875,12 @@ packages: dependencies: wrappy: 1.0.2 + /one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + dependencies: + fn.name: 1.1.0 + dev: false + /onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -14571,17 +14106,6 @@ packages: callsites: 3.1.0 dev: true - /parse-entities@2.0.0: - resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} - dependencies: - character-entities: 1.2.4 - character-entities-legacy: 1.1.4 - character-reference-invalid: 1.1.4 - is-alphanumerical: 1.0.4 - is-decimal: 1.0.4 - is-hexadecimal: 1.0.4 - dev: true - /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -15558,11 +15082,6 @@ packages: safe-regex: 1.1.0 dev: true - /regexp-tree@0.1.27: - resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} - hasBin: true - dev: true - /regexp.prototype.flags@1.5.0: resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} engines: {node: '>= 0.4'} @@ -15581,11 +15100,6 @@ packages: set-function-name: 2.0.1 dev: true - /regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - dev: true - /regexpu-core@5.3.2: resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} engines: {node: '>=4'} @@ -15905,12 +15419,6 @@ packages: ret: 0.1.15 dev: true - /safe-regex@2.1.1: - resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} - dependencies: - regexp-tree: 0.1.27 - dev: true - /safe-stable-stringify@2.4.3: resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} engines: {node: '>=10'} @@ -16162,6 +15670,12 @@ packages: resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==} engines: {node: '>=14'} + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: false + /sirv@2.0.3: resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} engines: {node: '>= 10'} @@ -16471,6 +15985,10 @@ packages: stackframe: 1.3.4 dev: false + /stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + dev: false + /stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -16554,17 +16072,6 @@ packages: engines: {node: '>=4.0.0'} dev: false - /streamroller@3.1.5: - resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==} - engines: {node: '>=8.0'} - dependencies: - date-format: 4.0.14 - debug: 4.3.4 - fs-extra: 8.1.0 - transitivePeerDependencies: - - supports-color - dev: false - /strict-uri-encode@1.1.0: resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==} engines: {node: '>=0.10.0'} @@ -17191,6 +16698,10 @@ packages: engines: {node: '>=8'} dev: true + /text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + dev: false + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -17376,6 +16887,11 @@ packages: engines: {node: '>=12'} dev: true + /triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + dev: false + /ts-api-utils@1.0.1(typescript@5.2.2): resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==} engines: {node: '>=16.13.0'} @@ -17517,16 +17033,6 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - /tsutils@3.21.0(typescript@5.2.2): - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - dependencies: - tslib: 1.14.1 - typescript: 5.2.2 - dev: true - /turbo-darwin-64@1.10.16: resolution: {integrity: sha512-+Jk91FNcp9e9NCLYlvDDlp2HwEDp14F9N42IoW3dmHI5ZkGSXzalbhVcrx3DOox3QfiNUHxzWg4d7CnVNCuuMg==} cpu: [x64] @@ -17919,12 +17425,6 @@ packages: set-value: 2.0.1 dev: true - /unist-util-stringify-position@2.0.3: - resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} - dependencies: - '@types/unist': 2.0.7 - dev: true - /universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -18872,6 +18372,45 @@ packages: execa: 4.1.0 dev: true + /winston-daily-rotate-file@4.7.1(winston@3.11.0): + resolution: {integrity: sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA==} + engines: {node: '>=8'} + peerDependencies: + winston: ^3 + dependencies: + file-stream-rotator: 0.6.1 + object-hash: 2.2.0 + triple-beam: 1.4.1 + winston: 3.11.0 + winston-transport: 4.6.0 + dev: false + + /winston-transport@4.6.0: + resolution: {integrity: sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==} + engines: {node: '>= 12.0.0'} + dependencies: + logform: 2.6.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + dev: false + + /winston@3.11.0: + resolution: {integrity: sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.4 + is-stream: 2.0.1 + logform: 2.6.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.4.3 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.6.0 + dev: false + /with@7.0.2: resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} engines: {node: '>= 10.0.0'} @@ -19057,18 +18596,10 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - /yaml-eslint-parser@1.2.2: - resolution: {integrity: sha512-pEwzfsKbTrB8G3xc/sN7aw1v6A6c/pKxLAkjclnAyo5g5qOh6eL9WGu0o3cSDQZKrTNk4KL4lQSwZW+nBkANEg==} - engines: {node: ^14.17.0 || >=16.0.0} - dependencies: - eslint-visitor-keys: 3.4.3 - lodash: 4.17.21 - yaml: 2.3.1 - dev: true - /yaml@2.3.1: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} engines: {node: '>= 14'} + dev: false /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}