@@ -5,7 +5,6 @@ import wcmatch from 'wildcard-match'
|
||||
import type { LogLevel } from '@nestjs/common'
|
||||
import type { NestFastifyApplication } from '@nestjs/platform-fastify'
|
||||
|
||||
import { ValidationPipe } from '@nestjs/common'
|
||||
import { NestFactory } from '@nestjs/core'
|
||||
|
||||
import { CROSS_DOMAIN, DEBUG_MODE, PORT } from './app.config'
|
||||
@@ -14,6 +13,7 @@ import { fastifyApp } from './common/adapters/fastify.adapter'
|
||||
import { RedisIoAdapter } from './common/adapters/socket.adapter'
|
||||
import { SpiderGuard } from './common/guards/spider.guard'
|
||||
import { LoggingInterceptor } from './common/interceptors/logging.interceptor'
|
||||
import { ExtendedValidationPipe } from './common/pipes/validation.pipe'
|
||||
import { logger } from './global/consola.global'
|
||||
import { isMainProcess, isTest } from './global/env.global'
|
||||
import { migrateDatabase } from './migration/migrate'
|
||||
@@ -70,16 +70,7 @@ export async function bootstrap() {
|
||||
app.useGlobalInterceptors(new LoggingInterceptor())
|
||||
}
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
errorHttpStatusCode: 422,
|
||||
forbidUnknownValues: true,
|
||||
enableDebugMessages: isDev,
|
||||
stopAtFirstError: true,
|
||||
}),
|
||||
)
|
||||
app.useGlobalPipes(ExtendedValidationPipe.shared)
|
||||
app.useGlobalGuards(new SpiderGuard())
|
||||
!isTest && app.useWebSocketAdapter(new RedisIoAdapter(app))
|
||||
|
||||
|
||||
19
apps/core/src/common/pipes/validation.pipe.ts
Normal file
19
apps/core/src/common/pipes/validation.pipe.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { ValidationPipeOptions } from '@nestjs/common'
|
||||
|
||||
import { Injectable, ValidationPipe } from '@nestjs/common'
|
||||
|
||||
@Injectable()
|
||||
export class ExtendedValidationPipe extends ValidationPipe {
|
||||
public static readonly options: ValidationPipeOptions = {
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
errorHttpStatusCode: 422,
|
||||
forbidUnknownValues: true,
|
||||
enableDebugMessages: isDev,
|
||||
stopAtFirstError: true,
|
||||
}
|
||||
|
||||
public static readonly shared = new ExtendedValidationPipe(
|
||||
ExtendedValidationPipe.options,
|
||||
)
|
||||
}
|
||||
@@ -10,3 +10,5 @@ export const DB_CONNECTION_TOKEN = '__db_connection_token__'
|
||||
export const DB_MODEL_TOKEN_SUFFIX = '__db_model_token_suffix__'
|
||||
|
||||
export const SKIP_LOGGING_METADATA = '__skipLogging__'
|
||||
|
||||
export const VALIDATION_PIPE_INJECTION = '__VALIDATION_PIPE__'
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { plainToInstance } from 'class-transformer'
|
||||
import { validateSync } from 'class-validator'
|
||||
import { FastifyReply, FastifyRequest } from 'fastify'
|
||||
import type { ValidatorOptions } from 'class-validator'
|
||||
|
||||
import { Body, HttpCode, Post, Req, Res, ValidationPipe } from '@nestjs/common'
|
||||
import { Body, HttpCode, Inject, Post, Req, Res } from '@nestjs/common'
|
||||
|
||||
import { ApiController } from '~/common/decorators/api-controller.decorator'
|
||||
import { Cookies } from '~/common/decorators/cookie.decorator'
|
||||
import { RedisKeys } from '~/constants/cache.constant'
|
||||
import { ExtendedValidationPipe } from '~/common/pipes/validation.pipe'
|
||||
import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant'
|
||||
import { CountingService } from '~/processors/helper/helper.counting.service'
|
||||
import { CacheService } from '~/processors/redis/cache.service'
|
||||
import { getRedisKey } from '~/utils'
|
||||
|
||||
import { AckDto, AckEventType, AckReadPayloadDto } from './ack.dto'
|
||||
|
||||
@@ -19,15 +18,10 @@ export class AckController {
|
||||
constructor(
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly countingService: CountingService,
|
||||
@Inject(VALIDATION_PIPE_INJECTION)
|
||||
private readonly validatePipe: ExtendedValidationPipe,
|
||||
) {}
|
||||
|
||||
private validateOptions: ValidatorOptions = {
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
stopAtFirstError: true,
|
||||
}
|
||||
private validate = new ValidationPipe(this.validateOptions)
|
||||
|
||||
@Post('/')
|
||||
@HttpCode(200)
|
||||
async ack(
|
||||
@@ -42,37 +36,55 @@ export class AckController {
|
||||
switch (type) {
|
||||
case AckEventType.READ: {
|
||||
const validPayload = plainToInstance(AckReadPayloadDto, payload)
|
||||
const errors = validateSync(validPayload, this.validateOptions)
|
||||
const errors = validateSync(
|
||||
validPayload,
|
||||
ExtendedValidationPipe.options,
|
||||
)
|
||||
if (errors.length) {
|
||||
const error = this.validate.createExceptionFactory()(errors as any[])
|
||||
const error = this.validatePipe.createExceptionFactory()(
|
||||
errors as any[],
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
const { id, type } = validPayload
|
||||
|
||||
if (uuidReq) {
|
||||
const cacheKey = getRedisKey(RedisKeys.Read, `ack-${uuidReq}-${id}`)
|
||||
const cacheValue = await this.cacheService.get(cacheKey)
|
||||
if (cacheValue) {
|
||||
return res.send()
|
||||
}
|
||||
await this.cacheService.set(cacheKey, '1', 12 * 60 * 60 * 1000)
|
||||
// @ts-expect-error
|
||||
await this.countingService.updateReadCount(type, id)
|
||||
return res.send()
|
||||
}
|
||||
|
||||
const cookieKey = `ack-read-${id}`
|
||||
// @ts-expect-error
|
||||
await this.countingService.updateReadCount(type, id)
|
||||
if (cookies[cookieKey]) {
|
||||
return res.send()
|
||||
}
|
||||
|
||||
res.cookie(cookieKey, '1', {
|
||||
maxAge:
|
||||
// 12 hour
|
||||
12 * 60 * 60 * 1000,
|
||||
})
|
||||
return res.send()
|
||||
|
||||
// disable ack read limit
|
||||
// const validPayload = plainToInstance(AckReadPayloadDto, payload)
|
||||
// const errors = validateSync(validPayload, this.validateOptions)
|
||||
// if (errors.length) {
|
||||
// const error = this.validate.createExceptionFactory()(errors as any[])
|
||||
// throw error
|
||||
// }
|
||||
// const { id, type } = validPayload
|
||||
|
||||
// if (uuidReq) {
|
||||
// const cacheKey = getRedisKey(RedisKeys.Read, `ack-${uuidReq}-${id}`)
|
||||
// const cacheValue = await this.cacheService.get(cacheKey)
|
||||
// if (cacheValue) {
|
||||
// return res.send()
|
||||
// }
|
||||
// await this.cacheService.set(cacheKey, '1', 12 * 60 * 60 * 1000)
|
||||
// // @ts-expect-error
|
||||
// await this.countingService.updateReadCount(type, id)
|
||||
// return res.send()
|
||||
// }
|
||||
|
||||
// const cookieKey = `ack-read-${id}`
|
||||
// // @ts-expect-error
|
||||
// await this.countingService.updateReadCount(type, id)
|
||||
// if (cookies[cookieKey]) {
|
||||
// return res.send()
|
||||
// }
|
||||
|
||||
// res.cookie(cookieKey, '1', {
|
||||
// maxAge:
|
||||
// // 12 hour
|
||||
// 12 * 60 * 60 * 1000,
|
||||
// })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { DiscoveryModule } from '@nestjs/core'
|
||||
|
||||
import { ExtendedValidationPipe } from '~/common/pipes/validation.pipe'
|
||||
import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant'
|
||||
|
||||
import { AckController } from './ack.controller'
|
||||
|
||||
@Module({
|
||||
controllers: [AckController],
|
||||
imports: [DiscoveryModule],
|
||||
providers: [
|
||||
{
|
||||
provide: VALIDATION_PIPE_INJECTION,
|
||||
useValue: ExtendedValidationPipe.shared,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AckModule {}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ActivityController {
|
||||
const { ip } = location
|
||||
const { id, type } = body
|
||||
|
||||
await this.service.likeAndEmit(type, id, ip)
|
||||
await this.service.likeAndEmit(type.toLowerCase() as any, id, ip)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export interface ActivityLikePayload {
|
||||
id: string
|
||||
ip: string
|
||||
type: 'Note' | 'Post'
|
||||
type: ActivityLikeSupportType
|
||||
}
|
||||
|
||||
export type ActivityLikeSupportType = 'Post' | 'Note'
|
||||
export type ActivityLikeSupportType = 'post' | 'note'
|
||||
|
||||
export interface ActivityPresence {
|
||||
operationTime: number
|
||||
|
||||
@@ -12,7 +12,12 @@ import type { UpdatePresenceDto } from './dtos/presence.dto'
|
||||
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common'
|
||||
|
||||
import { ArticleTypeEnum } from '~/constants/article.constant'
|
||||
import { BusinessEvents, EventScope } from '~/constants/business-event.constant'
|
||||
import {
|
||||
NOTE_COLLECTION_NAME,
|
||||
POST_COLLECTION_NAME,
|
||||
} from '~/constants/db.constant'
|
||||
import { DatabaseService } from '~/processors/database/database.service'
|
||||
import { GatewayService } from '~/processors/gateway/gateway.service'
|
||||
import { WebEventsGateway } from '~/processors/gateway/web/events.gateway'
|
||||
@@ -143,12 +148,12 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy {
|
||||
const { type, id } = item.payload as ActivityLikePayload
|
||||
|
||||
switch (type) {
|
||||
case 'Note': {
|
||||
acc.Note.push(id)
|
||||
case 'note': {
|
||||
acc.note.push(id)
|
||||
break
|
||||
}
|
||||
case 'Post': {
|
||||
acc.Post.push(id)
|
||||
case 'post': {
|
||||
acc.post.push(id)
|
||||
|
||||
break
|
||||
}
|
||||
@@ -156,8 +161,8 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy {
|
||||
return acc
|
||||
},
|
||||
{
|
||||
Post: [],
|
||||
Note: [],
|
||||
post: [],
|
||||
note: [],
|
||||
} as Record<ActivityLikeSupportType, string[]>,
|
||||
)
|
||||
|
||||
@@ -165,8 +170,8 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy {
|
||||
ActivityLikeSupportType,
|
||||
Collection<Document>
|
||||
> = {
|
||||
Note: this.databaseService.db.collection('notes'),
|
||||
Post: this.databaseService.db.collection('posts'),
|
||||
note: this.databaseService.db.collection(NOTE_COLLECTION_NAME),
|
||||
post: this.databaseService.db.collection(POST_COLLECTION_NAME),
|
||||
}
|
||||
|
||||
const refModelData = new Map<string, any>()
|
||||
@@ -237,9 +242,17 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async likeAndEmit(type: ActivityLikeSupportType, id: string, ip: string) {
|
||||
async likeAndEmit(type: 'post' | 'note', id: string, ip: string) {
|
||||
try {
|
||||
const res = await this.countingService.updateLikeCountWithIp(type, id, ip)
|
||||
const mapping = {
|
||||
post: ArticleTypeEnum.Post,
|
||||
note: ArticleTypeEnum.Note,
|
||||
}
|
||||
const res = await this.countingService.updateLikeCountWithIp(
|
||||
mapping[type],
|
||||
id,
|
||||
ip,
|
||||
)
|
||||
if (!res) {
|
||||
throw new BadRequestException('你已经支持过啦!')
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@ import { MongoIdDto } from '~/shared/dto/id.dto'
|
||||
import { ActivityLikeSupportType } from '../activity.interface'
|
||||
|
||||
export class LikeBodyDto extends MongoIdDto {
|
||||
@IsEnum(['Post', 'Note'])
|
||||
@IsEnum(['Post', 'Note', 'note', 'post'])
|
||||
type: ActivityLikeSupportType
|
||||
}
|
||||
|
||||
@@ -100,16 +100,7 @@ export class CommentService implements OnModuleInit {
|
||||
private getModelByRefType(
|
||||
type: CollectionRefTypes,
|
||||
): ReturnModelType<typeof WriteBaseModel> {
|
||||
switch (type) {
|
||||
case CollectionRefTypes.Note:
|
||||
return this.databaseService.getModelByRefType('Note') as any
|
||||
case CollectionRefTypes.Page:
|
||||
return this.databaseService.getModelByRefType('Page') as any
|
||||
case CollectionRefTypes.Post:
|
||||
return this.databaseService.getModelByRefType('Post') as any
|
||||
case CollectionRefTypes.Recently:
|
||||
return this.databaseService.getModelByRefType('Recently') as any
|
||||
}
|
||||
return this.databaseService.getModelByRefType(type) as any
|
||||
}
|
||||
|
||||
async checkSpam(doc: CommentModel) {
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { Global, Module } from '@nestjs/common'
|
||||
|
||||
import { ExtendedValidationPipe } from '~/common/pipes/validation.pipe'
|
||||
import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant'
|
||||
|
||||
import { UserModule } from '../user/user.module'
|
||||
import { ConfigsService } from './configs.service'
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [ConfigsService],
|
||||
providers: [
|
||||
ConfigsService,
|
||||
{
|
||||
provide: VALIDATION_PIPE_INJECTION,
|
||||
useValue: ExtendedValidationPipe.shared,
|
||||
},
|
||||
],
|
||||
imports: [UserModule],
|
||||
exports: [ConfigsService],
|
||||
})
|
||||
|
||||
@@ -3,21 +3,22 @@ import { plainToInstance } from 'class-transformer'
|
||||
import { validateSync } from 'class-validator'
|
||||
import { cloneDeep, mergeWith } from 'lodash'
|
||||
import type { ClassConstructor } from 'class-transformer'
|
||||
import type { ValidatorOptions } from 'class-validator'
|
||||
|
||||
import { Clerk } from '@clerk/clerk-sdk-node'
|
||||
import {
|
||||
BadRequestException,
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
UnprocessableEntityException,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common'
|
||||
import { ReturnModelType } from '@typegoose/typegoose'
|
||||
|
||||
import { ExtendedValidationPipe } from '~/common/pipes/validation.pipe'
|
||||
import { EventScope } from '~/constants/business-event.constant'
|
||||
import { RedisKeys } from '~/constants/cache.constant'
|
||||
import { EventBusEvents } from '~/constants/event-bus.constant'
|
||||
import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant'
|
||||
import { EventManagerService } from '~/processors/helper/helper.event.service'
|
||||
import { CacheService } from '~/processors/redis/cache.service'
|
||||
import { SubPubBridgeService } from '~/processors/redis/subpub.service'
|
||||
@@ -49,6 +50,9 @@ export class ConfigsService {
|
||||
private readonly subpub: SubPubBridgeService,
|
||||
|
||||
private readonly eventManager: EventManagerService,
|
||||
|
||||
@Inject(VALIDATION_PIPE_INJECTION)
|
||||
private readonly validate: ExtendedValidationPipe,
|
||||
) {
|
||||
this.configInit().then(() => {
|
||||
this.logger.log('Config 已经加载完毕!')
|
||||
@@ -182,12 +186,6 @@ export class ConfigsService {
|
||||
return newData
|
||||
}
|
||||
|
||||
private validateOptions: ValidatorOptions = {
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
}
|
||||
private validate = new ValidationPipe(this.validateOptions)
|
||||
|
||||
async patchAndValid<T extends keyof IConfig>(
|
||||
key: T,
|
||||
value: Partial<IConfig[T]>,
|
||||
@@ -290,10 +288,11 @@ export class ConfigsService {
|
||||
const validModel = plainToInstance(dto, value)
|
||||
const errors = Array.isArray(validModel)
|
||||
? (validModel as Array<any>).reduce(
|
||||
(acc, item) => acc.concat(validateSync(item, this.validateOptions)),
|
||||
(acc, item) =>
|
||||
acc.concat(validateSync(item, ExtendedValidationPipe.options)),
|
||||
[],
|
||||
)
|
||||
: validateSync(validModel, this.validateOptions)
|
||||
: validateSync(validModel, ExtendedValidationPipe.options)
|
||||
if (errors.length > 0) {
|
||||
const error = this.validate.createExceptionFactory()(errors as any[])
|
||||
throw error
|
||||
|
||||
@@ -158,7 +158,7 @@ export class SearchService {
|
||||
const tasks = search.hits.map((hit) => {
|
||||
const { type, objectID } = hit
|
||||
|
||||
const model = this.databaseService.getModelByRefType(type as any)
|
||||
const model = this.databaseService.getModelByRefType(type as 'post')
|
||||
if (!model) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { ArticleTypeEnum } from '~/constants/article.constant'
|
||||
import type { WriteBaseModel } from '~/shared/model/write-base.model'
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common'
|
||||
import { mongoose, ReturnModelType } from '@typegoose/typegoose'
|
||||
|
||||
@@ -31,17 +34,20 @@ export class DatabaseService {
|
||||
| ReturnModelType<typeof NoteModel>
|
||||
| ReturnModelType<typeof PageModel>
|
||||
| ReturnModelType<typeof RecentlyModel>
|
||||
public getModelByRefType(type: 'Post'): ReturnModelType<typeof PostModel>
|
||||
// @ts-ignore
|
||||
public getModelByRefType(
|
||||
type: ArticleTypeEnum,
|
||||
): ReturnModelType<typeof WriteBaseModel>
|
||||
public getModelByRefType(type: 'post'): ReturnModelType<typeof PostModel>
|
||||
public getModelByRefType(
|
||||
type: CollectionRefTypes.Post,
|
||||
): ReturnModelType<typeof PostModel>
|
||||
public getModelByRefType(type: 'Note'): ReturnModelType<typeof NoteModel>
|
||||
|
||||
public getModelByRefType(type: 'note'): ReturnModelType<typeof NoteModel>
|
||||
public getModelByRefType(
|
||||
type: CollectionRefTypes.Note,
|
||||
): ReturnModelType<typeof NoteModel>
|
||||
public getModelByRefType(type: 'Page'): ReturnModelType<typeof PageModel>
|
||||
|
||||
public getModelByRefType(type: 'page'): ReturnModelType<typeof PageModel>
|
||||
public getModelByRefType(
|
||||
type: CollectionRefTypes.Page,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ArticleType } from '~/constants/article.constant'
|
||||
import type { ArticleTypeEnum } from '~/constants/article.constant'
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common'
|
||||
|
||||
@@ -30,40 +30,15 @@ export class CountingService {
|
||||
return true
|
||||
}
|
||||
|
||||
// public async updateReadCountWithIp(
|
||||
// type: keyof typeof ArticleType,
|
||||
// id: string,
|
||||
// ip: string,
|
||||
// ) {
|
||||
// if (!this.checkIdAndIp(id, ip)) {
|
||||
// return
|
||||
// }
|
||||
// const redis = this.redis.getClient()
|
||||
|
||||
// const isReadBefore = await redis.sismember(
|
||||
// getRedisKey(RedisKeys.Read, id),
|
||||
// ip,
|
||||
// )
|
||||
// if (isReadBefore) {
|
||||
// this.logger.debug(`已经增加过计数了,${id}`)
|
||||
// return
|
||||
// }
|
||||
|
||||
// const doc = await this.updateReadCount(type, id)
|
||||
|
||||
// await redis.sadd(getRedisKey(RedisKeys.Read, doc.id), ip)
|
||||
// this.logger.debug(`增加阅读计数,(${doc.title}`)
|
||||
// }
|
||||
|
||||
public async updateLikeCountWithIp(
|
||||
type: keyof typeof ArticleType,
|
||||
type: ArticleTypeEnum,
|
||||
id: string,
|
||||
ip: string,
|
||||
): Promise<boolean> {
|
||||
const redis = this.redis.getClient()
|
||||
const isLikeBefore = await this.getThisRecordIsLiked(id, ip)
|
||||
|
||||
const model = this.databaseService.getModelByRefType(type as any)
|
||||
const model = this.databaseService.getModelByRefType(type)
|
||||
const doc = await model.findById(id)
|
||||
|
||||
if (!doc) {
|
||||
@@ -81,8 +56,9 @@ export class CountingService {
|
||||
this.logger.debug(`增加喜欢计数,(${doc.title}`)
|
||||
return true
|
||||
}
|
||||
public async updateReadCount(type: keyof typeof ArticleType, id: string) {
|
||||
const model = this.databaseService.getModelByRefType(type as any)
|
||||
|
||||
public async updateReadCount(type: ArticleTypeEnum, id: string) {
|
||||
const model = this.databaseService.getModelByRefType(type)
|
||||
const doc = await model.findById(id)
|
||||
|
||||
if (!doc) throw ''
|
||||
|
||||
@@ -33,7 +33,7 @@ export class ActivityController<ResponseWrapper> implements IController {
|
||||
likeIt(type: 'Post' | 'Note', id: string) {
|
||||
return this.proxy.like.post<never>({
|
||||
data: {
|
||||
type,
|
||||
type: type.toLowerCase(),
|
||||
id,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user