diff --git a/src/common/decorator/http.decorator.ts b/src/common/decorator/http.decorator.ts index 465e25e7..04e7f8cb 100644 --- a/src/common/decorator/http.decorator.ts +++ b/src/common/decorator/http.decorator.ts @@ -10,6 +10,9 @@ import { FileUploadDto } from '~/shared/dto/file.dto' import { IdempotenceOption } from '../interceptors/idempotence.interceptor' +/** + * @description 分页转换 + */ export const Paginator: MethodDecorator = ( target, key, @@ -19,15 +22,9 @@ export const Paginator: MethodDecorator = ( } /** - * @description 跳过响应体处理 + * @description 跳过响应体处理,JSON 格式的响应体 */ -export const Bypass: MethodDecorator = ( - target, - key, - descriptor: PropertyDescriptor, -) => { - SetMetadata(SYSTEM.RESPONSE_PASSTHROUGH_METADATA, true)(descriptor.value) -} +export const Bypass = SetMetadata(SYSTEM.RESPONSE_PASSTHROUGH_METADATA, true) export declare interface FileDecoratorProps { description: string diff --git a/src/common/interceptors/json-transform.interceptor.ts b/src/common/interceptors/json-transform.interceptor.ts index 8fcaba87..a93bf573 100644 --- a/src/common/interceptors/json-transform.interceptor.ts +++ b/src/common/interceptors/json-transform.interceptor.ts @@ -21,11 +21,13 @@ export class JSONTransformInterceptor implements NestInterceptor { constructor(private readonly reflector: Reflector) {} intercept(context: ExecutionContext, next: CallHandler): Observable { const handler = context.getHandler() + const classType = context.getClass() // 跳过 bypass 装饰的请求 - const bypass = this.reflector.get( + const bypass = this.reflector.getAllAndOverride( RESPONSE_PASSTHROUGH_METADATA, - handler, + [classType, handler], ) + if (bypass) { return next.handle() } diff --git a/src/common/interceptors/response.interceptor.ts b/src/common/interceptors/response.interceptor.ts index 92dce7b6..e7e68c36 100644 --- a/src/common/interceptors/response.interceptor.ts +++ b/src/common/interceptors/response.interceptor.ts @@ -33,11 +33,12 @@ export class ResponseInterceptor implements NestInterceptor> { return next.handle() } const handler = context.getHandler() + const classType = context.getClass() // 跳过 bypass 装饰的请求 - const bypass = this.reflector.get( + const bypass = this.reflector.getAllAndOverride( SYSTEM.RESPONSE_PASSTHROUGH_METADATA, - handler, + [classType, handler], ) if (bypass) { return next.handle() diff --git a/src/modules/markdown/markdown.controller.ts b/src/modules/markdown/markdown.controller.ts index eb78571b..0766d866 100644 --- a/src/modules/markdown/markdown.controller.ts +++ b/src/modules/markdown/markdown.controller.ts @@ -1,57 +1,27 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import dayjs from 'dayjs' -import { render } from 'ejs' 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, - ForbiddenException, - Get, - Header, - Param, - Post, - Query, -} from '@nestjs/common' +import { Body, CacheTTL, Get, Header, Param, Post, Query } from '@nestjs/common' import { ApiProperty } from '@nestjs/swagger' import { ApiController } from '~/common/decorator/api-controller.decorator' 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' -import { PageModel } from '../page/page.model' -import { PostModel } from '../post/post.model' -import { - DataListDto, - ExportMarkdownQueryDto, - MarkdownPreviewDto, -} from './markdown.dto' +import { DataListDto, ExportMarkdownQueryDto } from './markdown.dto' import { MarkdownYAMLProperty } from './markdown.interface' import { MarkdownService } from './markdown.service' @ApiController('markdown') @ApiName export class MarkdownController { - constructor( - private readonly service: MarkdownService, - - private readonly configs: ConfigsService, - ) {} + constructor(private readonly service: MarkdownService) {} @Post('/import') @Auth() @@ -164,104 +134,6 @@ export class MarkdownController { return readable } - @Get('/render/:id') - @Header('content-type', 'text/html') - @HTTPDecorators.Bypass - @CacheTTL(60 * 60) - async renderArticle( - @Param() params: MongoIdDto, - @Query('theme') theme: string, - @IsMaster() isMaster: boolean, - ) { - const { id } = params - const now = performance.now() - const [ - { html: markdownMacros, document, type }, - { - url: { webUrl }, - }, - { name: username }, - ] = await Promise.all([ - this.service.renderArticle(id), - this.configs.waitForConfigReady(), - this.configs.getMaster(), - ]) - - if (!isMaster) { - if ( - ('hide' in document && document.hide) || - ('password' in document && !isNil(document.password)) - ) { - throw new ForbiddenException('该文章已隐藏或加密') - } - } - - const relativePath = (() => { - switch (type.toLowerCase()) { - case 'post': - return `/posts/${((document as PostModel).category as any).slug}/${ - (document as PostModel).slug - }` - case 'note': - return `/notes/${(document as NoteModel).nid}` - case 'page': - return `/${(document as PageModel).slug}` - } - })() - - const url = new URL(relativePath!, webUrl) - - const structure = await this.service.getRenderedMarkdownHtmlStructure( - markdownMacros, - document.title, - theme, - ) - - const html = render(await this.service.getMarkdownEjsRenderTemplate(), { - ...structure, - - title: document.title, - footer: `

本文渲染于 ${getShortDateTime( - new Date(), - )},由 marked.js 解析生成,用时 ${(performance.now() - now).toFixed( - 2, - )}ms

-

作者:${username},撰写于${dayjs(document.created).format('llll')}

-

原文地址:${decodeURIComponent( - url.toString(), - )}

- `, - }) - - return html.trim() - } - - /** - * 后台预览 Markdown 可用接口, 传入 `title` 和 `md` - */ - @Post('/render') - @HttpCache.disable - @Auth() - @HTTPDecorators.Bypass - @Header('content-type', 'text/html') - async markdownPreview( - @Body() body: MarkdownPreviewDto, - @Query('theme') theme: string, - ) { - const { md, title } = body - const html = this.service.renderMarkdownContent(md) - const structure = await this.service.getRenderedMarkdownHtmlStructure( - html, - title, - theme, - ) - return render(await this.service.getMarkdownEjsRenderTemplate(), { - ...structure, - - title: xss(title), - }).trim() - } - @Get('/render/structure/:id') @CacheTTL(60 * 60) async getRenderedMarkdownHtmlStructure(@Param() params: MongoIdDto) { diff --git a/src/modules/render/render.controller.ts b/src/modules/render/render.controller.ts index bd98924f..998b707a 100644 --- a/src/modules/render/render.controller.ts +++ b/src/modules/render/render.controller.ts @@ -1,7 +1,137 @@ -import { Controller } from '@nestjs/common' +import dayjs from 'dayjs' +import { render } from 'ejs' +import { isNil } from 'lodash' +import xss from 'xss' +import { + Body, + CacheTTL, + Controller, + ForbiddenException, + Get, + Header, + Param, + Post, + Query, +} from '@nestjs/common' + +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 { MongoIdDto } from '~/shared/dto/id.dto' +import { getShortDateTime } from '~/utils' + +import { ConfigsService } from '../configs/configs.service' +import { MarkdownPreviewDto } from '../markdown/markdown.dto' +import { MarkdownService } from '../markdown/markdown.service' +import { NoteModel } from '../note/note.model' +import { PageModel } from '../page/page.model' +import { PostModel } from '../post/post.model' @ApiName @Controller('/render') -export class RenderEjsController {} +@HTTPDecorators.Bypass +export class RenderEjsController { + constructor( + private readonly service: MarkdownService, + private readonly configs: ConfigsService, + ) {} + + @Get('/markdown/:id') + @Header('content-type', 'text/html') + @CacheTTL(60 * 60) + async renderArticle( + @Param() params: MongoIdDto, + @Query('theme') theme: string, + @IsMaster() isMaster: boolean, + ) { + const { id } = params + const now = performance.now() + const [ + { html: markdownMacros, document, type }, + { + url: { webUrl }, + }, + { name: username }, + ] = await Promise.all([ + this.service.renderArticle(id), + this.configs.waitForConfigReady(), + this.configs.getMaster(), + ]) + + if (!isMaster) { + if ( + ('hide' in document && document.hide) || + ('password' in document && !isNil(document.password)) + ) { + throw new ForbiddenException('该文章已隐藏或加密') + } + } + + const relativePath = (() => { + switch (type.toLowerCase()) { + case 'post': + return `/posts/${((document as PostModel).category as any).slug}/${ + (document as PostModel).slug + }` + case 'note': + return `/notes/${(document as NoteModel).nid}` + case 'page': + return `/${(document as PageModel).slug}` + } + })() + + const url = new URL(relativePath!, webUrl) + + const structure = await this.service.getRenderedMarkdownHtmlStructure( + markdownMacros, + document.title, + theme, + ) + + const html = render(await this.service.getMarkdownEjsRenderTemplate(), { + ...structure, + + title: document.title, + footer: `

本文渲染于 ${getShortDateTime( + new Date(), + )},由 marked.js 解析生成,用时 ${(performance.now() - now).toFixed( + 2, + )}ms

+

作者:${username},撰写于${dayjs(document.created).format('llll')}

+

原文地址:${decodeURIComponent( + url.toString(), + )}

+ `, + }) + + return html.trim() + } + + /** + * 后台预览 Markdown 可用接口, 传入 `title` 和 `md` + */ + @Post('/markdown') + @HttpCache.disable + @Auth() + @Header('content-type', 'text/html') + async markdownPreview( + @Body() body: MarkdownPreviewDto, + @Query('theme') theme: string, + ) { + const { md, title } = body + const html = this.service.renderMarkdownContent(md) + const structure = await this.service.getRenderedMarkdownHtmlStructure( + html, + title, + theme, + ) + return render(await this.service.getMarkdownEjsRenderTemplate(), { + ...structure, + + title: xss(title), + }).trim() + } +} diff --git a/src/modules/render/render.module.ts b/src/modules/render/render.module.ts index 76ad12fb..bbdc2248 100644 --- a/src/modules/render/render.module.ts +++ b/src/modules/render/render.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common' +import { MarkdownModule } from '../markdown/markdown.module' import { RenderEjsController } from './render.controller' @Module({ controllers: [RenderEjsController], + imports: [MarkdownModule], }) export class RenderEjsModule {}