diff --git a/jest.config.js b/jest.config.js index a6c6b427..b5a9306d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,6 +17,7 @@ module.exports = { globals: { 'ts-jest': { useESM: true, + tsConfig: './test/tsconfig.json', }, isDev: process.env.NODE_ENV === 'development', }, diff --git a/src/app.module.ts b/src/app.module.ts index 3a9608e4..c82d22e3 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,5 +1,6 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common' +import { MiddlewareConsumer, Module, NestModule, Type } from '@nestjs/common' import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core' + import { AppController } from './app.controller' import { AllExceptionsFilter } from './common/filters/any-exception.filter' import { RolesGuard } from './common/guard/roles.guard' @@ -82,8 +83,8 @@ import { LoggerModule } from './processors/logger/logger.module' GatewayModule, HelperModule, - isDev ? DebugModule : null, - ].filter(Boolean), + isDev ? DebugModule : undefined, + ].filter(Boolean) as Type[], controllers: [AppController], providers: [ { diff --git a/src/bootstrap.ts b/src/bootstrap.ts index aaf27ee1..c7c3d7cb 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -1,8 +1,10 @@ import cluster from 'cluster' import { performance } from 'perf_hooks' + import { Logger, RequestMethod, ValidationPipe } from '@nestjs/common' import { NestFactory } from '@nestjs/core' import { NestFastifyApplication } from '@nestjs/platform-fastify' + import { API_VERSION, CROSS_DOMAIN, PORT } from './app.config' import { AppModule } from './app.module' import { fastifyApp } from './common/adapters/fastify.adapter' @@ -11,6 +13,7 @@ import { SpiderGuard } from './common/guard/spider.guard' import { LoggingInterceptor } from './common/interceptors/logging.interceptor' import { isTest } from './global/env.global' import { MyLogger } from './processors/logger/logger.service' + const Origin = Array.isArray(CROSS_DOMAIN.allowedOrigins) ? CROSS_DOMAIN.allowedOrigins : false @@ -28,7 +31,7 @@ export async function bootstrap() { // Origin 如果不是数组就全部允许跨域 app.enableCors( - Origin + hosts ? { origin: (origin, callback) => { const allow = hosts.some((host) => host.test(origin)) diff --git a/src/cluster.ts b/src/cluster.ts index 597b03b2..dcb40bee 100644 --- a/src/cluster.ts +++ b/src/cluster.ts @@ -12,7 +12,7 @@ export class Cluster { process.on('SIGINT', () => { consola.info('Cluster shutting down...') for (const id in cluster.workers) { - cluster.workers[id].kill() + cluster.workers[id]?.kill() } // exit the master process process.exit(0) @@ -26,9 +26,10 @@ export class Cluster { cluster.on('fork', (worker) => { worker.on('message', (msg) => { - Object.keys(cluster.workers).forEach((id) => { - cluster.workers[id].send(msg) - }) + cluster.workers && + Object.keys(cluster.workers).forEach((id) => { + cluster.workers?.[id]?.send(msg) + }) }) }) diff --git a/src/common/adapters/fastify.adapter.ts b/src/common/adapters/fastify.adapter.ts index 309379c2..64289491 100644 --- a/src/common/adapters/fastify.adapter.ts +++ b/src/common/adapters/fastify.adapter.ts @@ -1,9 +1,12 @@ -import { Logger } from '@nestjs/common' -import { FastifyAdapter } from '@nestjs/platform-fastify' import type { FastifyRequest } from 'fastify' import fastifyCookie from 'fastify-cookie' import FastifyMultipart from 'fastify-multipart' + +import { Logger } from '@nestjs/common' +import { FastifyAdapter } from '@nestjs/platform-fastify' + import { getIp } from '~/utils' + const app: FastifyAdapter = new FastifyAdapter({ trustProxy: true, }) @@ -35,7 +38,7 @@ app.getInstance().addHook('onRequest', (request, reply, done) => { return reply.code(418).send() } else if (url.match(/\/(adminer|admin|wp-login|phpMyAdmin|\.env)$/gi)) { - const isMxSpaceClient = ua.match('mx-space') + const isMxSpaceClient = ua?.match('mx-space') reply.raw.statusMessage = 'Hey, What the fuck are you doing!' reply.raw.statusCode = isMxSpaceClient ? 666 : 200 logWarn( diff --git a/src/common/decorator/auth.decorator.ts b/src/common/decorator/auth.decorator.ts index c8d06f8e..a9474afb 100644 --- a/src/common/decorator/auth.decorator.ts +++ b/src/common/decorator/auth.decorator.ts @@ -1,10 +1,13 @@ import { UseGuards, applyDecorators } from '@nestjs/common' import { ApiBearerAuth, ApiUnauthorizedResponse } from '@nestjs/swagger' -import { JWTAuthGuard } from '../guard/auth.guard' + import { SECURITY } from '~/app.config' +import { JWTAuthGuard } from '../guard/auth.guard' + export function Auth() { - const decorators = [] + const decorators: (ClassDecorator | PropertyDecorator | MethodDecorator)[] = + [] if (!SECURITY.skipAuth) { decorators.push(UseGuards(JWTAuthGuard)) } diff --git a/src/common/exceptions/master-lost.exception.ts b/src/common/exceptions/master-lost.exception.ts new file mode 100644 index 00000000..8c509721 --- /dev/null +++ b/src/common/exceptions/master-lost.exception.ts @@ -0,0 +1,7 @@ +import { InternalServerErrorException } from '@nestjs/common' + +export class MasterLostException extends InternalServerErrorException { + constructor() { + super('系统异常,站点主人信息已丢失') + } +} diff --git a/src/common/filters/any-exception.filter.ts b/src/common/filters/any-exception.filter.ts index 949c5fea..a723b078 100644 --- a/src/common/filters/any-exception.filter.ts +++ b/src/common/filters/any-exception.filter.ts @@ -1,5 +1,7 @@ +import { FastifyReply, FastifyRequest } from 'fastify' import { WriteStream } from 'fs' import { resolve } from 'path' + import { ArgumentsHost, Catch, @@ -10,14 +12,15 @@ import { Logger, } from '@nestjs/common' import { Reflector } from '@nestjs/core' -import { FastifyReply, FastifyRequest } from 'fastify' -import { getIp } from '../../utils/ip.util' -import { LoggingInterceptor } from '../interceptors/logging.interceptor' + import { HTTP_REQUEST_TIME } from '~/constants/meta.constant' import { LOG_DIR } from '~/constants/path.constant' import { REFLECTOR } from '~/constants/system.constant' import { isDev } from '~/global/env.global' +import { getIp } from '../../utils/ip.util' +import { LoggingInterceptor } from '../interceptors/logging.interceptor' + type myError = { readonly status: number readonly statusCode?: number @@ -46,6 +49,9 @@ export class AllExceptionsFilter implements ExceptionFilter { (exception as any)?.response?.message || (exception as myError)?.message || '' + + const url = request.raw.url! + if (status === HttpStatus.INTERNAL_SERVER_ERROR) { // message && Logger.debug(message, undefined, 'Catch') Logger.error(exception, undefined, 'Catch') @@ -59,7 +65,7 @@ export class AllExceptionsFilter implements ExceptionFilter { }) this.errorLogPipe.write( - `[${new Date().toISOString()}] ${decodeURI(request.raw.url)}: ${ + `[${new Date().toISOString()}] ${decodeURI(url)}: ${ (exception as any)?.response?.message || (exception as myError)?.message }\n${(exception as Error).stack}\n`, @@ -68,9 +74,7 @@ export class AllExceptionsFilter implements ExceptionFilter { } else { const ip = getIp(request) this.logger.warn( - `IP: ${ip} 错误信息: (${status}) ${message} Path: ${decodeURI( - request.raw.url, - )}`, + `IP: ${ip} 错误信息: (${status}) ${message} Path: ${decodeURI(url)}`, ) } // @ts-ignore diff --git a/src/common/interceptors/analyze.interceptor.ts b/src/common/interceptors/analyze.interceptor.ts index a9287033..b4b0ac5a 100644 --- a/src/common/interceptors/analyze.interceptor.ts +++ b/src/common/interceptors/analyze.interceptor.ts @@ -4,7 +4,11 @@ * @module interceptor/analyze * @author Innei */ +import isbot from 'isbot' +import { Observable } from 'rxjs' +import UAParser from 'ua-parser-js' import { URL } from 'url' + import { CallHandler, ExecutionContext, @@ -12,16 +16,13 @@ import { NestInterceptor, } from '@nestjs/common' import { ReturnModelType } from '@typegoose/typegoose' -import isbot from 'isbot' -import { Observable } from 'rxjs' -import UAParser from 'ua-parser-js' -import { InjectModel } from '~/transformers/model.transformer' + import { RedisKeys } from '~/constants/cache.constant' import { AnalyzeModel } from '~/modules/analyze/analyze.model' import { OptionModel } from '~/modules/configs/configs.model' import { CacheService } from '~/processors/cache/cache.service' import { getNestExecutionContextRequest } from '~/transformers/get-req.transformer' - +import { InjectModel } from '~/transformers/model.transformer' import { getIp } from '~/utils/ip.util' import { getRedisKey } from '~/utils/redis.util' @@ -76,7 +77,8 @@ export class AnalyzeInterceptor implements NestInterceptor { process.nextTick(async () => { try { - this.parser.setUA(request.headers['user-agent']) + request.headers['user-agent'] && + this.parser.setUA(request.headers['user-agent']) const ua = this.parser.getResult() diff --git a/src/common/middlewares/attach-auth.middleware.ts b/src/common/middlewares/attach-auth.middleware.ts index 579ba39a..3d0008f6 100644 --- a/src/common/middlewares/attach-auth.middleware.ts +++ b/src/common/middlewares/attach-auth.middleware.ts @@ -2,9 +2,10 @@ * 把 URL Search 上的 `token` 附加到 Header Authorization 上 * @author Innei */ - import { IncomingMessage, ServerResponse } from 'http' + import { Injectable, NestMiddleware } from '@nestjs/common' + import { parseRelativeUrl } from '~/utils/ip.util' @Injectable() @@ -15,7 +16,7 @@ export class AttachHeaderTokenMiddleware implements NestMiddleware { const parser = parseRelativeUrl(url) if (parser.searchParams.get('token')) { - req.headers.authorization = parser.searchParams.get('token') + req.headers.authorization = parser.searchParams.get('token') as string } next() diff --git a/src/modules/aggregate/aggregate.interface.ts b/src/modules/aggregate/aggregate.interface.ts index 54567448..04b08fc1 100644 --- a/src/modules/aggregate/aggregate.interface.ts +++ b/src/modules/aggregate/aggregate.interface.ts @@ -3,8 +3,8 @@ export interface RSSProps { url: string author: string data: { - created: Date - modified: Date + created: Date | null + modified: Date | null link: string title: string text: string diff --git a/src/modules/aggregate/aggregate.service.ts b/src/modules/aggregate/aggregate.service.ts index 9572b775..8ec172a6 100644 --- a/src/modules/aggregate/aggregate.service.ts +++ b/src/modules/aggregate/aggregate.service.ts @@ -1,11 +1,21 @@ -import { URL } from 'url' -import { FilterQuery } from 'mongoose' -import { pick } from 'lodash' import dayjs from 'dayjs' -import { AnyParamConstructor } from '@typegoose/typegoose/lib/types' -import { DocumentType, ReturnModelType } from '@typegoose/typegoose' -import { OnEvent } from '@nestjs/event-emitter' +import { pick } from 'lodash' +import { FilterQuery } from 'mongoose' +import { URL } from 'url' + import { Inject, Injectable, forwardRef } from '@nestjs/common' +import { OnEvent } from '@nestjs/event-emitter' +import { DocumentType, ReturnModelType } from '@typegoose/typegoose' +import { AnyParamConstructor } from '@typegoose/typegoose/lib/types' + +import { CacheKeys, RedisKeys } from '~/constants/cache.constant' +import { EventBusEvents } from '~/constants/event.constant' +import { CacheService } from '~/processors/cache/cache.service' +import { WebEventsGateway } from '~/processors/gateway/web/events.gateway' +import { addYearCondition } from '~/transformers/db-query.transformer' +import { getRedisKey } from '~/utils/redis.util' +import { getShortDate } from '~/utils/time.util' + import { CategoryModel } from '../category/category.model' import { CategoryService } from '../category/category.service' import { CommentState } from '../comment/comment.model' @@ -21,13 +31,7 @@ import { RecentlyService } from '../recently/recently.service' import { SayService } from '../say/say.service' import { TimelineType } from './aggregate.dto' import { RSSProps } from './aggregate.interface' -import { getShortDate } from '~/utils/time.util' -import { getRedisKey } from '~/utils/redis.util' -import { addYearCondition } from '~/transformers/db-query.transformer' -import { WebEventsGateway } from '~/processors/gateway/web/events.gateway' -import { CacheService } from '~/processors/cache/cache.service' -import { CacheKeys, RedisKeys } from '~/constants/cache.constant' -import { EventBusEvents } from '~/constants/event.constant' + @Injectable() export class AggregateService { constructor( @@ -120,7 +124,11 @@ export class AggregateService { return { notes, posts, says } } - async getTimeline(year: number, type: TimelineType, sortBy: 1 | -1 = 1) { + async getTimeline( + year: number | undefined, + type: TimelineType | undefined, + sortBy: 1 | -1 = 1, + ) { const data: any = {} const getPosts = () => this.postService.model @@ -186,7 +194,9 @@ export class AggregateService { .then((list) => list.map((doc) => ({ url: new URL(`/${doc.slug}`, baseURL), - published_at: new Date(doc.modified), + published_at: doc.modified + ? new Date(doc.modified) + : new Date(doc.created!), })), ), @@ -207,7 +217,9 @@ export class AggregateService { list.map((doc) => { return { url: new URL(`/notes/${doc.nid}`, baseURL), - published_at: new Date(doc.modified), + published_at: doc.modified + ? new Date(doc.modified) + : new Date(doc.created!), } }), ), @@ -224,7 +236,9 @@ export class AggregateService { `/posts/${(doc.category as CategoryModel).slug}/${doc.slug}`, baseURL, ), - published_at: new Date(doc.modified), + published_at: doc.modified + ? new Date(doc.modified) + : new Date(doc.created!), } }), ), @@ -276,7 +290,7 @@ export class AggregateService { return { title: post.title, text: post.text, - created: post.created, + created: post.created!, modified: post.modified, link: new URL( '/posts' + `/${(post.category as CategoryModel).slug}/${post.slug}`, @@ -291,14 +305,14 @@ export class AggregateService { return { title: note.title, text: isSecret ? '这篇文章暂时没有公开呢' : note.text, - created: note.created, + created: note.created!, modified: note.modified, link: new URL(`/notes/${note.nid}`, baseURL).toString(), } }) return postsRss .concat(notesRss) - .sort((a, b) => b.created.getTime() - a.created.getTime()) + .sort((a, b) => b.created!.getTime() - a.created!.getTime()) .slice(0, 10) } diff --git a/src/modules/analyze/analyze.controller.ts b/src/modules/analyze/analyze.controller.ts index 02ffe592..63f76ed1 100644 --- a/src/modules/analyze/analyze.controller.ts +++ b/src/modules/analyze/analyze.controller.ts @@ -1,7 +1,7 @@ -import { Controller, Delete, Get, HttpCode, Query, Scope } from '@nestjs/common' import dayjs from 'dayjs' -import { AnalyzeDto } from './analyze.dto' -import { AnalyzeService } from './analyze.service' + +import { Controller, Delete, Get, HttpCode, Query, Scope } from '@nestjs/common' + import { Auth } from '~/common/decorator/auth.decorator' import { Paginator } from '~/common/decorator/http.decorator' import { ApiName } from '~/common/decorator/openapi.decorator' @@ -11,6 +11,9 @@ import { PagerDto } from '~/shared/dto/pager.dto' import { getRedisKey } from '~/utils/redis.util' import { getTodayEarly, getWeekStart } from '~/utils/time.util' +import { AnalyzeDto } from './analyze.dto' +import { AnalyzeService } from './analyze.service' + @Controller({ path: 'analyze', scope: Scope.REQUEST }) @ApiName @Auth() @@ -151,7 +154,7 @@ export class AnalyzeController { return Promise.all( keys.map(async (key) => { - const id = key.split('_').pop() + const id = key.split('_').pop()! return { id, diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 6942aa51..28e5ca17 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,17 +1,21 @@ -import { Injectable } from '@nestjs/common' -import { JwtService } from '@nestjs/jwt' -import { DocumentType, ReturnModelType } from '@typegoose/typegoose' import dayjs from 'dayjs' import { isDate, omit } from 'lodash' import { customAlphabet } from 'nanoid/async' -import { TokenDto } from './auth.controller' -import { JwtPayload } from './interfaces/jwt-payload.interface' -import { InjectModel } from '~/transformers/model.transformer' + +import { Injectable } from '@nestjs/common' +import { JwtService } from '@nestjs/jwt' +import { DocumentType, ReturnModelType } from '@typegoose/typegoose' + +import { MasterLostException } from '~/common/exceptions/master-lost.exception' import { TokenModel, UserModel as User, UserDocument, } from '~/modules/user/user.model' +import { InjectModel } from '~/transformers/model.transformer' + +import { TokenDto } from './auth.controller' +import { JwtPayload } from './interfaces/jwt-payload.interface' @Injectable() export class AuthService { @@ -21,7 +25,11 @@ export class AuthService { ) {} async signToken(_id: string) { - const { authCode } = await this.userModel.findById(_id).select('authCode') + const user = await this.userModel.findById(_id).select('authCode') + if (!user) { + throw new MasterLostException() + } + const authCode = user.authCode const payload = { _id, authCode, @@ -29,17 +37,27 @@ export class AuthService { return this.jwtService.sign(payload) } - async verifyPayload(payload: JwtPayload): Promise { + async verifyPayload(payload: JwtPayload): Promise { const user = await this.userModel.findById(payload._id).select('+authCode') + if (!user) { + throw new MasterLostException() + } + return user && user.authCode === payload.authCode ? user : null } - private async getAccessTokens(): Promise[]> { + + private async getAccessTokens() { return (await this.userModel.findOne().select('apiToken').lean()) - .apiToken as any + ?.apiToken as TokenModel[] | undefined } async getAllAccessToken() { - return (await this.getAccessTokens()).map((token) => ({ + const tokens = await this.getAccessTokens() + if (!tokens) { + return [] + } + return tokens.map((token) => ({ + // @ts-ignore id: token._id, ...omit(token, ['_id', '__v', 'token']), })) as any as TokenModel[] @@ -47,7 +65,11 @@ export class AuthService { async getTokenSecret(id: string) { const tokens = await this.getAccessTokens() + if (!tokens) { + return null + } // note: _id is ObjectId not equal to string + // @ts-ignore return tokens.find((token) => String(token._id) === id) } diff --git a/src/modules/backup/backup.service.ts b/src/modules/backup/backup.service.ts index 71798a48..ac6cf64f 100644 --- a/src/modules/backup/backup.service.ts +++ b/src/modules/backup/backup.service.ts @@ -1,16 +1,17 @@ import { existsSync, statSync } from 'fs' import { readFile, readdir, rm, writeFile } from 'fs/promises' +import mkdirp from 'mkdirp' import { join, resolve } from 'path' import { Readable } from 'stream' -import mkdirp from 'mkdirp' +import { quiet } from 'zx-cjs' + import { BadRequestException, Injectable, InternalServerErrorException, Logger, } from '@nestjs/common' -import { quiet } from 'zx-cjs' -import { ConfigsService } from '../configs/configs.service' + import { MONGO_DB } from '~/app.config' import { BACKUP_DIR, DATA_DIR } from '~/constants/path.constant' import { CacheService } from '~/processors/cache/cache.service' @@ -19,6 +20,8 @@ import { EventTypes } from '~/processors/gateway/events.types' import { getMediumDateTime } from '~/utils' import { getFolderSize } from '~/utils/system.util' +import { ConfigsService } from '../configs/configs.service' + @Injectable() export class BackupService { private logger: Logger @@ -37,7 +40,7 @@ export class BackupService { return [] } const backupFilenames = await readdir(backupPath) - const backups = [] + const backups: { filename: string; path: string }[] = [] for (const filename of backupFilenames) { const path = resolve(backupPath, filename) @@ -53,6 +56,7 @@ export class BackupService { backups.map(async (item) => { const { path } = item const size = await getFolderSize(path) + // @ts-ignore delete item.path return { ...item, size } }), diff --git a/src/modules/comment/comment.controller.ts b/src/modules/comment/comment.controller.ts index 868275a5..68b81265 100644 --- a/src/modules/comment/comment.controller.ts +++ b/src/modules/comment/comment.controller.ts @@ -14,16 +14,7 @@ import { } from '@nestjs/common' import { ApiOperation, ApiParam } from '@nestjs/swagger' import { DocumentType } from '@typegoose/typegoose' -import { UserModel } from '../user/user.model' -import { - CommentDto, - CommentRefTypesDto, - StateDto, - TextOnlyDto, -} from './comment.dto' -import { CommentFilterEmailInterceptor } from './comment.interceptor' -import { CommentModel, CommentState } from './comment.model' -import { CommentService } from './comment.service' + import { Auth } from '~/common/decorator/auth.decorator' import { CurrentUser } from '~/common/decorator/current-user.decorator' import { IpLocation, IpRecord } from '~/common/decorator/ip.decorator' @@ -36,6 +27,18 @@ import { ReplyMailType } from '~/processors/helper/helper.email.service' import { MongoIdDto } from '~/shared/dto/id.dto' import { PagerDto } from '~/shared/dto/pager.dto' import { transformDataToPaginate } from '~/transformers/paginate.transformer' + +import { UserModel } from '../user/user.model' +import { + CommentDto, + CommentRefTypesDto, + StateDto, + TextOnlyDto, +} from './comment.dto' +import { CommentFilterEmailInterceptor } from './comment.interceptor' +import { CommentModel, CommentState } from './comment.model' +import { CommentService } from './comment.service' + @Controller({ path: 'comments' }) @UseInterceptors(CommentFilterEmailInterceptor) @ApiName @@ -236,6 +239,7 @@ export class CommentController { url, state: CommentState.Read, } as CommentDto + // @ts-ignore return await this.replyByCid(params, model, undefined, true, ipLocation) } diff --git a/src/modules/comment/comment.model.ts b/src/modules/comment/comment.model.ts index 970d8228..23644450 100644 --- a/src/modules/comment/comment.model.ts +++ b/src/modules/comment/comment.model.ts @@ -1,4 +1,6 @@ +import { Query, Types } from 'mongoose' import { URL } from 'url' + import { DocumentType, Ref, @@ -7,12 +9,13 @@ import { prop, } from '@typegoose/typegoose' import { BeAnObject } from '@typegoose/typegoose/lib/types' -import { Query, Types } from 'mongoose' + +import { BaseModel } from '~/shared/model/base.model' +import { getAvatar } from '~/utils' + import { NoteModel } from '../note/note.model' import { PageModel } from '../page/page.model' import { PostModel } from '../post/post.model' -import { getAvatar } from '~/utils' -import { BaseModel } from '~/shared/model/base.model' function autoPopulateSubs( this: Query< @@ -68,7 +71,7 @@ export class CommentModel extends BaseModel { url?: string @prop({ required: true }) - text!: string + text: string // 0 : 未读 // 1 : 已读 diff --git a/src/modules/comment/comment.service.ts b/src/modules/comment/comment.service.ts index 9c0c27d5..ee7bf700 100644 --- a/src/modules/comment/comment.service.ts +++ b/src/modules/comment/comment.service.ts @@ -1,21 +1,26 @@ +import { LeanDocument, Types } from 'mongoose' import { URL } from 'url' + import { BadRequestException, Injectable, Logger } from '@nestjs/common' import { DocumentType } from '@typegoose/typegoose' import { BeAnObject } from '@typegoose/typegoose/lib/types' -import { LeanDocument, Types } from 'mongoose' -import { ConfigsService } from '../configs/configs.service' -import { UserService } from '../user/user.service' -import BlockedKeywords from './block-keywords.json' -import { CommentModel, CommentRefTypes } from './comment.model' -import { InjectModel } from '~/transformers/model.transformer' + import { CannotFindException } from '~/common/exceptions/cant-find.exception' +import { MasterLostException } from '~/common/exceptions/master-lost.exception' import { DatabaseService } from '~/processors/database/database.service' import { EmailService, ReplyMailType, } from '~/processors/helper/helper.email.service' import { WriteBaseModel } from '~/shared/model/base.model' +import { InjectModel } from '~/transformers/model.transformer' import { hasChinese } from '~/utils' + +import { ConfigsService } from '../configs/configs.service' +import { UserService } from '../user/user.service' +import BlockedKeywords from './block-keywords.json' +import { CommentModel, CommentRefTypes } from './comment.model' + @Injectable() export class CommentService { private readonly logger: Logger = new Logger(CommentService.name) @@ -44,7 +49,7 @@ export class CommentService { } } - async checkSpam(doc: Partial) { + async checkSpam(doc: CommentModel) { const res = await (async () => { const commentOptions = await this.configs.get('commentOptions') if (!commentOptions.antiSpam) { @@ -55,7 +60,11 @@ export class CommentService { return false } if (commentOptions.blockIps) { + if (!doc.ip) { + return false + } const isBlock = commentOptions.blockIps.some((ip) => + // @ts-ignore new RegExp(ip, 'ig').test(doc.ip), ) if (isBlock) { @@ -99,13 +108,13 @@ export class CommentService { } else { const { type: type_, document } = await this.databaseService.findGlobalById(id) - ref = document + ref = document as any type = type_ as any } if (!ref) { throw new CannotFindException() } - const commentIndex = ref.commentsIndex + const commentIndex = ref.commentsIndex || 0 doc.key = `#${commentIndex + 1}` const comment = await this.commentModel.create({ ...doc, @@ -166,9 +175,15 @@ export class CommentService { if (type) { const model = this.getModelByRefType(type) const doc = await model.findById(id) + if (!doc) { + throw new CannotFindException() + } return doc.allowComment ?? true } else { const { document: doc } = await this.databaseService.findGlobalById(id) + if (!doc) { + throw new CannotFindException() + } return doc.allowComment ?? true } } @@ -201,16 +216,26 @@ export class CommentService { } this.userService.model.findOne().then(async (master) => { + if (!master) { + throw new MasterLostException() + } + const refType = model.refType const refModel = this.getModelByRefType(refType) const refDoc = await refModel.findById(model.ref).lean() - const time = new Date(model.created) - const parent = await this.commentModel.findOne({ _id: model.parent }) + const time = new Date(model.created!) + const parent = await this.commentModel + .findOne({ _id: model.parent }) + .lean() const parsedTime = `${time.getDate()}/${ time.getMonth() + 1 }/${time.getFullYear()}` + if (!parent || !parent.mail || !refDoc || !master.mail || !model.mail) { + return + } + this.mailService.sendCommentNotificationMail({ to: type === ReplyMailType.Owner ? master.mail : parent.mail, type, diff --git a/src/modules/configs/configs.jsonschema.decorator.ts b/src/modules/configs/configs.jsonschema.decorator.ts index 98ca01f1..569f420f 100644 --- a/src/modules/configs/configs.jsonschema.decorator.ts +++ b/src/modules/configs/configs.jsonschema.decorator.ts @@ -4,27 +4,21 @@ import { DecoratorSchema } from 'class-validator-jsonschema/build/decorators' export const JSONSchemaPasswordField = ( title: string, schema?: DecoratorSchema, -): PropertyDecorator => +) => JSONSchema({ title, 'ui:options': { showPassword: true }, ...schema, }) -export const JSONSchemaPlainField = ( - title: string, - schema?: DecoratorSchema, -): PropertyDecorator => +export const JSONSchemaPlainField = (title: string, schema?: DecoratorSchema) => JSONSchema({ title, // 'ui:options': {}, ...schema, }) -export const JSONSchemaArrayField = ( - title: string, - schema?: DecoratorSchema, -): PropertyDecorator => +export const JSONSchemaArrayField = (title: string, schema?: DecoratorSchema) => JSONSchema({ title, // 'ui:options': {}, @@ -34,7 +28,7 @@ export const JSONSchemaArrayField = ( export const JSONSchemaToggleField = ( title: string, schema?: DecoratorSchema, -): PropertyDecorator => +) => JSONSchema({ title, // 'ui:options': {}, @@ -44,7 +38,7 @@ export const JSONSchemaToggleField = ( export const JSONSchemaNumberField = ( title: string, schema?: DecoratorSchema, -): PropertyDecorator => +) => JSONSchema({ title, // 'ui:options': {}, diff --git a/src/modules/configs/configs.model.ts b/src/modules/configs/configs.model.ts index f7bc1524..ccf26a3d 100644 --- a/src/modules/configs/configs.model.ts +++ b/src/modules/configs/configs.model.ts @@ -1,12 +1,13 @@ -import { Severity, modelOptions, prop } from '@typegoose/typegoose' import { Schema } from 'mongoose' +import { Severity, modelOptions, prop } from '@typegoose/typegoose' + @modelOptions({ options: { allowMixed: Severity.ALLOW, customName: 'Option' }, schemaOptions: { timestamps: { - createdAt: null, - updatedAt: null, + createdAt: false, + updatedAt: false, }, }, }) diff --git a/src/modules/configs/configs.service.ts b/src/modules/configs/configs.service.ts index a5fc729d..adb8b46b 100644 --- a/src/modules/configs/configs.service.ts +++ b/src/modules/configs/configs.service.ts @@ -1,4 +1,10 @@ +import camelcaseKeys from 'camelcase-keys' +import { ClassConstructor, plainToInstance } from 'class-transformer' +import { ValidatorOptions, validateSync } from 'class-validator' import cluster from 'cluster' +import { cloneDeep, mergeWith } from 'lodash' +import { LeanDocument } from 'mongoose' + import { BadRequestException, Injectable, @@ -8,11 +14,14 @@ import { import { EventEmitter2 } from '@nestjs/event-emitter' import { DocumentType, ReturnModelType } from '@typegoose/typegoose' import { BeAnObject } from '@typegoose/typegoose/lib/types' -import camelcaseKeys from 'camelcase-keys' -import { ClassConstructor, plainToInstance } from 'class-transformer' -import { ValidatorOptions, validateSync } from 'class-validator' -import { cloneDeep, mergeWith } from 'lodash' -import { LeanDocument } from 'mongoose' + +import { RedisKeys } from '~/constants/cache.constant' +import { EventBusEvents } from '~/constants/event.constant' +import { CacheService } from '~/processors/cache/cache.service' +import { InjectModel } from '~/transformers/model.transformer' +import { sleep } from '~/utils' +import { getRedisKey } from '~/utils/redis.util' + import * as optionDtos from '../configs/configs.dto' import { UserModel } from '../user/user.model' import { UserService } from '../user/user.service' @@ -23,12 +32,6 @@ import { } from './configs.dto' import { IConfig, IConfigKeys } from './configs.interface' import { OptionModel } from './configs.model' -import { InjectModel } from '~/transformers/model.transformer' -import { RedisKeys } from '~/constants/cache.constant' -import { EventBusEvents } from '~/constants/event.constant' -import { CacheService } from '~/processors/cache/cache.service' -import { sleep } from '~/utils' -import { getRedisKey } from '~/utils/redis.util' const allOptionKeys: Set = new Set() const map: Record = Object.entries(optionDtos).reduce( @@ -66,7 +69,7 @@ const generateDefaultConfig: () => IConfig = () => ({ title: 'おかえり~', background: 'https://gitee.com/xun7788/my-imagination/raw/master/images/88426823_p0.jpg', - gaodemapKey: null, + gaodemapKey: null!, }, terminalOptions: { enable: false, diff --git a/src/modules/feed/feed.controller.ts b/src/modules/feed/feed.controller.ts index be526c95..390f69d2 100644 --- a/src/modules/feed/feed.controller.ts +++ b/src/modules/feed/feed.controller.ts @@ -1,13 +1,16 @@ -import { CacheKey, CacheTTL, Controller, Get, Header } from '@nestjs/common' import { minify } from 'html-minifier' import xss from 'xss' -import { AggregateService } from '../aggregate/aggregate.service' -import { ConfigsService } from '../configs/configs.service' -import { MarkdownService } from '../markdown/markdown.service' + +import { CacheKey, CacheTTL, Controller, Get, Header } from '@nestjs/common' + import { HTTPDecorators } from '~/common/decorator/http.decorator' import { ApiName } from '~/common/decorator/openapi.decorator' import { CacheKeys } from '~/constants/cache.constant' +import { AggregateService } from '../aggregate/aggregate.service' +import { ConfigsService } from '../configs/configs.service' +import { MarkdownService } from '../markdown/markdown.service' + @Controller('feed') @ApiName export class FeedController { @@ -43,7 +46,7 @@ export class FeedController { ${now.toISOString()} zh-CN - ${xss(avatar)} + ${xss(avatar || '')} ${title} ${xss(url)} diff --git a/src/modules/health/health.dto.ts b/src/modules/health/health.dto.ts index 25af2ef1..24899549 100644 --- a/src/modules/health/health.dto.ts +++ b/src/modules/health/health.dto.ts @@ -19,8 +19,7 @@ export class LogQueryDto { index: number @IsString() - @IsOptional() - filename?: string + filename: string } export class LogTypeDto { diff --git a/src/modules/link/link.service.ts b/src/modules/link/link.service.ts index f048c37b..d4902ed7 100644 --- a/src/modules/link/link.service.ts +++ b/src/modules/link/link.service.ts @@ -1,7 +1,10 @@ -import { BadRequestException, Injectable, Logger } from '@nestjs/common' -import { ConfigsService } from '../configs/configs.service' -import { LinkModel, LinkState, LinkType } from './link.model' -import { InjectModel } from '~/transformers/model.transformer' +import { + BadRequestException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common' + import { isDev } from '~/global/env.global' import { AdminEventsGateway } from '~/processors/gateway/admin/events.gateway' import { EventTypes } from '~/processors/gateway/events.types' @@ -10,6 +13,10 @@ import { LinkApplyEmailType, } from '~/processors/helper/helper.email.service' import { HttpService } from '~/processors/helper/helper.http.service' +import { InjectModel } from '~/transformers/model.transformer' + +import { ConfigsService } from '../configs/configs.service' +import { LinkModel, LinkState, LinkType } from './link.model' @Injectable() export class LinkService { @@ -51,6 +58,10 @@ export class LinkService { ) .lean() + if (!doc) { + throw new NotFoundException() + } + return doc } diff --git a/src/modules/markdown/markdown.controller.ts b/src/modules/markdown/markdown.controller.ts index 668e2cf3..2caa5565 100644 --- a/src/modules/markdown/markdown.controller.ts +++ b/src/modules/markdown/markdown.controller.ts @@ -1,7 +1,15 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import dayjs from 'dayjs' +import { render } from 'ejs' +import { minify } from 'html-minifier' +import JSZip from 'jszip' +import { isNil } from 'lodash' import { join } from 'path' import { performance } from 'perf_hooks' import { Readable } from 'stream' import { URL } from 'url' +import xss from 'xss' + import { Body, CacheTTL, @@ -14,12 +22,16 @@ import { Query, } from '@nestjs/common' import { ApiProperty } from '@nestjs/swagger' -import dayjs from 'dayjs' -import { render } from 'ejs' -import { minify } from 'html-minifier' -import JSZip from 'jszip' -import { isNil } from 'lodash' -import xss from 'xss' + +import { Auth } from '~/common/decorator/auth.decorator' +import { HttpCache } from '~/common/decorator/cache.decorator' +import { HTTPDecorators } from '~/common/decorator/http.decorator' +import { ApiName } from '~/common/decorator/openapi.decorator' +import { IsMaster } from '~/common/decorator/role.decorator' +import { ArticleTypeEnum } from '~/constants/article.constant' +import { MongoIdDto } from '~/shared/dto/id.dto' +import { getShortDateTime } from '~/utils' + import { CategoryModel } from '../category/category.model' import { ConfigsService } from '../configs/configs.service' import { NoteModel } from '../note/note.model' @@ -32,14 +44,6 @@ import { } from './markdown.dto' import { MarkdownYAMLProperty } from './markdown.interface' import { MarkdownService } from './markdown.service' -import { Auth } from '~/common/decorator/auth.decorator' -import { HttpCache } from '~/common/decorator/cache.decorator' -import { HTTPDecorators } from '~/common/decorator/http.decorator' -import { ApiName } from '~/common/decorator/openapi.decorator' -import { IsMaster } from '~/common/decorator/role.decorator' -import { ArticleTypeEnum } from '~/constants/article.constant' -import { MongoIdDto } from '~/shared/dto/id.dto' -import { getShortDateTime } from '~/utils' @Controller('markdown') @ApiName @@ -80,7 +84,7 @@ export class MarkdownController { T extends { text: string created?: Date - modified: Date + modified?: Date | null title: string slug?: string }, @@ -89,7 +93,7 @@ export class MarkdownController { extraMetaData: Record = {}, ): MarkdownYAMLProperty => { const meta = { - created: item.created, + created: item.created!, modified: item.modified, title: item.title, slug: item.slug || item.title, @@ -106,14 +110,14 @@ export class MarkdownController { } // posts const convertPost = posts.map((post) => - convertor(post, { + convertor(post!, { categories: (post.category as CategoryModel).name, type: 'post', permalink: `posts/${post.slug}`, }), ) const convertNote = notes.map((note) => - convertor(note, { + convertor(note!, { mood: note.mood, weather: note.weather, id: note.nid, @@ -123,7 +127,7 @@ export class MarkdownController { }), ) const convertPage = pages.map((page) => - convertor(page, { + convertor(page!, { subtitle: page.subtitle, type: 'page', permalink: page.slug, @@ -206,7 +210,7 @@ export class MarkdownController { } })() - const url = new URL(relativePath, webUrl) + const url = new URL(relativePath!, webUrl) const structure = await this.service.getRenderedMarkdownHtmlStructure( markdown, @@ -271,6 +275,7 @@ export class MarkdownController { async getRenderedMarkdownHtmlStructure(@Param() params: MongoIdDto) { const { id } = params const { html, document } = await this.service.renderArticle(id) + return this.service.getRenderedMarkdownHtmlStructure(html, document.title) } } diff --git a/src/modules/markdown/markdown.interface.ts b/src/modules/markdown/markdown.interface.ts index 872e03a9..801357f6 100644 --- a/src/modules/markdown/markdown.interface.ts +++ b/src/modules/markdown/markdown.interface.ts @@ -1,6 +1,6 @@ export type MetaType = { - created: Date - modified: Date + created?: Date | null | undefined + modified?: Date | null | undefined title: string slug: string } & Record diff --git a/src/modules/markdown/markdown.service.ts b/src/modules/markdown/markdown.service.ts index a5e22158..9dbe80d9 100644 --- a/src/modules/markdown/markdown.service.ts +++ b/src/modules/markdown/markdown.service.ts @@ -1,20 +1,29 @@ -import { BadRequestException, Injectable, Logger } from '@nestjs/common' -import { ReturnModelType } from '@typegoose/typegoose' import { dump } from 'js-yaml' import JSZip from 'jszip' import { omit } from 'lodash' import { marked } from 'marked' import { Types } from 'mongoose' import xss from 'xss' + +import { + BadRequestException, + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common' +import { ReturnModelType } from '@typegoose/typegoose' + +import { DatabaseService } from '~/processors/database/database.service' +import { AssetService } from '~/processors/helper/helper.asset.service' +import { InjectModel } from '~/transformers/model.transformer' + import { CategoryModel } from '../category/category.model' import { NoteModel } from '../note/note.model' import { PageModel } from '../page/page.model' import { PostModel } from '../post/post.model' import { DatatypeDto } from './markdown.dto' import { MarkdownYAMLProperty } from './markdown.interface' -import { InjectModel } from '~/transformers/model.transformer' -import { AssetService } from '~/processors/helper/helper.asset.service' -import { DatabaseService } from '~/processors/database/database.service' + @Injectable() export class MarkdownService { constructor( @@ -40,7 +49,7 @@ export class MarkdownService { }, ) - const insertOrCreateCategory = async (name: string) => { + const insertOrCreateCategory = async (name?: string) => { if (!name) { return } @@ -70,7 +79,9 @@ export class MarkdownService { const genDate = this.genDate const models = [] as PostModel[] const defaultCategory = await this.categoryModel.findOne() - + if (!defaultCategory) { + throw new InternalServerErrorException('分类不存在') + } for await (const item of data) { if (!item.meta) { models.push({ @@ -214,6 +225,7 @@ ${text.trim()} return { html: this.renderMarkdownContent(doc.document.text), ...doc, + document: doc.document, } } @@ -231,7 +243,7 @@ ${text.trim()} level: 'inline', name: 'spoiler', start(src) { - return src.match(/\|/)?.index + return src.match(/\|/)?.index ?? -1 }, renderer(token) { // @ts-ignore @@ -257,7 +269,7 @@ ${text.trim()} level: 'inline', name: 'mention', start(src) { - return src.match(/\(/)?.index + return src.match(/\(/)?.index ?? -1 }, renderer(token) { // @ts-ignore @@ -281,6 +293,10 @@ ${text.trim()} renderer: { image(src, title, _alt) { + if (typeof src !== 'string') { + return '' + } + const alt = _alt?.match(/^[!¡]/) ? _alt.replace(/^[¡!]/, '') : '' if (!alt) { return `` diff --git a/src/modules/note/note.controller.ts b/src/modules/note/note.controller.ts index acd92b87..781c4bb1 100644 --- a/src/modules/note/note.controller.ts +++ b/src/modules/note/note.controller.ts @@ -13,14 +13,7 @@ import { Query, } from '@nestjs/common' import { ApiOperation } from '@nestjs/swagger' -import { - ListQueryDto, - NidType, - NoteQueryDto, - PasswordQueryDto, -} from './note.dto' -import { NoteModel, PartialNoteModel } from './note.model' -import { NoteService } from './note.service' + import { Auth } from '~/common/decorator/auth.decorator' import { Paginator } from '~/common/decorator/http.decorator' import { IpLocation, IpRecord } from '~/common/decorator/ip.decorator' @@ -35,6 +28,15 @@ import { addYearCondition, } from '~/transformers/db-query.transformer' +import { + ListQueryDto, + NidType, + NoteQueryDto, + PasswordQueryDto, +} from './note.dto' +import { NoteModel, PartialNoteModel } from './note.model' +import { NoteService } from './note.service' + @ApiName @Controller({ path: 'notes' }) export class NoteController { @@ -221,7 +223,7 @@ export class NoteController { ) { const id = typeof param.id === 'number' - ? (await this.noteService.model.findOne({ nid: param.id }).lean())._id + ? (await this.noteService.model.findOne({ nid: param.id }).lean())?._id : param.id if (!id) { throw new CannotFindException() diff --git a/src/modules/note/note.service.ts b/src/modules/note/note.service.ts index 026f1115..0b17a60d 100644 --- a/src/modules/note/note.service.ts +++ b/src/modules/note/note.service.ts @@ -1,17 +1,20 @@ +import { isDefined, isMongoId } from 'class-validator' +import { FilterQuery } from 'mongoose' + import { Injectable } from '@nestjs/common' import { EventEmitter2 } from '@nestjs/event-emitter' import { DocumentType } from '@typegoose/typegoose' -import { isDefined, isMongoId } from 'class-validator' -import { FilterQuery } from 'mongoose' -import { NoteModel } from './note.model' -import { InjectModel } from '~/transformers/model.transformer' + import { CannotFindException } from '~/common/exceptions/cant-find.exception' import { EventBusEvents } from '~/constants/event.constant' import { EventTypes } from '~/processors/gateway/events.types' import { WebEventsGateway } from '~/processors/gateway/web/events.gateway' import { ImageService } from '~/processors/helper/helper.image.service' +import { InjectModel } from '~/transformers/model.transformer' import { deleteKeys } from '~/utils' +import { NoteModel } from './note.model' + @Injectable() export class NoteService { constructor( @@ -67,7 +70,7 @@ export class NoteService { checkPasswordToAccess( doc: T, - password: string, + password?: string, ): boolean { const hasPassword = doc.password if (!hasPassword) { @@ -113,6 +116,9 @@ export class NoteService { await Promise.all([ this.imageService.recordImageDimensions(this.noteModel, id), this.model.findById(id).then((doc) => { + if (!doc) { + return + } delete doc.password this.webGateway.broadcast(EventTypes.NOTE_UPDATE, doc) }), diff --git a/src/modules/page/page.model.ts b/src/modules/page/page.model.ts index 0667a735..33842419 100644 --- a/src/modules/page/page.model.ts +++ b/src/modules/page/page.model.ts @@ -1,5 +1,3 @@ -import { PartialType } from '@nestjs/mapped-types' -import { modelOptions, prop } from '@typegoose/typegoose' import { Transform } from 'class-transformer' import { IsEnum, @@ -9,8 +7,13 @@ import { IsString, Min, } from 'class-validator' + +import { PartialType } from '@nestjs/mapped-types' +import { modelOptions, prop } from '@typegoose/typegoose' + import { WriteBaseModel } from '~/shared/model/base.model' import { IsNilOrString } from '~/utils/validator/isNilOrString' + export enum PageType { 'md' = 'md', 'html' = 'html', @@ -28,7 +31,7 @@ export class PageModel extends WriteBaseModel { @IsNotEmpty() slug!: string - @prop({ trim: true }) + @prop({ trim: true, type: String }) @IsString() @IsOptional() @IsNilOrString() diff --git a/src/modules/pageproxy/pageproxy.service.ts b/src/modules/pageproxy/pageproxy.service.ts index b2bd45c7..19e3b081 100644 --- a/src/modules/pageproxy/pageproxy.service.ts +++ b/src/modules/pageproxy/pageproxy.service.ts @@ -1,10 +1,14 @@ -import { URL } from 'url' -import { Injectable, InternalServerErrorException } from '@nestjs/common' import jsdom from 'jsdom' -import { ConfigsService } from '../configs/configs.service' -import { InitService } from '../init/init.service' +import { URL } from 'url' + +import { Injectable, InternalServerErrorException } from '@nestjs/common' + import PKG from '~/../package.json' import { API_VERSION } from '~/app.config' + +import { ConfigsService } from '../configs/configs.service' +import { InitService } from '../init/init.service' + @Injectable() export class PageProxyService { constructor( @@ -39,7 +43,7 @@ export class PageProxyService { from?: string BASE_API?: string GATEWAY?: string - [key: string]: string + [key: string]: string | undefined }, ) { const config = await this.configs.waitForConfigReady() diff --git a/src/modules/project/project.model.ts b/src/modules/project/project.model.ts index e9130fe2..35ab9878 100644 --- a/src/modules/project/project.model.ts +++ b/src/modules/project/project.model.ts @@ -1,7 +1,9 @@ -import { PartialType } from '@nestjs/swagger' -import { modelOptions, prop } from '@typegoose/typegoose' import { Transform } from 'class-transformer' import { IsOptional, IsString, IsUrl, isURL } from 'class-validator' + +import { PartialType } from '@nestjs/swagger' +import { modelOptions, prop } from '@typegoose/typegoose' + import { BaseModel } from '~/shared/model/base.model' const validateURL = { @@ -16,6 +18,7 @@ const validateURL = { if (!isURL(v, { require_protocol: true })) { return false } + return true }, } diff --git a/src/modules/pty/pty.service.ts b/src/modules/pty/pty.service.ts index c9e5d796..e4e60237 100644 --- a/src/modules/pty/pty.service.ts +++ b/src/modules/pty/pty.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common' + import { RedisKeys } from '~/constants/cache.constant' import { CacheService } from '~/processors/cache/cache.service' import { getRedisKey } from '~/utils' @@ -18,7 +19,8 @@ export class PTYService { ) return values - .map((value) => { + .filter(Boolean) + .map((value: string) => { const [startTime, ip, endTime] = value.split(',') as [ string, string, diff --git a/src/modules/search/search.service.ts b/src/modules/search/search.service.ts index f07352e5..78a97198 100644 --- a/src/modules/search/search.service.ts +++ b/src/modules/search/search.service.ts @@ -1,3 +1,5 @@ +import algoliasearch from 'algoliasearch' + import type { SearchResponse } from '@algolia/client-search' import { BadRequestException, @@ -5,15 +7,16 @@ import { Injectable, forwardRef, } from '@nestjs/common' -import algoliasearch from 'algoliasearch' -import { ConfigsService } from '../configs/configs.service' -import { NoteService } from '../note/note.service' -import { PostService } from '../post/post.service' + import { SearchDto } from '~/modules/search/search.dto' import { DatabaseService } from '~/processors/database/database.service' import { Pagination } from '~/shared/interface/paginator.interface' import { transformDataToPaginate } from '~/transformers/paginate.transformer' +import { ConfigsService } from '../configs/configs.service' +import { NoteService } from '../note/note.service' +import { PostService } from '../post/post.service' + @Injectable() export class SearchService { constructor( @@ -84,6 +87,13 @@ export class SearchService { if (!algoliaSearchOptions.enable) { throw new BadRequestException('algolia not enable.') } + if ( + !algoliaSearchOptions.appId || + !algoliaSearchOptions.apiKey || + !algoliaSearchOptions.indexName + ) { + throw new BadRequestException('algolia not config.') + } const client = algoliasearch( algoliaSearchOptions.appId, algoliaSearchOptions.apiKey, @@ -128,7 +138,7 @@ export class SearchService { if (searchOption.rawAlgolia) { return search } - const data = [] + const data: any[] = [] const tasks = search.hits.map((hit) => { const { type, objectID } = hit @@ -141,8 +151,10 @@ export class SearchService { .select('_id title created modified categoryId slug nid') .lean() .then((doc) => { - Reflect.set(doc, 'type', type) - doc && data.push(doc) + if (doc) { + Reflect.set(doc, 'type', type) + data.push(doc) + } }) }) await Promise.all(tasks) diff --git a/src/modules/serverless/serverless.service.ts b/src/modules/serverless/serverless.service.ts index 5c63202a..92e87046 100644 --- a/src/modules/serverless/serverless.service.ts +++ b/src/modules/serverless/serverless.service.ts @@ -1,6 +1,9 @@ +import { isURL } from 'class-validator' import fs, { mkdir, stat } from 'fs/promises' +import { cloneDeep } from 'lodash' import path from 'path' import { nextTick } from 'process' + import { TransformOptions, parseAsync, transformAsync } from '@babel/core' import * as t from '@babel/types' import { VariableDeclaration } from '@babel/types' @@ -10,14 +13,7 @@ import { Logger, } from '@nestjs/common' import { Interval } from '@nestjs/schedule' -import { isURL } from 'class-validator' -import { cloneDeep } from 'lodash' -import PKG from '../../../package.json' -import { SnippetModel } from '../snippet/snippet.model' -import { - FunctionContextRequest, - FunctionContextResponse, -} from './function.types' + import { RedisKeys } from '~/constants/cache.constant' import { DATA_DIR, NODE_REQUIRE_PATH } from '~/constants/path.constant' import { CacheService } from '~/processors/cache/cache.service' @@ -29,6 +25,14 @@ import { UniqueArray } from '~/ts-hepler/unique' import { getRedisKey, safePathJoin } from '~/utils' import { safeEval } from '~/utils/safe-eval.util' import { isBuiltinModule } from '~/utils/sys.util' + +import PKG from '../../../package.json' +import { SnippetModel } from '../snippet/snippet.model' +import { + FunctionContextRequest, + FunctionContextResponse, +} from './function.types' + @Injectable() export class ServerlessService { constructor( @@ -253,6 +257,9 @@ export class ServerlessService { } private convertTypescriptCode(code: string) { return transformAsync(code, this.getBabelOptions()).then((res) => { + if (!res) { + throw new InternalServerErrorException('convert code error') + } if (isDev) { console.log(res.code) } diff --git a/src/modules/sitemap/sitemap.controller.ts b/src/modules/sitemap/sitemap.controller.ts index 08415222..dd80f994 100644 --- a/src/modules/sitemap/sitemap.controller.ts +++ b/src/modules/sitemap/sitemap.controller.ts @@ -1,9 +1,13 @@ -import { CacheKey, CacheTTL, Controller, Get, Header } from '@nestjs/common' import { minify } from 'html-minifier' -import { AggregateService } from '../aggregate/aggregate.service' + +import { CacheKey, CacheTTL, Controller, Get, Header } from '@nestjs/common' + import { HTTPDecorators } from '~/common/decorator/http.decorator' import { ApiName } from '~/common/decorator/openapi.decorator' import { CacheKeys } from '~/constants/cache.constant' + +import { AggregateService } from '../aggregate/aggregate.service' + @Controller('sitemap') @ApiName export class SitemapController { @@ -24,7 +28,7 @@ export class SitemapController { .map( (item) => ` ${item.url} - ${item.published_at.toISOString()} + ${item.published_at?.toISOString() || 'N/A'} `, ) .join('')} diff --git a/src/modules/snippet/snippet.service.ts b/src/modules/snippet/snippet.service.ts index b7875040..0ee399d3 100644 --- a/src/modules/snippet/snippet.service.ts +++ b/src/modules/snippet/snippet.service.ts @@ -1,3 +1,5 @@ +import { load } from 'js-yaml' + import { BadRequestException, Inject, @@ -5,14 +7,15 @@ import { NotFoundException, forwardRef, } from '@nestjs/common' -import { load } from 'js-yaml' -import { ServerlessService } from '../serverless/serverless.service' -import { SnippetModel, SnippetType } from './snippet.model' -import { InjectModel } from '~/transformers/model.transformer' + import { RedisKeys } from '~/constants/cache.constant' import { CacheService } from '~/processors/cache/cache.service' +import { InjectModel } from '~/transformers/model.transformer' import { getRedisKey } from '~/utils' +import { ServerlessService } from '../serverless/serverless.service' +import { SnippetModel, SnippetType } from './snippet.model' + @Injectable() export class SnippetService { constructor( @@ -58,6 +61,9 @@ export class SnippetService { async delete(id: string) { const doc = await this.model.findOneAndDelete({ _id: id }).lean() + if (!doc) { + throw new NotFoundException() + } await this.deleteCachedSnippet(doc.reference, doc.name) } diff --git a/src/modules/user/user.model.ts b/src/modules/user/user.model.ts index cc6c7ef8..85a9d721 100644 --- a/src/modules/user/user.model.ts +++ b/src/modules/user/user.model.ts @@ -1,11 +1,13 @@ +import { hashSync } from 'bcrypt' +import { Schema } from 'mongoose' + import { DocumentType, Severity, modelOptions, prop, } from '@typegoose/typegoose' -import { hashSync } from 'bcrypt' -import { Schema } from 'mongoose' + import { BaseModel } from '~/shared/model/base.model' export type UserDocument = DocumentType @@ -58,7 +60,7 @@ export class UserModel extends BaseModel { password!: string @prop() - mail?: string + mail: string @prop() url?: string diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 8bb2887a..8aa3f2c0 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,21 +1,27 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { compareSync } from 'bcrypt' +import { nanoid } from 'nanoid' + import { BadRequestException, ForbiddenException, Injectable, + InternalServerErrorException, Logger, UnprocessableEntityException, } from '@nestjs/common' import { ReturnModelType } from '@typegoose/typegoose' -import { compareSync } from 'bcrypt' -import { nanoid } from 'nanoid' -import { AuthService } from '../auth/auth.service' -import { UserDocument, UserModel } from './user.model' -import { InjectModel } from '~/transformers/model.transformer' + +import { MasterLostException } from '~/common/exceptions/master-lost.exception' import { RedisKeys } from '~/constants/cache.constant' import { CacheService } from '~/processors/cache/cache.service' +import { InjectModel } from '~/transformers/model.transformer' import { getAvatar, sleep } from '~/utils' import { getRedisKey } from '~/utils/redis.util' +import { AuthService } from '../auth/auth.service' +import { UserDocument, UserModel } from './user.model' + @Injectable() export class UserService { private Logger = new Logger(UserService.name) @@ -97,6 +103,11 @@ export class UserService { const currentUser = await this.userModel .findById(_id) .select('+password +apiToken') + + if (!currentUser) { + throw new MasterLostException() + } + // 1. 验证新旧密码是否一致 const isSamePassword = compareSync(password, currentUser.password) if (isSamePassword) { @@ -119,6 +130,9 @@ export class UserService { */ async recordFootstep(ip: string): Promise> { const master = await this.userModel.findOne() + if (!master) { + throw new MasterLostException() + } const PrevFootstep = { lastLoginTime: master.lastLoginTime || new Date(1586090559569), lastLoginIp: master.lastLoginIp || null, @@ -138,7 +152,7 @@ export class UserService { }) this.Logger.warn(`主人已登录, IP: ${ip}`) - return PrevFootstep + return PrevFootstep as any } // TODO 获取最近登陆次数 时间 从 Redis 取 diff --git a/src/processors/cache/cache.config.service.ts b/src/processors/cache/cache.config.service.ts index 98500adf..c0db3e27 100755 --- a/src/processors/cache/cache.config.service.ts +++ b/src/processors/cache/cache.config.service.ts @@ -4,14 +4,15 @@ * @module processor/cache/config.service * @author Surmon */ +import redisStore from 'cache-manager-ioredis' +import IORedis from 'ioredis' import { CacheModuleOptions, CacheOptionsFactory, Injectable, } from '@nestjs/common' -import redisStore from 'cache-manager-ioredis' -import IORedis from 'ioredis' + import { REDIS } from '~/app.config' @Injectable() @@ -27,7 +28,7 @@ export class CacheConfigService implements CacheOptionsFactory { } return { store: redisStore, - ttl: REDIS.ttl, + ttl: REDIS.ttl ?? undefined, // https://github.com/dabroek/node-cache-manager-redis-store/blob/master/CHANGELOG.md#breaking-changes // Any value (undefined | null) return true (cacheable) after redisStore v2.0.0 is_cacheable_value: () => true, diff --git a/src/processors/cache/cache.service.ts b/src/processors/cache/cache.service.ts index 629cd4cf..5abd50a1 100755 --- a/src/processors/cache/cache.service.ts +++ b/src/processors/cache/cache.service.ts @@ -1,14 +1,17 @@ -import { CACHE_MANAGER, Inject, Injectable, Logger } from '@nestjs/common' import { Cache } from 'cache-manager' import { Redis } from 'ioredis' -import type { RedisSubPub } from '../../utils/redis-subpub.util' + +import { CACHE_MANAGER, Inject, Injectable, Logger } from '@nestjs/common' + import { getRedisKey } from '~/utils/redis.util' +import type { RedisSubPub } from '../../utils/redis-subpub.util' + // Cache 客户端管理器 // 获取器 export type TCacheKey = string -export type TCacheResult = Promise +export type TCacheResult = Promise /** * @class CacheService diff --git a/src/processors/gateway/admin/auth.gateway.ts b/src/processors/gateway/admin/auth.gateway.ts index 51fbe65a..2d2a842b 100644 --- a/src/processors/gateway/admin/auth.gateway.ts +++ b/src/processors/gateway/admin/auth.gateway.ts @@ -1,3 +1,5 @@ +import { Namespace, Socket } from 'socket.io' + import { OnEvent } from '@nestjs/event-emitter' import { JwtService } from '@nestjs/jwt' import { @@ -5,12 +7,13 @@ import { OnGatewayDisconnect, WebSocketServer, } from '@nestjs/websockets' -import { Namespace, Socket } from 'socket.io' -import { BaseGateway } from '../base.gateway' -import { EventTypes } from '../events.types' + import { EventBusEvents } from '~/constants/event.constant' import { AuthService } from '~/modules/auth/auth.service' +import { BaseGateway } from '../base.gateway' +import { EventTypes } from '../events.types' + export abstract class AuthGateway extends BaseGateway implements OnGatewayConnection, OnGatewayDisconnect @@ -56,7 +59,9 @@ export abstract class AuthGateway async handleConnection(client: Socket) { const token = client.handshake.query.token || client.handshake.headers['authorization'] - + if (!token) { + return this.authFailed(client) + } if (!(await this.authToken(token as string))) { return this.authFailed(client) } @@ -76,7 +81,9 @@ export abstract class AuthGateway handleTokenExpired(token: string) { const server = this.namespace.server const sid = this.tokenSocketIdMap.get(token) - + if (!sid) { + return false + } const socket = server.of('/admin').sockets.get(sid) if (socket) { socket.disconnect() diff --git a/src/processors/gateway/web/events.gateway.ts b/src/processors/gateway/web/events.gateway.ts index e5f90398..045c2677 100644 --- a/src/processors/gateway/web/events.gateway.ts +++ b/src/processors/gateway/web/events.gateway.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { plainToClass } from 'class-transformer' +import { validate } from 'class-validator' +import SocketIO from 'socket.io' + import { ConnectedSocket, GatewayMetadata, @@ -9,17 +14,16 @@ import { WebSocketServer, } from '@nestjs/websockets' import { Emitter } from '@socket.io/redis-emitter' -import { plainToClass } from 'class-transformer' -import { validate } from 'class-validator' -import SocketIO from 'socket.io' -import { BaseGateway } from '../base.gateway' -import { EventTypes } from '../events.types' -import { DanmakuDto } from './dtos/danmaku.dto' + import { RedisKeys } from '~/constants/cache.constant' import { CacheService } from '~/processors/cache/cache.service' import { getRedisKey } from '~/utils/redis.util' import { getShortDate } from '~/utils/time.util' +import { BaseGateway } from '../base.gateway' +import { EventTypes } from '../events.types' +import { DanmakuDto } from './dtos/danmaku.dto' + @WebSocketGateway({ namespace: 'web', }) @@ -72,14 +76,15 @@ export class WebEventsGateway +(await redisClient.hget( getRedisKey(RedisKeys.MaxOnlineCount), dateFormat, - )) || 0 + ))! || 0 await redisClient.hset( getRedisKey(RedisKeys.MaxOnlineCount), dateFormat, Math.max(maxOnlineCount, await this.getcurrentClientCount()), ) const key = getRedisKey(RedisKeys.MaxOnlineCount, 'total') - const totalCount = +(await redisClient.hget(key, dateFormat)) || 0 + + const totalCount = +(await redisClient.hget(key, dateFormat))! || 0 await redisClient.hset(key, dateFormat, totalCount + 1) }) diff --git a/src/processors/helper/helper.cron.service.ts b/src/processors/helper/helper.cron.service.ts index dc7054b5..3e1295ed 100644 --- a/src/processors/helper/helper.cron.service.ts +++ b/src/processors/helper/helper.cron.service.ts @@ -1,16 +1,15 @@ import cluster from 'cluster' +import COS from 'cos-nodejs-sdk-v5' +import dayjs from 'dayjs' import { existsSync } from 'fs' import { readdir, rm } from 'fs/promises' +import mkdirp from 'mkdirp' import { join } from 'path' + import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common' import { OnEvent } from '@nestjs/event-emitter' import { Cron, CronExpression } from '@nestjs/schedule' -import COS from 'cos-nodejs-sdk-v5' -import dayjs from 'dayjs' -import mkdirp from 'mkdirp' -import { CacheService } from '../cache/cache.service' -import { HttpService } from './helper.http.service' -import { InjectModel } from '~/transformers/model.transformer' + import { isMainCluster } from '~/app.config' import { CronDescription } from '~/common/decorator/cron-description.decorator' import { RedisKeys } from '~/constants/cache.constant' @@ -24,8 +23,12 @@ import { NoteService } from '~/modules/note/note.service' import { PageService } from '~/modules/page/page.service' import { PostService } from '~/modules/post/post.service' import { SearchService } from '~/modules/search/search.service' +import { InjectModel } from '~/transformers/model.transformer' import { getRedisKey } from '~/utils/redis.util' +import { CacheService } from '../cache/cache.service' +import { HttpService } from './helper.http.service' + @Injectable() export class CronService { private logger: Logger @@ -65,7 +68,7 @@ export class CronService { } const originMethod = this[name] this[name] = (...args) => { - if (cluster.worker.id === 1 || isMainCluster) { + if (cluster.worker?.id === 1 || isMainCluster) { originMethod.call(this, ...args) } } diff --git a/src/processors/helper/helper.email.service.ts b/src/processors/helper/helper.email.service.ts index 6f3dd604..1a3e5772 100644 --- a/src/processors/helper/helper.email.service.ts +++ b/src/processors/helper/helper.email.service.ts @@ -1,14 +1,20 @@ +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ + +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import cluster from 'cluster' -import { Injectable, Logger } from '@nestjs/common' -import { OnEvent } from '@nestjs/event-emitter' import { render } from 'ejs' import { createTransport } from 'nodemailer' -import { CacheService } from '../cache/cache.service' -import { AssetService } from './helper.asset.service' + +import { Injectable, Logger } from '@nestjs/common' +import { OnEvent } from '@nestjs/event-emitter' + import { EventBusEvents } from '~/constants/event.constant' import { ConfigsService } from '~/modules/configs/configs.service' import { LinkModel } from '~/modules/link/link.model' +import { CacheService } from '../cache/cache.service' +import { AssetService } from './helper.asset.service' + export enum ReplyMailType { Owner = 'owner', Guest = 'guest', @@ -118,9 +124,10 @@ export class EmailService { this.logger.error(message) return j(message) } + // @ts-ignore r({ host: options?.host, - port: +options?.port || 465, + port: +options?.port! || 465, auth: { user, pass }, } as const) }) @@ -203,6 +210,7 @@ export class EmailService { }, } if (isDev) { + // @ts-ignore delete options.html Object.assign(options, { source }) this.logger.log(options) @@ -219,6 +227,7 @@ export class EmailService { }, } if (isDev) { + // @ts-ignore delete options.html Object.assign(options, { source }) this.logger.log(options) diff --git a/src/processors/helper/helper.image.service.ts b/src/processors/helper/helper.image.service.ts index fdb04e97..51c620fa 100644 --- a/src/processors/helper/helper.image.service.ts +++ b/src/processors/helper/helper.image.service.ts @@ -1,11 +1,18 @@ -import { Injectable, Logger } from '@nestjs/common' -import { ReturnModelType } from '@typegoose/typegoose' import imageSize from 'image-size' -import { HttpService } from './helper.http.service' + +import { + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common' +import { ReturnModelType } from '@typegoose/typegoose' + import { ConfigsService } from '~/modules/configs/configs.service' import { TextImageRecordType, WriteBaseModel } from '~/shared/model/base.model' import { getAverageRGB, pickImagesFromMarkdown } from '~/utils/pic.util' +import { HttpService } from './helper.http.service' + @Injectable() export class ImageService { private logger: Logger @@ -22,6 +29,11 @@ export class ImageService { ) { const model = _model as any as ReturnModelType const document = await model.findById(id).lean() + if (!document) { + throw new InternalServerErrorException( + `document not found, can not record image dimensions`, + ) + } const { text } = document const newImages = pickImagesFromMarkdown(text) @@ -81,7 +93,7 @@ export class ImageService { await model.updateOne( { _id: id }, // 过滤多余的 - { images: result.filter(({ src }) => newImages.includes(src)) }, + { images: result.filter(({ src }) => newImages.includes(src!)) }, ) } diff --git a/src/processors/logger/logger.service.ts b/src/processors/logger/logger.service.ts index 337f21d2..dc4dbf4e 100644 --- a/src/processors/logger/logger.service.ts +++ b/src/processors/logger/logger.service.ts @@ -1,10 +1,11 @@ /* eslint-disable prefer-rest-params */ import cluster from 'cluster' import { performance } from 'perf_hooks' + import { ConsoleLogger, ConsoleLoggerOptions } from '@nestjs/common' export class MyLogger extends ConsoleLogger { - constructor(context?: string, options?: ConsoleLoggerOptions) { + constructor(context: string, options: ConsoleLoggerOptions) { super(context, options) } private _getColorByLogLevel(logLevel: string) { @@ -71,7 +72,7 @@ export class MyLogger extends ConsoleLogger { const diff = this._updateAndGetTimestampDiff() const workerPrefix = cluster.isWorker - ? chalk.hex('#fab1a0')(`*Worker - ${cluster.worker.id}*`) + ? chalk.hex('#fab1a0')(`*Worker - ${cluster!.worker!.id}*`) : '' if (context && !argv.length) { print(`${workerPrefix} [${chalk.yellow(context)}] `, formatMessage, diff) diff --git a/src/shared/dto/pager.dto.ts b/src/shared/dto/pager.dto.ts index 3456f8c3..c5abe780 100644 --- a/src/shared/dto/pager.dto.ts +++ b/src/shared/dto/pager.dto.ts @@ -1,4 +1,3 @@ -import { ApiProperty } from '@nestjs/swagger' import { Expose, Transform } from 'class-transformer' import { IsInt, @@ -11,6 +10,8 @@ import { ValidateIf, } from 'class-validator' +import { ApiProperty } from '@nestjs/swagger' + class DbQueryDto { @IsOptional() db_query?: any @@ -24,7 +25,7 @@ export class PagerDto extends DbQueryDto { toClassOnly: true, }) @ApiProperty({ example: 10 }) - size?: number + size: number @Transform(({ value: val }) => (val ? parseInt(val) : 1), { toClassOnly: true, @@ -33,7 +34,7 @@ export class PagerDto extends DbQueryDto { @IsInt() @Expose() @ApiProperty({ example: 1 }) - page?: number + page: number @IsOptional() @IsString() diff --git a/src/shared/model/base.model.ts b/src/shared/model/base.model.ts index 58906d5d..339838b1 100644 --- a/src/shared/model/base.model.ts +++ b/src/shared/model/base.model.ts @@ -1,5 +1,3 @@ -import { ApiHideProperty } from '@nestjs/swagger' -import { index, modelOptions, plugin, prop } from '@typegoose/typegoose' import { Type } from 'class-transformer' import { IsBoolean, @@ -15,6 +13,9 @@ import LeanId from 'mongoose-lean-id' import { default as mongooseLeanVirtuals } from 'mongoose-lean-virtuals' import Paginate from 'mongoose-paginate-v2' +import { ApiHideProperty } from '@nestjs/swagger' +import { index, modelOptions, plugin, prop } from '@typegoose/typegoose' + @plugin(mongooseLeanVirtuals) @plugin(Paginate) @plugin(LeanId) @@ -24,7 +25,7 @@ import Paginate from 'mongoose-paginate-v2' toObject: { virtuals: true }, timestamps: { createdAt: 'created', - updatedAt: null, + updatedAt: false, }, versionKey: false, }, @@ -70,7 +71,7 @@ abstract class ImageModel { @prop() @IsOptional() @IsUrl() - src: string + src?: string } export abstract class BaseCommentIndexModel extends BaseModel { @@ -105,7 +106,7 @@ export class WriteBaseModel extends BaseCommentIndexModel { @Type(() => ImageModel) images?: ImageModel[] - @prop({ default: null }) + @prop({ default: null, type: Date }) @ApiHideProperty() modified: Date | null diff --git a/src/utils/sys.util.ts b/src/utils/sys.util.ts index beb9f82d..a3221500 100644 --- a/src/utils/sys.util.ts +++ b/src/utils/sys.util.ts @@ -1,12 +1,14 @@ import { builtinModules } from 'module' -export const isBuiltinModule = (module: string, ignoreList = []) => { - // @ts-ignore - return (builtinModules || Object.keys(process.binding('natives'))) - .filter( - (x) => - !/^_|^(internal|v8|node-inspect)\/|\//.test(x) && - !ignoreList.includes(x), - ) - .includes(module) +export const isBuiltinModule = (module: string, ignoreList: string[] = []) => { + return ( + // @ts-ignore + (builtinModules || (Object.keys(process.binding('natives')) as string[])) + .filter( + (x) => + !/^_|^(internal|v8|node-inspect)\/|\//.test(x) && + !ignoreList.includes(x), + ) + .includes(module) + ) } diff --git a/src/utils/tool.util.ts b/src/utils/tool.util.ts index b18c826a..ff1140da 100644 --- a/src/utils/tool.util.ts +++ b/src/utils/tool.util.ts @@ -1,10 +1,10 @@ -import { join } from 'path' import { isObject } from 'lodash' +import { join } from 'path' export const md5 = (text: string) => require('crypto').createHash('md5').update(text).digest('hex') -export function getAvatar(mail: string) { +export function getAvatar(mail: string | undefined) { if (!mail) { return '' } diff --git a/test/tsconfig.json b/test/tsconfig.json index 6aaa2013..d5e910dc 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -6,6 +6,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "esModuleInterop": true, + "strictNullChecks": false, "allowSyntheticDefaultImports": true, "target": "ES2019", "sourceMap": true, diff --git a/tsconfig.json b/tsconfig.json index 8647b4dc..d17ba4dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "module": "CommonJS", "declaration": true, "removeComments": true, + "strictNullChecks": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "esModuleInterop": true,