@@ -12,7 +12,7 @@ export const ApiController: (
|
||||
}
|
||||
|
||||
const transformPath = (path: string) =>
|
||||
`${apiRoutePrefix}/${path.replace(/\//, '')}`
|
||||
`${apiRoutePrefix}/${path.replace(/^\/*/, '')}`
|
||||
|
||||
if (typeof controller === 'string') {
|
||||
return Controller(transformPath(controller), ...args)
|
||||
|
||||
@@ -6,11 +6,13 @@ import { BadRequestException, Get, Query, Sse } from '@nestjs/common'
|
||||
import { ApiController } from '~/common/decorator/api-controller.decorator'
|
||||
import { Auth } from '~/common/decorator/auth.decorator'
|
||||
import { HTTPDecorators } from '~/common/decorator/http.decorator'
|
||||
import { ApiName } from '~/common/decorator/openapi.decorator'
|
||||
import { DATA_DIR } from '~/constants/path.constant'
|
||||
import { installPKG } from '~/utils'
|
||||
|
||||
@ApiController('dependencies')
|
||||
@Auth()
|
||||
@ApiName
|
||||
export class DependencyController {
|
||||
constructor() {}
|
||||
|
||||
|
||||
@@ -1,247 +1,35 @@
|
||||
import { FastifyReply } from 'fastify'
|
||||
import { isFunction, isString } from 'lodash'
|
||||
import { resolve } from 'path'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
Res,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common'
|
||||
import { Reflector } from '@nestjs/core'
|
||||
import { SchedulerRegistry } from '@nestjs/schedule'
|
||||
import { Get } from '@nestjs/common'
|
||||
|
||||
import { ApiController } from '~/common/decorator/api-controller.decorator'
|
||||
import { Auth } from '~/common/decorator/auth.decorator'
|
||||
import { BanInDemo } from '~/common/decorator/demo.decorator'
|
||||
import { HttpCache } from '~/common/decorator/cache.decorator'
|
||||
import { HTTPDecorators } from '~/common/decorator/http.decorator'
|
||||
import { ApiName } from '~/common/decorator/openapi.decorator'
|
||||
import { CRON_DESCRIPTION } from '~/constants/meta.constant'
|
||||
import { LOG_DIR } from '~/constants/path.constant'
|
||||
import { SCHEDULE_CRON_OPTIONS } from '~/constants/system.constant'
|
||||
import { CronService } from '~/processors/helper/helper.cron.service'
|
||||
import { TaskQueueService } from '~/processors/helper/helper.tq.service'
|
||||
import { formatByteSize } from '~/utils'
|
||||
import { getTodayLogFilePath } from '~/utils/path.util'
|
||||
import { EmailService } from '~/processors/helper/helper.email.service'
|
||||
|
||||
import { LogQueryDto, LogTypeDto } from './health.dto'
|
||||
|
||||
@ApiController({
|
||||
path: 'health',
|
||||
})
|
||||
@Auth()
|
||||
@ApiController('health')
|
||||
@ApiName
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private schedulerRegistry: SchedulerRegistry,
|
||||
private readonly cronService: CronService,
|
||||
private readonly reflector: Reflector,
|
||||
private readonly taskQueue: TaskQueueService,
|
||||
) {}
|
||||
constructor(private readonly emailService: EmailService) {}
|
||||
|
||||
@Get('/cron')
|
||||
// 跳过 JSON 结构转换
|
||||
@Get('/')
|
||||
@HTTPDecorators.Bypass
|
||||
async getAllCron() {
|
||||
const cron = Object.getPrototypeOf(this.cronService)
|
||||
const keys = Object.getOwnPropertyNames(cron).slice(1)
|
||||
const map = {}
|
||||
for (const key of keys) {
|
||||
const method = cron[key]
|
||||
if (!isFunction(method)) {
|
||||
continue
|
||||
}
|
||||
const options = this.reflector.get(SCHEDULE_CRON_OPTIONS, method)
|
||||
const description = this.reflector.get(CRON_DESCRIPTION, method) || ''
|
||||
const job = this.schedulerRegistry.getCronJob(options.name)
|
||||
map[key] = {
|
||||
...options,
|
||||
description,
|
||||
lastDate: job?.lastDate() || null,
|
||||
nextDate: job?.nextDate() || null,
|
||||
status: job?.running ? 'running' : 'stopped',
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
@HttpCache({
|
||||
disable: true,
|
||||
})
|
||||
async check() {
|
||||
// TODO
|
||||
return 'OK'
|
||||
}
|
||||
|
||||
@Post('/cron/run/:name')
|
||||
@BanInDemo
|
||||
async runCron(@Param('name') name: string) {
|
||||
if (!isString(name)) {
|
||||
throw new UnprocessableEntityException('name must be string')
|
||||
}
|
||||
const cron = Object.getPrototypeOf(this.cronService)
|
||||
const keys = Object.getOwnPropertyNames(cron).slice(1)
|
||||
const hasMethod = keys.find((key) => key === name)
|
||||
if (!hasMethod) {
|
||||
throw new BadRequestException(`${name} is not a cron`)
|
||||
}
|
||||
this.taskQueue.add(name, async () =>
|
||||
this.cronService[name].call(this.cronService),
|
||||
)
|
||||
}
|
||||
|
||||
@Get('/cron/task/:name')
|
||||
async getCronTaskStatus(@Param('name') name: string) {
|
||||
if (!isString(name)) {
|
||||
throw new BadRequestException('name must be string')
|
||||
}
|
||||
const task = await this.taskQueue.get(name)
|
||||
if (!task) {
|
||||
throw new BadRequestException(`${name} is not a cron in task queue`)
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
@Get('/log/list/:type')
|
||||
async getPM2List(@Param() params: LogTypeDto) {
|
||||
const { type } = params
|
||||
let logDir: string
|
||||
|
||||
switch (type) {
|
||||
case 'native':
|
||||
logDir = LOG_DIR
|
||||
break
|
||||
case 'pm2':
|
||||
logDir = resolve(os.homedir(), '.pm2', 'logs')
|
||||
break
|
||||
}
|
||||
|
||||
if (!fs.pathExistsSync(logDir)) {
|
||||
throw new BadRequestException('log dir not exists')
|
||||
}
|
||||
const files = await fs.readdir(logDir)
|
||||
const allFile = [] as string[]
|
||||
switch (type) {
|
||||
case 'pm2':
|
||||
for (const file of files) {
|
||||
if (file.startsWith('mx-server-') && file.endsWith('.log')) {
|
||||
allFile.push(file)
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'native':
|
||||
allFile.push(...files)
|
||||
break
|
||||
}
|
||||
const res = [] as {
|
||||
size: string
|
||||
filename: string
|
||||
type: string
|
||||
index: number
|
||||
created: number
|
||||
}[]
|
||||
for (const [i, file] of Object.entries(allFile)) {
|
||||
const stat = await fs.stat(path.join(logDir, file))
|
||||
const byteSize = stat.size
|
||||
|
||||
const size = formatByteSize(byteSize)
|
||||
let index: number
|
||||
let _type: string
|
||||
|
||||
switch (type) {
|
||||
case 'pm2':
|
||||
_type = file.split('-')[2].split('.')[0]
|
||||
index = parseInt(file.split('-')[3], 10) || 0
|
||||
break
|
||||
case 'native':
|
||||
_type = 'log'
|
||||
index = +i
|
||||
break
|
||||
@Get('/email/test')
|
||||
@Auth()
|
||||
async testEmail() {
|
||||
return this.emailService.sendTestEmail().catch((err) => {
|
||||
return {
|
||||
message: err.message,
|
||||
trace: err.stack,
|
||||
}
|
||||
res.push({
|
||||
size,
|
||||
filename: file,
|
||||
index,
|
||||
type: _type,
|
||||
created: stat.ctimeMs,
|
||||
})
|
||||
}
|
||||
|
||||
return res.sort((a, b) => b.created - a.created)
|
||||
}
|
||||
|
||||
@Get('/log/:type')
|
||||
@HTTPDecorators.Bypass
|
||||
async getLog(
|
||||
@Query() query: LogQueryDto,
|
||||
@Param() params: LogTypeDto,
|
||||
@Res() reply: FastifyReply,
|
||||
) {
|
||||
const { type: logType } = params
|
||||
let stream: Readable
|
||||
switch (logType) {
|
||||
case 'pm2': {
|
||||
const { index, type = 'out', filename: __filename } = query
|
||||
const logDir = resolve(os.homedir(), '.pm2', 'logs')
|
||||
|
||||
if (!fs.pathExistsSync(logDir)) {
|
||||
throw new BadRequestException('log dir not exists')
|
||||
}
|
||||
const filename =
|
||||
__filename ?? `mx-server-${type}${index === 0 ? '' : `-${index}`}.log`
|
||||
const logPath = path.join(logDir, filename)
|
||||
if (!fs.existsSync(logPath)) {
|
||||
throw new BadRequestException('log file not exists')
|
||||
}
|
||||
|
||||
stream = fs.createReadStream(logPath, {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'native': {
|
||||
const { filename } = query
|
||||
const logDir = LOG_DIR
|
||||
if (!filename) {
|
||||
throw new UnprocessableEntityException('filename must be string')
|
||||
}
|
||||
|
||||
stream = fs.createReadStream(path.join(logDir, filename), {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
reply.type('text/plain')
|
||||
return reply.send(stream)
|
||||
}
|
||||
|
||||
@Delete('/log/:type')
|
||||
async deleteLog(@Param() params: LogTypeDto, @Query() query: LogQueryDto) {
|
||||
const { type } = params
|
||||
const { filename } = query
|
||||
|
||||
switch (type) {
|
||||
case 'native': {
|
||||
const logPath = path.join(LOG_DIR, filename)
|
||||
const todayLogFile = getTodayLogFilePath()
|
||||
|
||||
if (logPath.endsWith('error.log') || todayLogFile === logPath) {
|
||||
await fs.writeFile(logPath, '', { encoding: 'utf8', flag: 'w' })
|
||||
break
|
||||
}
|
||||
await fs.rm(logPath)
|
||||
break
|
||||
}
|
||||
case 'pm2': {
|
||||
const logDir = resolve(os.homedir(), '.pm2', 'logs')
|
||||
if (!fs.pathExistsSync(logDir)) {
|
||||
throw new BadRequestException('log dir not exists')
|
||||
}
|
||||
await fs.rm(path.join(logDir, filename))
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
|
||||
import { HealthController } from './health.controller'
|
||||
import { HealthCronController } from './sub-controller/cron.controller'
|
||||
import { HealthLogController } from './sub-controller/log.controller'
|
||||
|
||||
@Module({ controllers: [HealthController] })
|
||||
@Module({
|
||||
controllers: [HealthController, HealthCronController, HealthLogController],
|
||||
})
|
||||
export class HealthModule {}
|
||||
|
||||
90
src/modules/health/sub-controller/cron.controller.ts
Normal file
90
src/modules/health/sub-controller/cron.controller.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { isString } from 'class-validator'
|
||||
import { isFunction } from 'lodash'
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common'
|
||||
import { Reflector } from '@nestjs/core'
|
||||
import { SchedulerRegistry } from '@nestjs/schedule'
|
||||
import { SCHEDULE_CRON_OPTIONS } from '@nestjs/schedule/dist/schedule.constants'
|
||||
|
||||
import { ApiController } from '~/common/decorator/api-controller.decorator'
|
||||
import { Auth } from '~/common/decorator/auth.decorator'
|
||||
import { BanInDemo } from '~/common/decorator/demo.decorator'
|
||||
import { HTTPDecorators } from '~/common/decorator/http.decorator'
|
||||
import { ApiName } from '~/common/decorator/openapi.decorator'
|
||||
import { CRON_DESCRIPTION } from '~/constants/meta.constant'
|
||||
import { CronService } from '~/processors/helper/helper.cron.service'
|
||||
import { TaskQueueService } from '~/processors/helper/helper.tq.service'
|
||||
|
||||
@ApiController('health/cron')
|
||||
@Auth()
|
||||
@ApiName
|
||||
export class HealthCronController {
|
||||
constructor(
|
||||
private schedulerRegistry: SchedulerRegistry,
|
||||
private readonly cronService: CronService,
|
||||
private readonly reflector: Reflector,
|
||||
private readonly taskQueue: TaskQueueService,
|
||||
) {}
|
||||
@Get('/')
|
||||
// 跳过 JSON 结构转换
|
||||
@HTTPDecorators.Bypass
|
||||
async getAllCron() {
|
||||
const cron = Object.getPrototypeOf(this.cronService)
|
||||
const keys = Object.getOwnPropertyNames(cron).slice(1)
|
||||
const map = {}
|
||||
for (const key of keys) {
|
||||
const method = cron[key]
|
||||
if (!isFunction(method)) {
|
||||
continue
|
||||
}
|
||||
const options = this.reflector.get(SCHEDULE_CRON_OPTIONS, method)
|
||||
const description = this.reflector.get(CRON_DESCRIPTION, method) || ''
|
||||
const job = this.schedulerRegistry.getCronJob(options.name)
|
||||
map[key] = {
|
||||
...options,
|
||||
description,
|
||||
lastDate: job?.lastDate() || null,
|
||||
nextDate: job?.nextDate() || null,
|
||||
status: job?.running ? 'running' : 'stopped',
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
@Post('/run/:name')
|
||||
@BanInDemo
|
||||
async runCron(@Param('name') name: string) {
|
||||
if (!isString(name)) {
|
||||
throw new UnprocessableEntityException('name must be string')
|
||||
}
|
||||
const cron = Object.getPrototypeOf(this.cronService)
|
||||
const keys = Object.getOwnPropertyNames(cron).slice(1)
|
||||
const hasMethod = keys.find((key) => key === name)
|
||||
if (!hasMethod) {
|
||||
throw new BadRequestException(`${name} is not a cron`)
|
||||
}
|
||||
this.taskQueue.add(name, async () =>
|
||||
this.cronService[name].call(this.cronService),
|
||||
)
|
||||
}
|
||||
|
||||
@Get('/task/:name')
|
||||
async getCronTaskStatus(@Param('name') name: string) {
|
||||
if (!isString(name)) {
|
||||
throw new BadRequestException('name must be string')
|
||||
}
|
||||
const task = await this.taskQueue.get(name)
|
||||
if (!task) {
|
||||
throw new BadRequestException(`${name} is not a cron in task queue`)
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
}
|
||||
171
src/modules/health/sub-controller/log.controller.ts
Normal file
171
src/modules/health/sub-controller/log.controller.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Readable } from 'form-data'
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Query,
|
||||
Res,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common'
|
||||
|
||||
import { ApiController } from '~/common/decorator/api-controller.decorator'
|
||||
import { Auth } from '~/common/decorator/auth.decorator'
|
||||
import { HTTPDecorators } from '~/common/decorator/http.decorator'
|
||||
import { ApiName } from '~/common/decorator/openapi.decorator'
|
||||
import { LOG_DIR } from '~/constants/path.constant'
|
||||
import { AdapterResponse } from '~/types/request'
|
||||
import { formatByteSize } from '~/utils'
|
||||
import { getTodayLogFilePath } from '~/utils/path.util'
|
||||
|
||||
import { LogQueryDto, LogTypeDto } from '../health.dto'
|
||||
|
||||
@ApiController('health/log')
|
||||
@Auth()
|
||||
@ApiName
|
||||
export class HealthLogController {
|
||||
@Get('/list/:type')
|
||||
async getPM2List(@Param() params: LogTypeDto) {
|
||||
const { type } = params
|
||||
let logDir: string
|
||||
|
||||
switch (type) {
|
||||
case 'native':
|
||||
logDir = LOG_DIR
|
||||
break
|
||||
case 'pm2':
|
||||
logDir = path.resolve(os.homedir(), '.pm2', 'logs')
|
||||
break
|
||||
}
|
||||
|
||||
if (!fs.pathExistsSync(logDir)) {
|
||||
throw new BadRequestException('log dir not exists')
|
||||
}
|
||||
const files = await fs.readdir(logDir)
|
||||
const allFile = [] as string[]
|
||||
switch (type) {
|
||||
case 'pm2':
|
||||
for (const file of files) {
|
||||
if (file.startsWith('mx-server-') && file.endsWith('.log')) {
|
||||
allFile.push(file)
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'native':
|
||||
allFile.push(...files)
|
||||
break
|
||||
}
|
||||
const res = [] as {
|
||||
size: string
|
||||
filename: string
|
||||
type: string
|
||||
index: number
|
||||
created: number
|
||||
}[]
|
||||
for (const [i, file] of Object.entries(allFile)) {
|
||||
const stat = await fs.stat(path.join(logDir, file))
|
||||
const byteSize = stat.size
|
||||
|
||||
const size = formatByteSize(byteSize)
|
||||
let index: number
|
||||
let _type: string
|
||||
|
||||
switch (type) {
|
||||
case 'pm2':
|
||||
_type = file.split('-')[2].split('.')[0]
|
||||
index = parseInt(file.split('-')[3], 10) || 0
|
||||
break
|
||||
case 'native':
|
||||
_type = 'log'
|
||||
index = +i
|
||||
break
|
||||
}
|
||||
res.push({
|
||||
size,
|
||||
filename: file,
|
||||
index,
|
||||
type: _type,
|
||||
created: stat.ctimeMs,
|
||||
})
|
||||
}
|
||||
|
||||
return res.sort((a, b) => b.created - a.created)
|
||||
}
|
||||
|
||||
@Get('/:type')
|
||||
@HTTPDecorators.Bypass
|
||||
async getLog(
|
||||
@Query() query: LogQueryDto,
|
||||
@Param() params: LogTypeDto,
|
||||
@Res() reply: AdapterResponse,
|
||||
) {
|
||||
const { type: logType } = params
|
||||
let stream: Readable
|
||||
switch (logType) {
|
||||
case 'pm2': {
|
||||
const { index, type = 'out', filename: __filename } = query
|
||||
const logDir = path.resolve(os.homedir(), '.pm2', 'logs')
|
||||
|
||||
if (!fs.pathExistsSync(logDir)) {
|
||||
throw new BadRequestException('log dir not exists')
|
||||
}
|
||||
const filename =
|
||||
__filename ?? `mx-server-${type}${index === 0 ? '' : `-${index}`}.log`
|
||||
const logPath = path.join(logDir, filename)
|
||||
if (!fs.existsSync(logPath)) {
|
||||
throw new BadRequestException('log file not exists')
|
||||
}
|
||||
|
||||
stream = fs.createReadStream(logPath, {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'native': {
|
||||
const { filename } = query
|
||||
const logDir = LOG_DIR
|
||||
if (!filename) {
|
||||
throw new UnprocessableEntityException('filename must be string')
|
||||
}
|
||||
|
||||
stream = fs.createReadStream(path.join(logDir, filename), {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
reply.type('text/plain')
|
||||
return reply.send(stream)
|
||||
}
|
||||
|
||||
@Delete('/:type')
|
||||
async deleteLog(@Param() params: LogTypeDto, @Query() query: LogQueryDto) {
|
||||
const { type } = params
|
||||
const { filename } = query
|
||||
|
||||
switch (type) {
|
||||
case 'native': {
|
||||
const logPath = path.join(LOG_DIR, filename)
|
||||
const todayLogFile = getTodayLogFilePath()
|
||||
|
||||
if (logPath.endsWith('error.log') || todayLogFile === logPath) {
|
||||
await fs.writeFile(logPath, '', { encoding: 'utf8', flag: 'w' })
|
||||
break
|
||||
}
|
||||
await fs.rm(logPath)
|
||||
break
|
||||
}
|
||||
case 'pm2': {
|
||||
const logDir = path.resolve(os.homedir(), '.pm2', 'logs')
|
||||
if (!fs.pathExistsSync(logDir)) {
|
||||
throw new BadRequestException('log dir not exists')
|
||||
}
|
||||
await fs.rm(path.join(logDir, filename))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { dashboard } from '~/../package.json'
|
||||
import { ApiController } from '~/common/decorator/api-controller.decorator'
|
||||
import { Auth } from '~/common/decorator/auth.decorator'
|
||||
import { HTTPDecorators } from '~/common/decorator/http.decorator'
|
||||
import { ApiName } from '~/common/decorator/openapi.decorator'
|
||||
import { LOCAL_ADMIN_ASSET_PATH } from '~/constants/path.constant'
|
||||
|
||||
import { UpdateAdminDto } from './update.dto'
|
||||
@@ -15,6 +16,7 @@ import { UpdateService } from './update.service'
|
||||
|
||||
@ApiController('update')
|
||||
@Auth()
|
||||
@ApiName
|
||||
export class UpdateController {
|
||||
constructor(private readonly service: UpdateService) {}
|
||||
|
||||
|
||||
@@ -117,8 +117,8 @@ export class EmailService {
|
||||
this.configsService.waitForConfigReady().then(({ mailOptions }) => {
|
||||
const { options, user, pass } = mailOptions
|
||||
if (!user && !pass) {
|
||||
const message = '邮件件客户端未认证'
|
||||
this.logger.error(message)
|
||||
const message = '未启动邮件通知'
|
||||
this.logger.warn(message)
|
||||
return j(message)
|
||||
}
|
||||
// @ts-ignore
|
||||
@@ -137,7 +137,7 @@ export class EmailService {
|
||||
|
||||
// 验证有效性
|
||||
private verifyClient() {
|
||||
return new Promise<boolean>((r, j) => {
|
||||
return new Promise<boolean>((r) => {
|
||||
this.instance.verify((error) => {
|
||||
if (error) {
|
||||
this.logger.error('邮件客户端初始化连接失败!')
|
||||
@@ -211,6 +211,17 @@ export class EmailService {
|
||||
} as EmailTemplateRenderProps)
|
||||
}
|
||||
|
||||
async sendTestEmail() {
|
||||
const master = await this.configsService.getMaster()
|
||||
const mailOptons = await this.configsService.get('mailOptions')
|
||||
return this.instance.sendMail({
|
||||
from: `"Mx Space" <${mailOptons.user}>`,
|
||||
to: master.mail,
|
||||
subject: '测试邮件',
|
||||
text: '这是一封测试邮件',
|
||||
})
|
||||
}
|
||||
|
||||
getInstance() {
|
||||
return this.instance
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FastifyReply, FastifyRequest } from 'fastify'
|
||||
|
||||
import { UserModel } from '~/modules/user/user.model'
|
||||
|
||||
export type Request = FastifyRequest &
|
||||
export type AdapterRequest = FastifyRequest &
|
||||
(
|
||||
| {
|
||||
isGuest: true
|
||||
@@ -16,4 +16,4 @@ export type Request = FastifyRequest &
|
||||
}
|
||||
) &
|
||||
Record<string, any>
|
||||
export type Response = FastifyReply & Record<string, any>
|
||||
export type AdapterResponse = FastifyReply & Record<string, any>
|
||||
|
||||
Reference in New Issue
Block a user