dev/snippet (#130)
This commit is contained in:
@@ -12,6 +12,7 @@ import { HttpCacheInterceptor } from './common/interceptors/cache.interceptor'
|
||||
import { CountingInterceptor } from './common/interceptors/counting.interceptor'
|
||||
import { JSONSerializeInterceptor } from './common/interceptors/json-serialize.interceptor'
|
||||
import { ResponseInterceptor } from './common/interceptors/response.interceptor'
|
||||
import { AttachHeaderTokenMiddleware } from './common/middlewares/attach-auth.middleware'
|
||||
import {
|
||||
DATA_DIR,
|
||||
LOGGER_DIR,
|
||||
@@ -40,6 +41,7 @@ import { RecentlyModule } from './modules/recently/recently.module'
|
||||
import { SayModule } from './modules/say/say.module'
|
||||
import { SearchModule } from './modules/search/search.module'
|
||||
import { SitemapModule } from './modules/sitemap/sitemap.module'
|
||||
import { SnippetModule } from './modules/snippet/snippet.module'
|
||||
import { ToolModule } from './modules/tool/tool.module'
|
||||
import { UserModule } from './modules/user/user.module'
|
||||
import { CacheModule } from './processors/cache/cache.module'
|
||||
@@ -103,6 +105,7 @@ mkdirs()
|
||||
GatewayModule,
|
||||
HelperModule,
|
||||
LoggerModule,
|
||||
SnippetModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
@@ -141,11 +144,6 @@ mkdirs()
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
// FIXME: nestjs 8 middleware bug
|
||||
// consumer
|
||||
// .apply(AnalyzeMiddleware)
|
||||
// .forRoutes({ path: '(.*?)', method: RequestMethod.GET })
|
||||
// .apply(SkipBrowserDefaultRequestMiddleware, SecurityMiddleware)
|
||||
// .forRoutes({ path: '(.*?)', method: RequestMethod.ALL })
|
||||
consumer.apply(AttachHeaderTokenMiddleware).forRoutes('*')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
|
||||
import { AuthGuard as _AuthGuard } from '@nestjs/passport'
|
||||
import { mockUser1 } from '~/mock/user.mock'
|
||||
import { isTest } from '~/utils/index.util'
|
||||
import { getNestExecutionContextRequest } from '~/utils/nest.util'
|
||||
|
||||
/**
|
||||
@@ -15,6 +17,12 @@ export class JWTAuthGuard extends _AuthGuard('jwt') implements CanActivate {
|
||||
return true
|
||||
}
|
||||
|
||||
/// for e2e-test mock user
|
||||
if (isTest) {
|
||||
request.user = { ...mockUser1 }
|
||||
return true
|
||||
}
|
||||
|
||||
return super.canActivate(context)
|
||||
}
|
||||
|
||||
|
||||
23
src/common/middlewares/attach-auth.middleware.ts
Normal file
23
src/common/middlewares/attach-auth.middleware.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 把 URL Search 上的 `token` 附加到 Header Authorization 上
|
||||
* @author Innei <https://innei.ren>
|
||||
*/
|
||||
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common'
|
||||
import { IncomingMessage, ServerResponse } from 'http'
|
||||
import { parseRelativeUrl } from '~/utils/ip.util'
|
||||
|
||||
@Injectable()
|
||||
export class AttachHeaderTokenMiddleware implements NestMiddleware {
|
||||
async use(req: IncomingMessage, res: ServerResponse, next: () => void) {
|
||||
// @ts-ignore
|
||||
const url = req.originalUrl?.replace(/^\/api(\/v\d)?/, '')
|
||||
const parser = parseRelativeUrl(url)
|
||||
|
||||
if (parser.searchParams.get('token')) {
|
||||
req.headers.authorization = parser.searchParams.get('token')
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
21
src/mock/user.mock.ts
Normal file
21
src/mock/user.mock.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { UserModel } from '~/modules/user/user.model'
|
||||
|
||||
export const mockUser1: UserModel = {
|
||||
id: '1',
|
||||
name: 'John Doe',
|
||||
mail: 'example@ee.com',
|
||||
password: '**********',
|
||||
authCode: '*****',
|
||||
username: 'johndoe',
|
||||
created: new Date('2021/1/1 10:00:11'),
|
||||
}
|
||||
|
||||
export const mockUser2: UserModel = {
|
||||
id: '2',
|
||||
name: 'Shawn Carter',
|
||||
mail: 'example@ee.com',
|
||||
password: '**********',
|
||||
authCode: '*****',
|
||||
username: 'shawn',
|
||||
created: new Date('2020/10/10 19:22:22'),
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types'
|
||||
import { ApiProperty } from '@nestjs/swagger'
|
||||
import { modelOptions, prop } from '@typegoose/typegoose'
|
||||
import { Transform } from 'class-transformer'
|
||||
import {
|
||||
@@ -24,20 +23,17 @@ export enum PageType {
|
||||
},
|
||||
})
|
||||
export class PageModel extends WriteBaseModel {
|
||||
@ApiProperty({ description: 'Slug', required: true })
|
||||
@prop({ trim: 1, index: true, required: true, unique: true })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
slug!: string
|
||||
|
||||
@ApiProperty({ description: 'SubTitle', required: false })
|
||||
@prop({ trim: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsNilOrString()
|
||||
subtitle?: string | null
|
||||
|
||||
@ApiProperty({ description: 'Order', required: false })
|
||||
@prop({ default: 1 })
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@@ -45,10 +41,6 @@ export class PageModel extends WriteBaseModel {
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
order!: number
|
||||
|
||||
@ApiProperty({
|
||||
enum: PageType,
|
||||
required: false,
|
||||
})
|
||||
@prop({ default: 'md' })
|
||||
@IsEnum(PageType)
|
||||
@IsOptional()
|
||||
|
||||
95
src/modules/snippet/snippet.controller.ts
Normal file
95
src/modules/snippet/snippet.controller.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
} from '@nestjs/common'
|
||||
import { Auth } from '~/common/decorator/auth.decorator'
|
||||
import { ApiName } from '~/common/decorator/openapi.decorator'
|
||||
import { IsMaster } from '~/common/decorator/role.decorator'
|
||||
import { MongoIdDto } from '~/shared/dto/id.dto'
|
||||
import { PagerDto } from '~/shared/dto/pager.dto'
|
||||
import { transformDataToPaginate } from '~/utils/transfrom.util'
|
||||
import { SnippetModel } from './snippet.model'
|
||||
import { SnippetService } from './snippet.service'
|
||||
|
||||
@ApiName
|
||||
@Controller('snippets')
|
||||
export class SnippetController {
|
||||
constructor(private readonly snippetService: SnippetService) {}
|
||||
|
||||
@Get('/')
|
||||
@Auth()
|
||||
async getList(@Query() query: PagerDto) {
|
||||
const { page, size, select = '' } = query
|
||||
|
||||
return transformDataToPaginate(
|
||||
await this.snippetService.model.paginate(
|
||||
{},
|
||||
{ page, limit: size, select },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
@Auth()
|
||||
async create(@Body() body: SnippetModel) {
|
||||
return await this.snippetService.create(body)
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
async getSnippetById(
|
||||
@Param() param: MongoIdDto,
|
||||
@IsMaster() isMaster: boolean,
|
||||
) {
|
||||
const { id } = param
|
||||
const snippet = await this.snippetService.getSnippetById(id)
|
||||
if (snippet.private && !isMaster) {
|
||||
throw new ForbiddenException('snippet is private')
|
||||
}
|
||||
return snippet
|
||||
}
|
||||
|
||||
@Get('/:reference/:name')
|
||||
async getSnippetByName(
|
||||
@Param('name') name: string,
|
||||
@Param('reference') reference: string,
|
||||
@IsMaster() isMaster: boolean,
|
||||
) {
|
||||
if (typeof name !== 'string') {
|
||||
throw new ForbiddenException('name should be string')
|
||||
}
|
||||
|
||||
if (typeof reference !== 'string') {
|
||||
throw new ForbiddenException('reference should be string')
|
||||
}
|
||||
|
||||
const snippet = await this.snippetService.getSnippetByName(name, reference)
|
||||
|
||||
if (snippet.private && !isMaster) {
|
||||
throw new ForbiddenException('snippet is private')
|
||||
}
|
||||
return snippet
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Auth()
|
||||
async update(@Param() param: MongoIdDto, @Body() body: SnippetModel) {
|
||||
const { id } = param
|
||||
|
||||
await this.snippetService.update(id, body)
|
||||
return await this.snippetService.getSnippetById(id)
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Auth()
|
||||
async delete(@Param() param: MongoIdDto) {
|
||||
const { id } = param
|
||||
await this.snippetService.delete(id)
|
||||
}
|
||||
}
|
||||
0
src/modules/snippet/snippet.dto.ts
Normal file
0
src/modules/snippet/snippet.dto.ts
Normal file
73
src/modules/snippet/snippet.model.ts
Normal file
73
src/modules/snippet/snippet.model.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { index, modelOptions, prop } from '@typegoose/typegoose'
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
} from 'class-validator'
|
||||
import { BaseModel } from '~/shared/model/base.model'
|
||||
|
||||
export enum SnippetType {
|
||||
JSON = 'json',
|
||||
Function = 'function',
|
||||
Text = 'text',
|
||||
}
|
||||
|
||||
@modelOptions({
|
||||
options: {
|
||||
customName: 'snippet',
|
||||
},
|
||||
schemaOptions: {
|
||||
timestamps: {
|
||||
createdAt: 'created',
|
||||
updatedAt: 'updated',
|
||||
},
|
||||
},
|
||||
})
|
||||
@index({ name: 1, reference: 1 })
|
||||
@index({ type: 1 })
|
||||
export class SnippetModel extends BaseModel {
|
||||
@prop({ default: SnippetType['JSON'] })
|
||||
@IsEnum(SnippetType)
|
||||
type: SnippetType
|
||||
|
||||
@prop({ default: false })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
private: boolean
|
||||
|
||||
@prop({ require: true })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
raw: string
|
||||
|
||||
@prop({ require: true, trim: true })
|
||||
@IsNotEmpty()
|
||||
@Matches(/^[a-zA-Z0-9_]{1,30}$/, {
|
||||
message: 'name 只能使用英文字母和数字下划线且不超过 30 个字符',
|
||||
})
|
||||
name: string
|
||||
|
||||
// 适用于
|
||||
@prop({ default: 'root' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
reference: string
|
||||
|
||||
// 注释
|
||||
@prop({})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
comment?: string
|
||||
|
||||
// 类型注释
|
||||
@prop({ maxlength: 20 })
|
||||
@MaxLength(20)
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
metatype?: string
|
||||
}
|
||||
13
src/modules/snippet/snippet.module.ts
Normal file
13
src/modules/snippet/snippet.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 数据配置区块
|
||||
*/
|
||||
import { Module } from '@nestjs/common'
|
||||
import { SnippetController } from './snippet.controller'
|
||||
import { SnippetService } from './snippet.service'
|
||||
|
||||
@Module({
|
||||
controllers: [SnippetController],
|
||||
exports: [SnippetService],
|
||||
providers: [SnippetService],
|
||||
})
|
||||
export class SnippetModule {}
|
||||
44
src/modules/snippet/snippet.readme.md
Normal file
44
src/modules/snippet/snippet.readme.md
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
# 数据区块 (Snippet)
|
||||
|
||||
拟定于存储一些动态扩展配置. 一期实现存储 JSON 和 plain text 的区块
|
||||
|
||||
二期实现低配云函数
|
||||
|
||||
拟定:
|
||||
|
||||
JSON:
|
||||
|
||||
```
|
||||
input: json `{"foo":"bar"}`
|
||||
|
||||
|
||||
output:
|
||||
|
||||
{
|
||||
metatype: null,
|
||||
type: 'json',
|
||||
data: {
|
||||
foo: 'bar'
|
||||
},
|
||||
raw: '{"foo":"bar"}',
|
||||
id: xx
|
||||
}
|
||||
```
|
||||
|
||||
serverless function
|
||||
|
||||
```
|
||||
input: `module.exports = ctx => { return 'foo' }`
|
||||
|
||||
output:
|
||||
|
||||
{
|
||||
raw: `module.exports = ctx => { return 'foo' }`,
|
||||
data: 'foo',
|
||||
....
|
||||
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
99
src/modules/snippet/snippet.service.ts
Normal file
99
src/modules/snippet/snippet.service.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common'
|
||||
import { InjectModel } from 'nestjs-typegoose'
|
||||
import { SnippetModel, SnippetType } from './snippet.model'
|
||||
|
||||
@Injectable()
|
||||
export class SnippetService {
|
||||
constructor(
|
||||
@InjectModel(SnippetModel)
|
||||
private readonly snippetModel: MongooseModel<SnippetModel>,
|
||||
) {}
|
||||
|
||||
get model() {
|
||||
return this.snippetModel
|
||||
}
|
||||
|
||||
async create(model: SnippetModel) {
|
||||
const isExist = await this.model.countDocuments({
|
||||
name: model.name,
|
||||
reference: model.reference || 'root',
|
||||
})
|
||||
|
||||
if (isExist) {
|
||||
throw new BadRequestException('snippet is exist')
|
||||
}
|
||||
// 验证正确类型
|
||||
await this.validateType(model)
|
||||
return await this.model.create({ ...model, created: new Date() })
|
||||
}
|
||||
|
||||
async update(id: string, model: SnippetModel) {
|
||||
await this.validateType(model)
|
||||
|
||||
await this.model.updateOne(
|
||||
{
|
||||
_id: id,
|
||||
},
|
||||
model,
|
||||
)
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
await this.model.deleteOne({ _id: id })
|
||||
}
|
||||
|
||||
private async validateType(model: SnippetModel) {
|
||||
switch (model.type) {
|
||||
case SnippetType.JSON: {
|
||||
const isValidJSON = JSON.parse(model.raw)
|
||||
if (!isValidJSON) {
|
||||
throw new BadRequestException('content is not valid json')
|
||||
}
|
||||
break
|
||||
}
|
||||
case SnippetType.Function:
|
||||
// TODO
|
||||
throw new BadRequestException(
|
||||
'Serverless functions are not currently supported',
|
||||
)
|
||||
|
||||
case SnippetType.Text:
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getSnippetById(id: string) {
|
||||
const doc = await this.model.findById(id).lean()
|
||||
return this.attachSnippet(doc)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param name
|
||||
* @param reference 引用类型, 可以理解为 type
|
||||
* @returns
|
||||
*/
|
||||
async getSnippetByName(name: string, reference: string) {
|
||||
const doc = await this.model.findOne({ name, reference }).lean()
|
||||
return this.attachSnippet(doc)
|
||||
}
|
||||
|
||||
async attachSnippet(model: SnippetModel) {
|
||||
switch (model.type) {
|
||||
case SnippetType.JSON: {
|
||||
Reflect.set(model, 'data', JSON.parse(model.raw))
|
||||
break
|
||||
}
|
||||
case SnippetType.Text: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
// TODO serverless function
|
||||
// async runSnippet(model: SnippetModel) {}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { PageModel } from '~/modules/page/page.model'
|
||||
import { ProjectModel } from '~/modules/project/project.model'
|
||||
import { RecentlyModel } from '~/modules/recently/recently.model'
|
||||
import { SayModel } from '~/modules/say/say.model'
|
||||
import { SnippetModel } from '~/modules/snippet/snippet.model'
|
||||
import { CategoryModel } from '../../modules/category/category.model'
|
||||
import { PostModel } from '../../modules/post/post.model'
|
||||
import { UserModel } from '../../modules/user/user.model'
|
||||
@@ -27,6 +28,7 @@ const models = TypegooseModule.forFeature([
|
||||
ProjectModel,
|
||||
RecentlyModel,
|
||||
SayModel,
|
||||
SnippetModel,
|
||||
UserModel,
|
||||
])
|
||||
@Module({
|
||||
|
||||
@@ -25,6 +25,7 @@ export class BaseModel {
|
||||
@Field(() => Date)
|
||||
created?: Date
|
||||
@Field(() => ID)
|
||||
@ApiHideProperty()
|
||||
id?: string
|
||||
|
||||
static get protectedKeys() {
|
||||
|
||||
@@ -3,8 +3,11 @@ import { isObject } from 'lodash'
|
||||
export * from './ip.util'
|
||||
export const isDev = process.env.NODE_ENV == 'development'
|
||||
|
||||
export const isTest = !!process.env.TEST
|
||||
|
||||
export const md5 = (text: string) =>
|
||||
require('crypto').createHash('md5').update(text).digest('hex')
|
||||
|
||||
export function getAvatar(mail: string) {
|
||||
if (!mail) {
|
||||
return ''
|
||||
@@ -20,33 +23,6 @@ export function hasChinese(str: string) {
|
||||
return escape(str).indexOf('%u') < 0 ? false : true
|
||||
}
|
||||
|
||||
export const escapeShell = function (cmd: string) {
|
||||
return '"' + cmd.replace(/(["\s'$`\\])/g, '\\$1') + '"'
|
||||
}
|
||||
|
||||
export function arrDifference(a1: string[], a2: string[]) {
|
||||
const a = [],
|
||||
diff = []
|
||||
|
||||
for (let i = 0; i < a1.length; i++) {
|
||||
a[a1[i]] = true
|
||||
}
|
||||
|
||||
for (let i = 0; i < a2.length; i++) {
|
||||
if (a[a2[i]]) {
|
||||
delete a[a2[i]]
|
||||
} else {
|
||||
a[a2[i]] = true
|
||||
}
|
||||
}
|
||||
|
||||
for (const k in a) {
|
||||
diff.push(k)
|
||||
}
|
||||
|
||||
return diff
|
||||
}
|
||||
|
||||
export function deleteKeys<T extends KV>(
|
||||
target: T,
|
||||
keys: (keyof T)[],
|
||||
|
||||
Reference in New Issue
Block a user