feat: demo mode

This commit is contained in:
Innei 2022-05-25 20:48:26 +08:00 committed by
parent f3f452ed98
commit ad1b5f6df3
20 changed files with 114 additions and 9 deletions

1
.gitattributes vendored
View File

@ -1 +1,2 @@
*.paw filter=lfs diff=lfs merge=lfs -text
demo-data.zip filter=lfs diff=lfs merge=lfs -text

View File

@ -5,6 +5,9 @@ import { cwd, isDev, isTest } from './global/env.global'
export const PORT = argv.port || process.env.PORT || 2333
export const API_VERSION = 2
export const isInDemoMode = argv.demo || false
export const CROSS_DOMAIN = {
allowedOrigins: argv.allowed_origins
? argv.allowed_origins?.split?.(',')
@ -23,7 +26,7 @@ export const CROSS_DOMAIN = {
}
export const MONGO_DB = {
dbName: argv.collection_name || 'mx-space',
dbName: argv.collection_name || (isInDemoMode ? 'mx-space_demo' : 'mx-space'),
host: argv.db_host || '127.0.0.1',
port: argv.db_port || 27017,
get uri() {

View File

@ -11,6 +11,7 @@ import { ApiTags } from '@nestjs/swagger'
import { InjectModel } from '~/transformers/model.transformer'
import PKG from '../package.json'
import { isInDemoMode } from './app.config'
import { Auth } from './common/decorator/auth.decorator'
import { HttpCache } from './common/decorator/cache.decorator'
import { IpLocation, IpRecord } from './common/decorator/ip.decorator'
@ -35,7 +36,7 @@ export class AppController {
return {
name: PKG.name,
author: PKG.author,
version: isDev ? 'dev' : PKG.version,
version: isDev ? 'dev' : `${isInDemoMode ? 'demo/' : ''}${PKG.version}`,
homepage: PKG.homepage,
issues: PKG.issues,
}

View File

@ -1,6 +1,7 @@
import { MiddlewareConsumer, Module, NestModule, Type } from '@nestjs/common'
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'
import { isInDemoMode } from './app.config'
import { AppController } from './app.controller'
import { AllExceptionsFilter } from './common/filters/any-exception.filter'
import { RolesGuard } from './common/guard/roles.guard'
@ -19,6 +20,7 @@ import { CategoryModule } from './modules/category/category.module'
import { CommentModule } from './modules/comment/comment.module'
import { ConfigsModule } from './modules/configs/configs.module'
import { DebugModule } from './modules/debug/debug.module'
import { DemoModule } from './modules/demo/demo.module'
import { FeedModule } from './modules/feed/feed.module'
import { FileModule } from './modules/file/file.module'
import { HealthModule } from './modules/health/health.module'
@ -60,6 +62,7 @@ import { LoggerModule } from './processors/logger/logger.module'
CategoryModule,
CommentModule,
ConfigsModule,
isInDemoMode && DemoModule,
FeedModule,
FileModule,
HealthModule,

View File

@ -0,0 +1,13 @@
import { Observable } from 'rxjs'
import { CanActivate, UseGuards, applyDecorators } from '@nestjs/common'
import { banInDemo } from '~/utils'
class DemoGuard implements CanActivate {
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
banInDemo()
return true
}
}
export const BanInDemo = applyDecorators(UseGuards(DemoGuard))

View File

@ -0,0 +1,9 @@
import { ErrorCodeEnum } from '~/constants/error-code.constant'
import { BusinessException } from './business.exception'
export class BanInDemoExcpetion extends BusinessException {
constructor() {
super(ErrorCodeEnum.BanInDemo)
}
}

View File

@ -1,9 +1,13 @@
export enum ErrorCodeEnum {
SlugNotAvailable = 'slug_not_available',
BanInDemo = 'ban_in_demo',
}
export const ErrorCode = Object.freeze<Record<ErrorCodeEnum, [string, number]>>(
{
[ErrorCodeEnum.SlugNotAvailable]: ['slug 不可用', 400],
[ErrorCodeEnum.BanInDemo]: ['Demo 模式下此操作不可用', 400],
},
)

View File

@ -18,6 +18,7 @@ import {
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger'
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 { UploadService } from '~/processors/helper/helper.upload.service'
@ -28,6 +29,7 @@ import { BackupService } from './backup.service'
@Controller({ path: 'backups', scope: Scope.REQUEST })
@ApiName
@Auth()
@BanInDemo
export class BackupController {
constructor(
private readonly backupService: BackupService,

View File

@ -79,7 +79,8 @@ export class BackupService {
await $`mongodump -h ${MONGO_DB.host} --port ${MONGO_DB.port} -d ${MONGO_DB.dbName} --excludeCollection analyzes -o ${backupDirPath} >/dev/null 2>&1`
// 打包 DB
cd(backupDirPath)
await quiet($`zip -r backup-${dateDir} mx-space/* && rm -rf mx-space`)
await nothrow(quiet($`mv ${MONGO_DB.dbName} mx-space`))
await quiet($`zip -r backup-${dateDir} mx-space/* && rm -rf mx-space`)
// 打包数据目录
@ -204,6 +205,7 @@ export class BackupService {
this.cacheService.cleanAllRedisKey(),
this.cacheService.cleanCatch(),
])
await rm(join(dirPath, 'backup_data'), { force: true, recursive: true })
}
async rollbackTo(dirname: string) {

View File

@ -15,11 +15,12 @@ import { EventEmitter2 } from '@nestjs/event-emitter'
import { DocumentType, ReturnModelType } from '@typegoose/typegoose'
import { BeAnObject } from '@typegoose/typegoose/lib/types'
import { isInDemoMode } from '~/app.config'
import { RedisKeys } from '~/constants/cache.constant'
import { EventBusEvents } from '~/constants/event-bus.constant'
import { CacheService } from '~/processors/cache/cache.service'
import { InjectModel } from '~/transformers/model.transformer'
import { sleep } from '~/utils'
import { banInDemo, sleep } from '~/utils'
import { getRedisKey } from '~/utils/redis.util'
import * as optionDtos from '../configs/configs.dto'
@ -61,14 +62,13 @@ const generateDefaultConfig: () => IConfig = () => ({
mailOptions: {} as MailOptionsDto,
commentOptions: { antiSpam: false },
friendLinkOptions: { allowApply: true },
backupOptions: { enable: true } as BackupOptionsDto,
backupOptions: { enable: isInDemoMode ? false : true } as BackupOptionsDto,
baiduSearchOptions: { enable: false },
algoliaSearchOptions: { enable: false, apiKey: '', appId: '', indexName: '' },
adminExtra: {
enableAdminProxy: true,
title: 'おかえり~',
background:
'https://gitee.com/xun7788/my-imagination/raw/master/images/88426823_p0.jpg',
background: '',
gaodemapKey: null!,
},
terminalOptions: {
@ -209,6 +209,7 @@ export class ConfigsService {
key: T,
value: Partial<IConfig[T]>,
) {
banInDemo()
value = camelcaseKeys(value, { deep: true }) as any
switch (key) {

View File

@ -0,0 +1,27 @@
import { resolve } from 'path'
import { Module } from '@nestjs/common'
import { Cron, CronExpression } from '@nestjs/schedule'
import { AssetService } from '~/processors/helper/helper.asset.service'
import { BackupModule } from '../backup/backup.module'
import { BackupService } from '../backup/backup.service'
@Module({
imports: [BackupModule],
})
export class DemoModule {
constructor(
private readonly backupService: BackupService,
private readonly assetService: AssetService,
) {
this.reset()
}
@Cron(CronExpression.EVERY_DAY_AT_1AM)
reset() {
this.backupService.restore(
resolve(this.assetService.embedAssetPath, 'demo-data.zip'),
)
}
}

View File

@ -4,6 +4,7 @@ import { Readable } from 'stream'
import { BadRequestException, Injectable } from '@nestjs/common'
import { STATIC_FILE_DIR } from '~/constants/path.constant'
import { banInDemo } from '~/utils'
import { ConfigsService } from '../configs/configs.service'
import { FileType } from './file.type'
@ -33,6 +34,7 @@ export class FileService {
data: Readable,
encoding?: BufferEncoding,
) {
banInDemo()
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
const filePath = this.resolveFilePath(type, name)
@ -58,6 +60,7 @@ export class FileService {
}
deleteFile(type: FileType, name: string) {
banInDemo()
return fs.unlink(this.resolveFilePath(type, name)).catch(() => null)
}

View File

@ -19,6 +19,7 @@ import { Reflector } from '@nestjs/core'
import { SchedulerRegistry } from '@nestjs/schedule'
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'
@ -73,6 +74,7 @@ export class HealthController {
}
@Post('/cron/run/:name')
@BanInDemo
async runCron(@Param('name') name: string) {
if (!isString(name)) {
throw new UnprocessableEntityException('name must be string')

View File

@ -9,6 +9,7 @@ import {
UnprocessableEntityException,
} from '@nestjs/common'
import { BanInDemo } from '~/common/decorator/demo.decorator'
import { HTTPDecorators } from '~/common/decorator/http.decorator'
import { IConfig } from '~/modules/configs/configs.interface'
import { ConfigsService } from '~/modules/configs/configs.service'
@ -50,6 +51,7 @@ export class BaseOptionController {
}
@Patch('/:key')
@BanInDemo
patch(@Param() params: ConfigKeyDto, @Body() body: Record<string, any>) {
if (typeof body !== 'object') {
throw new UnprocessableEntityException('body must be object')

View File

@ -12,6 +12,7 @@ import {
WebSocketGateway,
} from '@nestjs/websockets'
import { isInDemoMode } from '~/app.config'
import { BusinessEvents } from '~/constants/business-event.constant'
import { RedisKeys } from '~/constants/cache.constant'
import { DATA_DIR } from '~/constants/path.constant'
@ -42,6 +43,17 @@ export class PTYGateway
client: Socket,
data?: { password?: string; cols: number; rows: number },
) {
if (isInDemoMode) {
client.send(
this.gatewayMessageFormat(
BusinessEvents.PTY_MESSAGE,
'PTY 在演示模式下不可用',
),
)
return
}
const password = data?.password
const terminalOptions = await this.configService.get('terminalOptions')
if (!terminalOptions.enable) {

View File

@ -11,6 +11,7 @@ import {
} from '@nestjs/common'
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 { IsMaster } from '~/common/decorator/role.decorator'
@ -74,6 +75,7 @@ export class SnippetController {
@Post('/')
@Auth()
@BanInDemo
async create(@Body() body: SnippetModel) {
return await this.snippetService.create(body)
}
@ -146,6 +148,7 @@ export class SnippetController {
@Put('/:id')
@Auth()
@BanInDemo
async update(@Param() param: MongoIdDto, @Body() body: SnippetModel) {
const { id } = param
@ -154,6 +157,7 @@ export class SnippetController {
@Delete('/:id')
@Auth()
@BanInDemo
async delete(@Param() param: MongoIdDto) {
const { id } = param
await this.snippetService.delete(id)

View File

@ -15,7 +15,7 @@ import { MasterLostException } from '~/common/exceptions/master-lost.exception'
import { RedisKeys } from '~/constants/cache.constant'
import { CacheService } from '~/processors/cache/cache.service'
import { InjectModel } from '~/transformers/model.transformer'
import { getAvatar, sleep } from '~/utils'
import { banInDemo, getAvatar, sleep } from '~/utils'
import { getRedisKey } from '~/utils/redis.util'
import { AuthService } from '../auth/auth.service'
@ -95,6 +95,7 @@ export class UserService {
* @param {Partial} data -
*/
async patchUserData(user: UserDocument, data: Partial<UserModel>) {
banInDemo()
const { password } = data
const doc = { ...data }
if (password !== undefined) {

View File

@ -10,7 +10,7 @@ import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common'
import { OnEvent } from '@nestjs/event-emitter'
import { Cron, CronExpression } from '@nestjs/schedule'
import { isMainCluster } from '~/app.config'
import { isInDemoMode, isMainCluster } from '~/app.config'
import { CronDescription } from '~/common/decorator/cron-description.decorator'
import { RedisKeys } from '~/constants/cache.constant'
import { EventBusEvents } from '~/constants/event-bus.constant'
@ -82,6 +82,9 @@ export class CronService {
@Cron(CronExpression.EVERY_DAY_AT_1AM, { name: 'backupDB' })
@CronDescription('备份 DB 并上传 COS')
async backupDB({ uploadCOS = true }: { uploadCOS?: boolean } = {}) {
if (isInDemoMode) {
return
}
const backup = await this.backupService.backup()
if (!backup) {
this.logger.log('没有开启备份')

11
src/utils/demo.util.ts Normal file
View File

@ -0,0 +1,11 @@
import { isInDemoMode } from '~/app.config'
import { BanInDemoExcpetion } from '~/common/exceptions/ban-in-demo.exception'
/**
* demo
*/
export const banInDemo = () => {
if (isInDemoMode) {
throw new BanInDemoExcpetion()
}
}

View File

@ -6,3 +6,4 @@ export * from './redis.util'
export * from './system.util'
export * from './time.util'
export * from './tool.util'
export * from './demo.util'