feat: request context

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei
2023-12-14 22:13:51 +08:00
parent 62b7fef9f2
commit c64309446e
12 changed files with 188 additions and 46 deletions

View File

@@ -84,6 +84,7 @@
"class-transformer": "0.5.1",
"class-validator": "0.13.2",
"class-validator-jsonschema": "npm:@innei/class-validator-jsonschema@3.1.2",
"cls-hooked": "^4.2.2",
"commander": "11.1.0",
"dayjs": "1.11.10",
"ejs": "3.1.9",
@@ -134,6 +135,7 @@
"@types/babel__core": "7.20.5",
"@types/bcrypt": "5.0.2",
"@types/cache-manager": "4.0.6",
"@types/cls-hooked": "^4.3.8",
"@types/ejs": "3.1.5",
"@types/get-image-colors": "4.0.5",
"@types/js-yaml": "4.0.9",

View File

@@ -1,5 +1,10 @@
import { LoggerModule } from 'nestjs-pretty-logger'
import type { DynamicModule, NestModule, Type } from '@nestjs/common'
import type {
DynamicModule,
MiddlewareConsumer,
NestModule,
Type,
} from '@nestjs/common'
import { Module } from '@nestjs/common'
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'
@@ -15,6 +20,7 @@ import { DbQueryInterceptor } from './common/interceptors/db-query.interceptor'
import { IdempotenceInterceptor } from './common/interceptors/idempotence.interceptor'
import { JSONTransformInterceptor } from './common/interceptors/json-transform.interceptor'
import { ResponseInterceptor } from './common/interceptors/response.interceptor'
import { RequestContextMiddleware } from './common/middlewares/request-context.middleware'
import { AckModule } from './modules/ack/ack.module'
import { ActivityModule } from './modules/activity/activity.module'
import { AggregateModule } from './modules/aggregate/aggregate.module'
@@ -157,11 +163,14 @@ import { RedisModule } from './processors/redis/redis.module'
},
],
})
export class AppModule {
export class AppModule implements NestModule {
static register(isInit: boolean): DynamicModule {
return {
module: AppModule,
imports: [!isInit && InitModule].filter(Boolean) as Type<NestModule>[],
}
}
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestContextMiddleware).forRoutes('(.*)')
}
}

View File

@@ -6,7 +6,7 @@ import type { LogLevel } from '@nestjs/common'
import type { NestFastifyApplication } from '@nestjs/platform-fastify'
import { ValidationPipe } from '@nestjs/common'
import { ContextIdFactory, NestFactory } from '@nestjs/core'
import { NestFactory } from '@nestjs/core'
import { CROSS_DOMAIN, DEBUG_MODE, PORT } from './app.config'
import { AppModule } from './app.module'
@@ -14,7 +14,6 @@ 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 { AggregateByTenantContextIdStrategy } from './common/strategies/context.strategy'
import { logger } from './global/consola.global'
import { isMainProcess, isTest } from './global/env.global'
import { migrateDatabase } from './migration/migrate'
@@ -84,8 +83,6 @@ export async function bootstrap() {
app.useGlobalGuards(new SpiderGuard())
!isTest && app.useWebSocketAdapter(new RedisIoAdapter(app))
ContextIdFactory.apply(new AggregateByTenantContextIdStrategy())
await app.listen(+PORT, '0.0.0.0', async () => {
app.useLogger(app.get(Logger))
logger.info('ENV:', process.env.NODE_ENV)

View File

@@ -0,0 +1,70 @@
// @reference https://github.com/ever-co/ever-gauzy/blob/d36b4f40b1446f3c33d02e0ba00b53a83109d950/packages/core/src/core/context/request-context.ts
import * as cls from 'cls-hooked'
import type { UserDocument } from '~/modules/user/user.model'
import type { IncomingMessage, ServerResponse } from 'http'
import { UnauthorizedException } from '@nestjs/common'
type Nullable<T> = T | null
export class RequestContext {
readonly id: number
request: IncomingMessage
response: ServerResponse
constructor(request: IncomingMessage, response: ServerResponse) {
this.id = Math.random()
this.request = request
this.response = response
}
static currentRequestContext(): Nullable<RequestContext> {
const session = cls.getNamespace(RequestContext.name)
if (session && session.active) {
return session.get(RequestContext.name)
}
return null
}
static currentRequest(): Nullable<IncomingMessage> {
const requestContext = RequestContext.currentRequestContext()
if (requestContext) {
return requestContext.request
}
return null
}
static currentUser(throwError?: boolean): Nullable<UserDocument> {
const requestContext = RequestContext.currentRequestContext()
if (requestContext) {
const user: UserDocument = requestContext.request['user']
if (user) {
return user
}
}
if (throwError) {
throw new UnauthorizedException()
}
return null
}
static currentIsMaster() {
const requestContext = RequestContext.currentRequestContext()
if (requestContext) {
const isMaster = requestContext.request['isMaster']
if (isMaster) {
return isMaster
}
}
return false
}
}

View File

@@ -1,5 +1,7 @@
import { isJWT } from 'class-validator'
import type { CanActivate, ExecutionContext } from '@nestjs/common'
import type { UserModel } from '~/modules/user/user.model'
import type { FastifyBizRequest } from '~/transformers/get-req.transformer'
import { Injectable, UnauthorizedException } from '@nestjs/common'
@@ -38,8 +40,8 @@ export class AuthGuard implements CanActivate {
if (!isValid) {
throw new UnauthorizedException('令牌无效')
}
request.user = userModel
request.token = Authorization
this.attachUserAndToken(request, userModel, Authorization)
return true
}
@@ -57,12 +59,29 @@ export class AuthGuard implements CanActivate {
}
}
request.user = await this.userService.getMaster()
request.token = jwt
this.attachUserAndToken(
request,
await this.userService.getMaster(),
Authorization,
)
return true
}
getRequest(context: ExecutionContext) {
return getNestExecutionContextRequest(context)
}
attachUserAndToken(
request: FastifyBizRequest,
user: UserModel,
token: string,
) {
request.user = user
request.token = token
Object.assign(request.raw, {
user,
token,
})
}
}

