refactor: fix type guard (#404)

This commit is contained in:
2022-03-27 18:25:23 +08:00
committed by GitHub
parent b89897aafc
commit 382d04667b
53 changed files with 519 additions and 274 deletions

View File

@@ -17,6 +17,7 @@ module.exports = {
globals: {
'ts-jest': {
useESM: true,
tsConfig: './test/tsconfig.json',
},
isDev: process.env.NODE_ENV === 'development',
},

View File

@@ -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<NestModule>[],
controllers: [AppController],
providers: [
{

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
import { InternalServerErrorException } from '@nestjs/common'
export class MasterLostException extends InternalServerErrorException {
constructor() {
super('系统异常,站点主人信息已丢失')
}
}

View File

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

View File

@@ -4,7 +4,11 @@
* @module interceptor/analyze
* @author Innei <https://github.com/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()

View File

@@ -2,9 +2,10 @@
* 把 URL Search 上的 `token` 附加到 Header Authorization 上
* @author Innei <https://innei.ren>
*/
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()

View File

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

View File

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

View File

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

View File

@@ -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<UserDocument> {
async verifyPayload(payload: JwtPayload): Promise<UserDocument | null> {
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<DocumentType<TokenModel>[]> {
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)
}

View File

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

View File

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

View File

@@ -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 : 已读

View File

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

View File

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

View File

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

View File

@@ -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<IConfigKeys> = new Set()
const map: Record<string, any> = 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,

View File

@@ -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 {
<lastBuildDate>${now.toISOString()}</lastBuildDate>
<language>zh-CN</language>
<image>
<url>${xss(avatar)}</url>
<url>${xss(avatar || '')}</url>
<title>${title}</title>
<link>${xss(url)}</link>
</image>

View File

@@ -19,8 +19,7 @@ export class LogQueryDto {
index: number
@IsString()
@IsOptional()
filename?: string
filename: string
}
export class LogTypeDto {

View File

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

View File

@@ -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<string, any> = {},
): 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)
}
}

View File

@@ -1,6 +1,6 @@
export type MetaType = {
created: Date
modified: Date
created?: Date | null | undefined
modified?: Date | null | undefined
title: string
slug: string
} & Record<string, any>

View File

@@ -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 `<img src="${xss(src)}"/>`

View File

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

View File

@@ -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<T extends NoteModel>(
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)
}),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) => `<url>
<loc>${item.url}</loc>
<lastmod>${item.published_at.toISOString()}</lastmod>
<lastmod>${item.published_at?.toISOString() || 'N/A'}</lastmod>
</url>`,
)
.join('')}

View File

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

View File

@@ -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<UserModel>
@@ -58,7 +60,7 @@ export class UserModel extends BaseModel {
password!: string
@prop()
mail?: string
mail: string
@prop()
url?: string

View File

@@ -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<Record<string, Date | string>> {
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 取

View File

@@ -4,14 +4,15 @@
* @module processor/cache/config.service
* @author Surmon <https://github.com/surmon-china>
*/
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,

View File

@@ -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<T> = Promise<T>
export type TCacheResult<T> = Promise<T | undefined>
/**
* @class CacheService

View File

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

View File

@@ -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<GatewayMetadata>({
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)
})

View File

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

View File

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

View File

@@ -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<typeof WriteBaseModel>
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!)) },
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"esModuleInterop": true,
"strictNullChecks": false,
"allowSyntheticDefaultImports": true,
"target": "ES2019",
"sourceMap": true,

View File

@@ -3,6 +3,7 @@
"module": "CommonJS",
"declaration": true,
"removeComments": true,
"strictNullChecks": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"esModuleInterop": true,