View File

@@ -35,6 +35,11 @@ export class RolesGuard extends AuthGuard implements CanActivate {
request.isGuest = !isMaster
request.isMaster = isMaster
Object.assign(request.raw, {
isGuest: !isMaster,
isMaster,
})
return true
}

View File

@@ -0,0 +1,25 @@
// https://github.dev/ever-co/ever-gauzy/packages/core/src/core/context/request-context.middleware.ts
import * as cls from 'cls-hooked'
import type { NestMiddleware } from '@nestjs/common'
import type { IncomingMessage, ServerResponse } from 'http'
import { Injectable } from '@nestjs/common'
import { RequestContext } from '../contexts/request.context'
@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
use(req: IncomingMessage, res: ServerResponse, next: () => any) {
const requestContext = new RequestContext(req, res)
const session =
cls.getNamespace(RequestContext.name) ||
cls.createNamespace(RequestContext.name)
session.run(async () => {
session.set(RequestContext.name, requestContext)
next()
})
}
}

View File

@@ -1,29 +0,0 @@
import type {
ContextId,
ContextIdStrategy,
HostComponentInfo,
} from '@nestjs/core'
import type { FastifyRequest } from 'fastify'
import { ContextIdFactory } from '@nestjs/core'
const tenants = new Map<string, ContextId>()
export class AggregateByTenantContextIdStrategy implements ContextIdStrategy {
attach(contextId: ContextId, request: FastifyRequest) {
const tenantId = request.headers['x-tenant-id'] as string
let tenantSubTreeId: ContextId
if (tenants.has(tenantId)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
tenantSubTreeId = tenants.get(tenantId)!
} else {
tenantSubTreeId = ContextIdFactory.create()
tenants.set(tenantId, tenantSubTreeId)
}
// If tree is not durable, return the original "contextId" object
return (info: HostComponentInfo) =>
info.isTreeDurable ? tenantSubTreeId : contextId
}
}

View File

@@ -2,6 +2,7 @@ import dayjs from 'dayjs'
import { BadRequestException, Injectable, Logger } from '@nestjs/common'
import { RequestContext } from '~/common/contexts/request.context'
import { ConfigsService } from '~/modules/configs/configs.service'
import { deepCloneWithFunction } from '~/utils'
import { safeEval } from '~/utils/safe-eval.util'
@@ -74,9 +75,9 @@ export class TextMacroService {
// time utils
dayjs: deepCloneWithFunction(dayjs),
fromNow: (time: Date | string) => dayjs(time).fromNow(),
// onlyMe: (text: string) => {
// return this.request.isMaster ? text : ''
// },
onlyMe: (text: string) => {
return RequestContext.currentIsMaster() ? text : ''
},
// typography
center: (text: string) => {

View File

@@ -2,8 +2,12 @@ import type { ExecutionContext } from '@nestjs/common'
import type { UserModel } from '~/modules/user/user.model'
import type { FastifyRequest } from 'fastify'
export type FastifyBizRequest = FastifyRequest & { user?: UserModel } & Record<
string,
any
>
export function getNestExecutionContextRequest(
context: ExecutionContext,
): FastifyRequest & { user?: UserModel } & Record<string, any> {
): FastifyBizRequest {
return context.switchToHttp().getRequest<FastifyRequest>()
}

View File

@@ -1,10 +1,8 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { ContextIdFactory } from '@nestjs/core'
import { Test } from '@nestjs/testing'
import { AggregateByTenantContextIdStrategy } from '~/common/strategies/context.strategy'
import { ConfigsService } from '~/modules/configs/configs.service'
import { TextMacroService } from '~/processors/helper/helper.macro.service'
@@ -25,7 +23,6 @@ describe('test TextMarcoService', () => {
})
},
})
ContextIdFactory.apply(new AggregateByTenantContextIdStrategy())
const module = await moduleRef.compile()
service = await module.resolve(TextMacroService)

42
pnpm-lock.yaml generated
View File

@@ -181,6 +181,9 @@ importers:
class-validator-jsonschema:
specifier: npm:@innei/class-validator-jsonschema@3.1.2
version: /@innei/class-validator-jsonschema@3.1.2(class-transformer@0.5.1)(class-validator@0.13.2)
cls-hooked:
specifier: ^4.2.2
version: 4.2.2
commander:
specifier: 11.1.0
version: 11.1.0
@@ -333,6 +336,9 @@ importers:
'@types/cache-manager':
specifier: 4.0.6
version: 4.0.6
'@types/cls-hooked':
specifier: ^4.3.8
version: 4.3.8
'@types/ejs':
specifier: 3.1.5
version: 3.1.5
@@ -2477,6 +2483,12 @@ packages:
resolution: {integrity: sha512-gbiHvCuBS9aXkE3OEDfS69bscNLTYtbbx2TQf6WyOu+4eCH1AH1gPSiDGF2UzwkRFAbqKNsC5F0mY0xcaEHCbg==}
dev: true
/@types/cls-hooked@4.3.8:
resolution: {integrity: sha512-tf/7H883gFA6MPlWI15EQtfNZ+oPL0gLKkOlx9UHFrun1fC/FkuyNBpTKq1B5E3T4fbvjId6WifHUdSGsMMuPg==}
dependencies:
'@types/node': 20.10.4
dev: true
/@types/component-emitter@1.2.11:
resolution: {integrity: sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==}
dev: false
@@ -3422,6 +3434,13 @@ packages:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
dev: true
/async-hook-jl@1.7.6:
resolution: {integrity: sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==}
engines: {node: ^4.7 || >=6.9 || >=7.3}
dependencies:
stack-chain: 1.3.7
dev: false
/async-mutex@0.4.0:
resolution: {integrity: sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==}
requiresBuild: true
@@ -3878,6 +3897,15 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
/cls-hooked@4.2.2:
resolution: {integrity: sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==}
engines: {node: ^4.7 || >=6.9 || >=7.3 || >=8.2.1}
dependencies:
async-hook-jl: 1.7.6
emitter-listener: 1.1.2
semver: 7.5.4
dev: false
/cluster-key-slot@1.1.0:
resolution: {integrity: sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==}
engines: {node: '>=0.10.0'}
@@ -4378,6 +4406,12 @@ packages:
/electron-to-chromium@1.4.610:
resolution: {integrity: sha512-mqi2oL1mfeHYtOdCxbPQYV/PL7YrQlxbvFEZ0Ee8GbDdShimqt2/S6z2RWqysuvlwdOrQdqvE0KZrBTipAeJzg==}
/emitter-listener@1.1.2:
resolution: {integrity: sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==}
dependencies:
shimmer: 1.2.1
dev: false
/emoji-regex@10.3.0:
resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==}
dev: true
@@ -8250,6 +8284,10 @@ packages:
rechoir: 0.6.2
dev: true
/shimmer@1.2.1:
resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==}
dev: false
/side-channel@1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
@@ -8446,6 +8484,10 @@ packages:
through: 2.3.8
dev: true
/stack-chain@1.3.7:
resolution: {integrity: sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==}
dev: false
/stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
dev: true