From 048775899bb8120ffce7af11578ca0a7565e908a Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Mon, 14 Aug 2023 21:56:11 +0300 Subject: [PATCH] server side asset rendering example, totp challenge start --- apps/auth/src/auth.controller.ts | 24 +- apps/auth/src/auth.module.ts | 18 +- apps/auth/src/enums/challenge.enum.ts | 3 + apps/auth/src/services/auth.service.ts | 174 +++++++++-- apps/freeblox-web-service/src/app.module.ts | 5 + .../src/services/auth/auth.controller.ts | 35 ++- .../auth/dtos/challenge-request.dto.ts | 13 + .../auth/dtos/challenge-response.dto.ts | 24 ++ .../services/auth/dtos/login-response.dto.ts | 12 - .../services/catalog/catalog.controller.ts | 10 + apps/render/src/render.controller.ts | 6 + apps/render/src/renderer/index.ts | 48 ++++ apps/render/src/services/render.service.ts | 53 +++- docker-compose.yml | 45 +++ libs/shared/src/cache/cache-manager.ts | 14 + libs/shared/src/cache/redis.provider.ts | 6 + libs/shared/src/database/make-typeorm.ts | 14 +- libs/shared/src/index.ts | 2 + package.json | 12 +- pnpm-lock.yaml | 272 +++++++++++++++++- webpack.config.js | 66 +++-- 21 files changed, 767 insertions(+), 89 deletions(-) create mode 100644 apps/auth/src/enums/challenge.enum.ts create mode 100644 apps/freeblox-web-service/src/services/auth/dtos/challenge-request.dto.ts create mode 100644 apps/freeblox-web-service/src/services/auth/dtos/challenge-response.dto.ts delete mode 100644 apps/freeblox-web-service/src/services/auth/dtos/login-response.dto.ts create mode 100644 apps/render/src/renderer/index.ts create mode 100644 libs/shared/src/cache/cache-manager.ts create mode 100644 libs/shared/src/cache/redis.provider.ts diff --git a/apps/auth/src/auth.controller.ts b/apps/auth/src/auth.controller.ts index 06ec6f4..38e3a78 100644 --- a/apps/auth/src/auth.controller.ts +++ b/apps/auth/src/auth.controller.ts @@ -3,19 +3,35 @@ import { AuthService } from './services/auth.service'; import { MessagePattern } from '@nestjs/microservices'; import { LoginRequest } from './interfaces/auth.interface'; import { UserInfo } from '@freeblox/shared'; +import { AuthChallenge } from './enums/challenge.enum'; @Controller() export class AuthController { constructor(private readonly authService: AuthService) {} @MessagePattern('auth.login') - login({ body }: { body: LoginRequest }) { - return this.authService.login(body); + login({ body, ip }: { body: LoginRequest; ip: string }) { + return this.authService.login(body, ip); } @MessagePattern('auth.loginByRefreshToken') - loginByRefreshToken({ token }: { token: string }) { - return this.authService.loginByRefreshToken(token); + loginByRefreshToken({ token, ip }: { token: string; ip: string }) { + return this.authService.loginByRefreshToken(token, ip); + } + + @MessagePattern('auth.loginByChallenge') + loginByChallenge({ + challenge, + secret, + body, + ip, + }: { + challenge: AuthChallenge; + secret: string; + body: Record; + ip: string; + }) { + return this.authService.loginByChallenge(challenge, secret, body, ip); } @MessagePattern('auth.verify') diff --git a/apps/auth/src/auth.module.ts b/apps/auth/src/auth.module.ts index c0e26f9..528585f 100644 --- a/apps/auth/src/auth.module.ts +++ b/apps/auth/src/auth.module.ts @@ -2,7 +2,13 @@ import { Module, OnModuleInit } from '@nestjs/common'; import { AuthController } from './auth.controller'; import { AuthService } from './services/auth.service'; import { ClientsModule } from '@nestjs/microservices'; -import { makeKnex, makeTypeOrm, natsClient } from '@freeblox/shared'; +import { + getCacheManager, + getTypeOrm, + makeKnex, + makeTypeOrm, + natsClient, +} from '@freeblox/shared'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import knex from 'knex'; @@ -33,14 +39,8 @@ const entities = [ ignoreEnvFile: process.env.NODE_ENV === 'development', load: [makeKnex('auth', __dirname), makeTypeOrm('auth'), security], }), - TypeOrmModule.forRootAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: (config: ConfigService) => ({ - ...config.get('typeorm'), - entities, - }), - }), + getTypeOrm(entities), + getCacheManager(), TypeOrmModule.forFeature(entities), ClientsModule.register([natsClient('auth')]), ], diff --git a/apps/auth/src/enums/challenge.enum.ts b/apps/auth/src/enums/challenge.enum.ts new file mode 100644 index 0000000..5a5a543 --- /dev/null +++ b/apps/auth/src/enums/challenge.enum.ts @@ -0,0 +1,3 @@ +export enum AuthChallenge { + OTP = 'OTP', +} diff --git a/apps/auth/src/services/auth.service.ts b/apps/auth/src/services/auth.service.ts index e2e03f6..4b88075 100644 --- a/apps/auth/src/services/auth.service.ts +++ b/apps/auth/src/services/auth.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { LoginRequest } from '../interfaces/auth.interface'; import { JWTService } from './jwt.service'; import { ILike, Repository } from 'typeorm'; @@ -11,12 +11,15 @@ import { BanService } from './ban.service'; import { BadRequestRpcException, ForbiddenRpcException, - PreconditionFailedRpcException, UserInfo, + generateString, } from '@freeblox/shared'; import { RoleService } from './role.service'; import { ConfigService } from '@nestjs/config'; import { RefreshService } from './refresh.service'; +import { AuthChallenge } from '../enums/challenge.enum'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; @Injectable() export class AuthService { @@ -29,6 +32,7 @@ export class AuthService { @InjectRepository(UserEntity) private readonly userRepository: Repository, private readonly config: ConfigService, + @Inject(CACHE_MANAGER) private cache: Cache, ) {} /** @@ -36,7 +40,7 @@ export class AuthService { * @param body Username/email and password * @returns JWT token */ - async login(body: LoginRequest) { + async login(body: LoginRequest, ip: string) { if (!body.email || !body.password) { throw new BadRequestRpcException('Invalid username or password'); } @@ -68,20 +72,9 @@ export class AuthService { } // Check TOTP - const userOTPToken = await this.otpService.getUserTOTP(userEntity); + const userOTPToken = await this.otpService.userHasTOTP(userEntity); if (userOTPToken) { - if (!body.totpToken) { - throw new PreconditionFailedRpcException('TOTP Token required'); - } - - const validate = this.otpService.validateTOTP( - userOTPToken.token, - body.totpToken, - ); - - if (!validate) { - throw new ForbiddenRpcException('Invalid TOTP Token'); - } + return this.createOTPChallenge(userEntity); } // Issue access token @@ -100,13 +93,104 @@ export class AuthService { ); return { - token: issuedToken, - refresh: refreshToken, - expires_in: exp, + challenge: null, + challengeSecret: null, + authResult: { + token: issuedToken, + refresh: refreshToken, + expires_in: exp, + }, }; } - async loginByRefreshToken(token: string) { + /** + * Login by challenge. + * @param challenge Challenge type + * @param token Challenge secret + * @param response Challenge response from user + * @returns Authentication result or another challenge + */ + async loginByChallenge( + challenge: AuthChallenge, + token: string, + response: Record, + ip: string, + ) { + const decrypted = await this.jwtService.decrypt(token); + const tokenKey = `chlg:${challenge}:${decrypted.token}`; + const challengeSubject = await this.cache.get(tokenKey); + + // Invalid token + if (!challengeSubject) { + throw new ForbiddenRpcException('Invalid challenge response'); + } + + // Wrong subject, somehow.. + if (challengeSubject !== decrypted.sub) { + await this.cache.del(tokenKey); + throw new ForbiddenRpcException('Invalid challenge response'); + } + + const loginUser = await this.userRepository.findOneByOrFail({ + id: challengeSubject, + }); + + // TOTP verification login + if (challenge === AuthChallenge.OTP) { + // If no code is provided, assume request is incorrect + // and delete the challenge from cache. + if (!response.code) { + await this.cache.del(tokenKey); + throw new ForbiddenRpcException('Invalid challenge response'); + } + + const userOTPToken = await this.otpService.getUserTOTP(loginUser); + + // User does not actually have TOTP? + if (!userOTPToken) { + await this.cache.del(tokenKey); + throw new ForbiddenRpcException('Invalid challenge response'); + } + + const validate = this.otpService.validateTOTP( + userOTPToken.token, + response.code, + ); + + // Here we do not delete from the cache, let the user retry. + if (!validate) { + throw new ForbiddenRpcException('Invalid challenge response'); + } + } else { + await this.cache.del(tokenKey); + throw new ForbiddenRpcException('Invalid challenge'); + } + + await this.cache.del(tokenKey); + + // Issue access and refresh token + const exp = this.config.get('security.jwtTokenExpiry'); + const issuedToken = await this.issueAccessToken(loginUser); + const refreshToken = await this.refreshService.issueRefreshToken(loginUser); + + // Set login time to now + await this.userRepository.update( + { id: loginUser.id }, + { loginAt: new Date() }, + ); + + return { + challenge: null, + challengeSecret: null, + authResult: { + token: issuedToken, + refresh: refreshToken, + expires_in: exp, + }, + }; + } + + async loginByRefreshToken(token: string, ip: string) { const refreshToken = await this.refreshService.useRefreshToken(token); const userEntity = refreshToken.user; @@ -121,9 +205,13 @@ export class AuthService { ); return { - token: issuedToken, - refresh: refreshToken.token, - expires_in: exp, + challenge: null, + challengeSecret: null, + authResult: { + token: issuedToken, + refresh: refreshToken.token, + expires_in: exp, + }, }; } @@ -193,21 +281,51 @@ export class AuthService { (privileges, ban) => [...privileges, ...(ban.privileges || [])], [], ); - const privileges = await this.roleService.getUserPrivileges( + + const userPrivileges = await this.roleService.getUserPrivileges( user, bannedPrivileges, ); + // If we only have privilege bans, only restrict those privileges. + // If we have a system ban, only return the web privilege. + const privileges = + !banned || bans.every((ban) => ban.privilegeBan) + ? userPrivileges + : ['web']; + // Issue new token return this.jwtService.sign({ sub: user.id, username: user.username, display_name: user.displayName, language: user.language, - banned: banned, - privileges: banned - ? [] - : privileges.map((privilege) => privilege.privilege), + banned, + privileges, }); } + + /** + * Create OTP challenge for user's login. + * @private + * @param userEntity User + * @returns Challenge response + */ + async createOTPChallenge(userEntity: UserEntity) { + // Create challenge for auth. + const challengeToken = generateString(256); + const tokenKey = `chlg:${AuthChallenge.OTP}:${challengeToken}`; + const encryptedToken = await this.jwtService.encrypt({ + token: challengeToken, + sub: userEntity.id, + }); + + // Challenge is only valid for 5 minutes. + await this.cache.set(tokenKey, userEntity.id, 300000); + return { + challenge: AuthChallenge.OTP, + challengeSecret: encryptedToken, + authResult: null, + }; + } } diff --git a/apps/freeblox-web-service/src/app.module.ts b/apps/freeblox-web-service/src/app.module.ts index ad3d2c5..b621266 100644 --- a/apps/freeblox-web-service/src/app.module.ts +++ b/apps/freeblox-web-service/src/app.module.ts @@ -7,10 +7,15 @@ import { AuthModule } from './services/auth/auth.module'; import { CatalogModule } from './services/catalog/catalog.module'; import { UserMiddleware } from './middleware/user.middleware'; import { AssetsModule } from './services/assets/assets.module'; +import { ThrottlerModule } from '@nestjs/throttler'; @Module({ imports: [ ClientsModule.register([natsClient('auth')]), + ThrottlerModule.forRoot({ + ttl: 60, + limit: 10, + }), AuthModule, CatalogModule, AssetsModule, diff --git a/apps/freeblox-web-service/src/services/auth/auth.controller.ts b/apps/freeblox-web-service/src/services/auth/auth.controller.ts index c5db782..ceef67c 100644 --- a/apps/freeblox-web-service/src/services/auth/auth.controller.ts +++ b/apps/freeblox-web-service/src/services/auth/auth.controller.ts @@ -4,6 +4,7 @@ import { Controller, Get, Inject, + Ip, Post, UseGuards, UseInterceptors, @@ -21,8 +22,10 @@ import { UserInfo } from '@freeblox/shared'; import { lastValueFrom } from 'rxjs'; import { AuthGuard } from '../../guards/auth.guard'; import { UserDto } from './dtos/user.dto'; -import { LoginResponseDto } from './dtos/login-response.dto'; +import { ChallengeResponseDto } from './dtos/challenge-response.dto'; import { LoginByRefreshTokenDto } from './dtos/login-refresh-token.dto'; +import { ChallengeRequestDto } from './dtos/challenge-request.dto'; +import { Throttle } from '@nestjs/throttler'; @Controller({ version: '1', @@ -35,17 +38,35 @@ export class AuthController { constructor(@Inject('auth') private auth: ClientProxy) {} @Post('login') + @Throttle(3, 60) @ApiOperation({ summary: 'Login by username or email and password' }) - @ApiOkResponse({ type: LoginResponseDto }) - async login(@Body() body: LoginDto) { - return this.auth.send('auth.login', { body }); + @ApiOkResponse({ type: ChallengeResponseDto }) + async login(@Body() body: LoginDto, @Ip() ip: string) { + return this.auth.send('auth.login', { body, ip }); + } + + @Post('challenge') + @Throttle(3, 60) + @ApiOperation({ summary: 'Login by challenge' }) + @ApiOkResponse({ type: ChallengeResponseDto }) + async challenge(@Body() body: ChallengeRequestDto, @Ip() ip: string) { + return this.auth.send('auth.loginByChallenge', { + challenge: body.challenge, + secret: body.secret, + body: body.body, + ip, + }); } @Post('refresh') + @Throttle(3, 60) @ApiOperation({ summary: 'Login by refresh token' }) - @ApiOkResponse({ type: LoginResponseDto }) - async refresh(@Body() body: LoginByRefreshTokenDto) { - return this.auth.send('auth.loginByRefreshToken', { token: body.token }); + @ApiOkResponse({ type: ChallengeResponseDto }) + async refresh(@Body() body: LoginByRefreshTokenDto, @Ip() ip: string) { + return this.auth.send('auth.loginByRefreshToken', { + token: body.token, + ip, + }); } @Get('me') diff --git a/apps/freeblox-web-service/src/services/auth/dtos/challenge-request.dto.ts b/apps/freeblox-web-service/src/services/auth/dtos/challenge-request.dto.ts new file mode 100644 index 0000000..73eafcf --- /dev/null +++ b/apps/freeblox-web-service/src/services/auth/dtos/challenge-request.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { AuthChallenge } from 'apps/auth/src/enums/challenge.enum'; + +export class ChallengeRequestDto { + @ApiProperty({ type: String, enum: AuthChallenge }) + challenge: AuthChallenge; + + @ApiProperty() + secret: string; + + @ApiProperty() + body: Record; +} diff --git a/apps/freeblox-web-service/src/services/auth/dtos/challenge-response.dto.ts b/apps/freeblox-web-service/src/services/auth/dtos/challenge-response.dto.ts new file mode 100644 index 0000000..82cc594 --- /dev/null +++ b/apps/freeblox-web-service/src/services/auth/dtos/challenge-response.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { AuthChallenge } from '../../../../../auth/src/enums/challenge.enum'; + +export class AuthResultResponse { + @ApiProperty() + token: string; + + @ApiProperty() + refresh: string; + + @ApiProperty() + expires_in: number; +} + +export class ChallengeResponseDto { + @ApiProperty({ type: String, enum: AuthChallenge, nullable: true }) + challenge: AuthChallenge; + + @ApiProperty({ nullable: true }) + challengeSecret: string; + + @ApiProperty({ type: AuthResultResponse, nullable: true }) + authResult: AuthResultResponse; +} diff --git a/apps/freeblox-web-service/src/services/auth/dtos/login-response.dto.ts b/apps/freeblox-web-service/src/services/auth/dtos/login-response.dto.ts deleted file mode 100644 index 257c182..0000000 --- a/apps/freeblox-web-service/src/services/auth/dtos/login-response.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class LoginResponseDto { - @ApiProperty() - token: string; - - @ApiProperty() - refresh: string; - - @ApiProperty() - expires_in: number; -} diff --git a/apps/freeblox-web-service/src/services/catalog/catalog.controller.ts b/apps/freeblox-web-service/src/services/catalog/catalog.controller.ts index 647c851..d6137c9 100644 --- a/apps/freeblox-web-service/src/services/catalog/catalog.controller.ts +++ b/apps/freeblox-web-service/src/services/catalog/catalog.controller.ts @@ -24,6 +24,7 @@ import { lastValueFrom } from 'rxjs'; import { CategoryResponseDto } from './dtos/category-response.dto'; import { ContentAssetDto } from './dtos/content-asset.dto'; import { RequirePrivileges } from '../../decorators/require-privileges.decorator'; +import { Throttle } from '@nestjs/throttler'; @Controller({ version: '1', @@ -42,6 +43,7 @@ export class CatalogController { } @Post('content') + @Throttle(3, 60) @ApiOperation({ summary: 'Create new content' }) @ApiOkResponse({ type: ContentResponseDto }) @RequirePrivileges('create:*', 'contentedit') @@ -83,6 +85,7 @@ export class CatalogController { } @Patch('content/:id') + @Throttle(3, 60) @ApiOperation({ summary: 'Update content details' }) @ApiOkResponse({ type: ContentResponseDto }) @RequirePrivileges('create:*', 'contentedit') @@ -95,6 +98,7 @@ export class CatalogController { } @Post('content/:id/revision') + @Throttle(3, 60) @ApiOperation({ summary: 'Create a new revision (upload new content for item)', }) @@ -115,6 +119,7 @@ export class CatalogController { } @Patch('content/:id/publish') + @Throttle(3, 60) @ApiOkResponse({ type: ContentResponseDto }) @ApiOperation({ summary: 'Publish content', @@ -129,4 +134,9 @@ export class CatalogController { user, }); } + + @Post('test') + async test() { + return this.catalog.send('render.assetid', {}); + } } diff --git a/apps/render/src/render.controller.ts b/apps/render/src/render.controller.ts index da3a6c5..702a97b 100644 --- a/apps/render/src/render.controller.ts +++ b/apps/render/src/render.controller.ts @@ -1,7 +1,13 @@ import { Controller } from '@nestjs/common'; import { RenderService } from './services/render.service'; +import { MessagePattern } from '@nestjs/microservices'; @Controller() export class RenderController { constructor(private readonly renderService: RenderService) {} + + @MessagePattern('render.assetid') + async renderAsset({ id }: { id: string }) { + return this.renderService.render(); + } } diff --git a/apps/render/src/renderer/index.ts b/apps/render/src/renderer/index.ts new file mode 100644 index 0000000..06a5f65 --- /dev/null +++ b/apps/render/src/renderer/index.ts @@ -0,0 +1,48 @@ +import { + Engine, + EngineEvents, + EnvironmentComponent, + EventEmitter, + LevelComponent, + ViewportComponent, + WebGLRenderer, + instanceCharacterObject, +} from '@freeblox/engine'; +import { Vector2, Vector3 } from 'three'; + +export class BarebonesRenderer extends Engine { + public events = new EventEmitter(); + override mount(element: HTMLElement): void { + this.element = element; + this.render = new WebGLRenderer(element, new Vector2(800, 800), { + alpha: true, + }); + this.render.renderer.autoClear = false; + this.use(ViewportComponent); + this.use(EnvironmentComponent); + this.use(LevelComponent); + this.getComponent(ViewportComponent).setSize(800, 800); + } +} + +(async function () { + const engine = new BarebonesRenderer(); + const div = document.createElement('div'); + document.body.appendChild(div); + document.body.style.margin = '0'; + document.body.style.padding = '0'; + engine.mount(div); + + const level = engine.getComponent(LevelComponent); + const viewport = engine.getComponent(ViewportComponent); + + const char = await instanceCharacterObject('test'); + level.world.add(char); + + viewport.camera.position.set(2, 6, 5); + viewport.camera.lookAt(new Vector3(0, 3.5, 0)); + + engine.render.renderer.setClearColor(0x000000, 0); + engine.render.render(); + engine.stop(); +})(); diff --git a/apps/render/src/services/render.service.ts b/apps/render/src/services/render.service.ts index ceb90f9..9f73d73 100644 --- a/apps/render/src/services/render.service.ts +++ b/apps/render/src/services/render.service.ts @@ -1,4 +1,53 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import puppeteer from 'puppeteer'; @Injectable() -export class RenderService {} +export class RenderService { + private logger = new Logger(RenderService.name); + + async render() { + const file = readFileSync(join(__dirname, 'renderer.js'), 'utf-8'); + + const browser = await puppeteer.launch({ + headless: true, + args: [ + '--use-gl=swiftshader', + '--no-sandbox', + '--enable-surface-synchronization', + ], + }); + + const page = await browser.newPage(); + await page.setViewport({ width: 800, height: 800 }); + + page + .on('console', (message) => + this.logger.log(`${message.type().toUpperCase()} ${message.text()}`), + ) + .on('pageerror', ({ message }) => this.logger.log(message)) + .on('response', (response) => + this.logger.log(`${response.status()} ${response.url()}`), + ) + .on('requestfailed', (request) => + this.logger.log(`${request.failure().errorText} ${request.url()}`), + ); + + await page.setContent( + ``, + { + waitUntil: ['load', 'networkidle0'], + }, + ); + + const picture = await page.screenshot({ + encoding: 'base64', + omitBackground: true, + }); + + await browser.close(); + + return picture; + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 95698cf..e915b31 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,13 @@ services: - ./docker/postgres:/docker-entrypoint-initdb.d networks: - fblx + redis: + container_name: fblx-redis + image: redis:7-alpine + volumes: + - fblx-redis:/data + networks: + - fblx auth: container_name: fblx-auth build: @@ -28,6 +35,10 @@ services: dockerfile: Dockerfile.dev args: - SERVICE=auth + depends_on: + - nats + - postgres + - redis networks: - fblx environment: @@ -49,6 +60,10 @@ services: dockerfile: Dockerfile.dev args: - SERVICE=catalog + depends_on: + - nats + - postgres + - redis networks: - fblx environment: @@ -66,6 +81,10 @@ services: dockerfile: Dockerfile.dev args: - SERVICE=game + depends_on: + - nats + - postgres + - redis networks: - fblx environment: @@ -83,6 +102,10 @@ services: dockerfile: Dockerfile.dev args: - SERVICE=player + depends_on: + - nats + - postgres + - redis networks: - fblx environment: @@ -100,6 +123,10 @@ services: dockerfile: Dockerfile.dev args: - SERVICE=server + depends_on: + - nats + - postgres + - redis networks: - fblx environment: @@ -117,6 +144,10 @@ services: dockerfile: Dockerfile.dev args: - SERVICE=session + depends_on: + - nats + - postgres + - redis networks: - fblx environment: @@ -134,6 +165,10 @@ services: dockerfile: Dockerfile.dev args: - SERVICE=bank + depends_on: + - nats + - postgres + - redis networks: - fblx environment: @@ -151,6 +186,10 @@ services: dockerfile: Dockerfile.dev args: - SERVICE=assets + depends_on: + - nats + - postgres + - redis networks: - fblx environment: @@ -171,6 +210,9 @@ services: build: context: . dockerfile: Dockerfile.render.dev + depends_on: + - nats + - redis networks: - fblx environment: @@ -185,6 +227,8 @@ services: dockerfile: Dockerfile.dev args: - SERVICE=freeblox-web-service + depends_on: + - nats networks: - fblx ports: @@ -224,3 +268,4 @@ networks: fblx: volumes: fblx-pgadmin: + fblx-redis: diff --git a/libs/shared/src/cache/cache-manager.ts b/libs/shared/src/cache/cache-manager.ts new file mode 100644 index 0000000..22e9075 --- /dev/null +++ b/libs/shared/src/cache/cache-manager.ts @@ -0,0 +1,14 @@ +import { CacheModule } from '@nestjs/cache-manager'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { redisConfig } from './redis.provider'; +import * as redisStore from 'cache-manager-redis-store'; + +export const getCacheManager = () => + CacheModule.registerAsync({ + imports: [ConfigModule, ConfigModule.forFeature(redisConfig)], + useFactory: (config) => ({ + store: redisStore, + ...config.get('redis'), + }), + inject: [ConfigService], + }); diff --git a/libs/shared/src/cache/redis.provider.ts b/libs/shared/src/cache/redis.provider.ts new file mode 100644 index 0000000..cf05aa0 --- /dev/null +++ b/libs/shared/src/cache/redis.provider.ts @@ -0,0 +1,6 @@ +import { registerAs } from '@nestjs/config'; + +export const redisConfig = registerAs('redis', () => ({ + host: String(process.env.REDIS_HOST || 'redis'), + port: Number(process.env.REDIS_PORT) || 6379, +})); diff --git a/libs/shared/src/database/make-typeorm.ts b/libs/shared/src/database/make-typeorm.ts index 37eed79..f6673a1 100644 --- a/libs/shared/src/database/make-typeorm.ts +++ b/libs/shared/src/database/make-typeorm.ts @@ -1,5 +1,5 @@ -import { registerAs } from '@nestjs/config'; -import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { ConfigModule, ConfigService, registerAs } from '@nestjs/config'; +import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; export const makeTypeOrm = (database: string) => registerAs( @@ -14,3 +14,13 @@ export const makeTypeOrm = (database: string) => database, } as TypeOrmModuleOptions), ); + +export const getTypeOrm = (entities?: any[]) => + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + ...config.get('typeorm'), + entities, + }), + }); diff --git a/libs/shared/src/index.ts b/libs/shared/src/index.ts index e097032..35413f2 100644 --- a/libs/shared/src/index.ts +++ b/libs/shared/src/index.ts @@ -12,3 +12,5 @@ export * from './types/userinfo'; export * from './types/page-query.interface'; export * from './exception/rpc.exception'; export * from './filters/rpc-exception.filter'; +export * from './cache/redis.provider'; +export * from './cache/cache-manager'; diff --git a/package.json b/package.json index de7700b..956b88b 100644 --- a/package.json +++ b/package.json @@ -28,16 +28,22 @@ "@aws-sdk/s3-request-presigner": "^3.375.0", "@aws-sdk/url-parser": "^3.374.0", "@aws-sdk/util-format-url": "^3.370.0", + "@freeblox/engine": "^0.0.4", "@nestjs/axios": "^3.0.0", + "@nestjs/cache-manager": "^2.1.0", "@nestjs/common": "^10.0.3", "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.0.3", "@nestjs/microservices": "^10.0.3", "@nestjs/platform-express": "^10.0.3", + "@nestjs/schedule": "^3.0.1", "@nestjs/swagger": "^7.0.11", + "@nestjs/throttler": "^4.2.1", "@nestjs/typeorm": "^10.0.0", "axios": "^1.4.0", "bcrypt": "^5.1.0", + "cache-manager": "^5.2.3", + "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "jose": "^4.14.4", @@ -53,13 +59,15 @@ "rxjs": "^7.8.1", "three": "^0.154.0", "typeorm": "^0.3.17", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "webpack-merge": "^5.9.0" }, "devDependencies": { "@nestjs/cli": "^10.0.5", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^10.0.3", "@types/bcrypt": "^5.0.0", + "@types/cron": "^2.0.1", "@types/express": "^4.17.17", "@types/jest": "29.5.2", "@types/multer": "^1.4.7", @@ -106,4 +114,4 @@ "^@freeblox/shared(|/.*)$": "/libs/shared/src/$1" } } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 091ade0..6429d10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,15 @@ dependencies: '@aws-sdk/util-format-url': specifier: ^3.370.0 version: 3.370.0 + '@freeblox/engine': + specifier: ^0.0.4 + version: 0.0.4 '@nestjs/axios': specifier: ^3.0.0 version: 3.0.0(@nestjs/common@10.0.3)(axios@1.4.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/cache-manager': + specifier: ^2.1.0 + version: 2.1.0(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(cache-manager@5.2.3)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/common': specifier: ^10.0.3 version: 10.0.3(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) @@ -40,13 +46,19 @@ dependencies: version: 10.0.3(@nestjs/common@10.0.3)(@nestjs/microservices@10.0.3)(@nestjs/platform-express@10.0.3)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/microservices': specifier: ^10.0.3 - version: 10.0.3(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) + version: 10.0.3(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(cache-manager@5.2.3)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/platform-express': specifier: ^10.0.3 version: 10.0.3(@nestjs/common@10.0.3)(@nestjs/core@10.0.3) + '@nestjs/schedule': + specifier: ^3.0.1 + version: 3.0.1(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(reflect-metadata@0.1.13) '@nestjs/swagger': specifier: ^7.0.11 version: 7.0.11(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) + '@nestjs/throttler': + specifier: ^4.2.1 + version: 4.2.1(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(reflect-metadata@0.1.13) '@nestjs/typeorm': specifier: ^10.0.0 version: 10.0.0(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typeorm@0.3.17) @@ -56,6 +68,12 @@ dependencies: bcrypt: specifier: ^5.1.0 version: 5.1.0 + cache-manager: + specifier: ^5.2.3 + version: 5.2.3 + cache-manager-redis-store: + specifier: ^3.0.1 + version: 3.0.1 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -104,6 +122,9 @@ dependencies: uuid: specifier: ^9.0.0 version: 9.0.0 + webpack-merge: + specifier: ^5.9.0 + version: 5.9.0 devDependencies: '@nestjs/cli': @@ -118,6 +139,9 @@ devDependencies: '@types/bcrypt': specifier: ^5.0.0 version: 5.0.0 + '@types/cron': + specifier: ^2.0.1 + version: 2.0.1 '@types/express': specifier: ^4.17.17 version: 4.17.17 @@ -1300,6 +1324,10 @@ packages: dependencies: '@jridgewell/trace-mapping': 0.3.9 + /@dimforge/rapier3d@0.11.2: + resolution: {integrity: sha512-B+AKkPmtJxED3goMTGU8v0ju8hUAUQGLgghzCos4G4OeN9X+mJ5lfN2xtNA0n8tJRJk2YfsMk9BOj/6AN89Acg==} + dev: false + /@eslint-community/eslint-utils@4.4.0(eslint@8.43.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1337,6 +1365,17 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@freeblox/engine@0.0.4: + resolution: {integrity: sha512-faxtovSxW6hHNgpOdPeRTYuZk01LiHt8hZA8Vp6cBkdbBMP8jmXBXX/ADFTLv/v9GBXK2DBLbmeZhZ0R46imwg==, tarball: https://git.icynet.eu/api/packages/freeblox/npm/%40freeblox%2Fengine/-/0.0.4/engine-0.0.4.tgz} + dependencies: + '@dimforge/rapier3d': 0.11.2 + buffer: 6.0.3 + reflect-metadata: 0.1.13 + smart-buffer: 4.2.0 + three: 0.153.0 + uuid: 9.0.0 + dev: false + /@humanwhocodes/config-array@0.11.10: resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} engines: {node: '>=10.10.0'} @@ -1683,6 +1722,22 @@ packages: rxjs: 7.8.1 dev: false + /@nestjs/cache-manager@2.1.0(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(cache-manager@5.2.3)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-9kep3a8Mq5cMuXN/anGhSYc0P48CRBXk5wyJJRBFxhNkCH8AIzZF4CASGVDIEMmm3OjVcEUHojjyJwCODS17Qw==} + peerDependencies: + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + cache-manager: <=5 + reflect-metadata: ^0.1.12 + rxjs: ^7.0.0 + dependencies: + '@nestjs/common': 10.0.3(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.3(@nestjs/common@10.0.3)(@nestjs/microservices@10.0.3)(@nestjs/platform-express@10.0.3)(reflect-metadata@0.1.13)(rxjs@7.8.1) + cache-manager: 5.2.3 + reflect-metadata: 0.1.13 + rxjs: 7.8.1 + dev: false + /@nestjs/cli@10.0.5: resolution: {integrity: sha512-Btc1lzAkm4j+af9YA0Kv+6N2x7vDneEhsozzEp87kHZrONYNS8zg/SBFTRx5b/e6g0MJshv6vP56PVrdNZy3kA==} engines: {node: '>= 16'} @@ -1778,7 +1833,7 @@ packages: optional: true dependencies: '@nestjs/common': 10.0.3(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/microservices': 10.0.3(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/microservices': 10.0.3(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(cache-manager@5.2.3)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/platform-express': 10.0.3(@nestjs/common@10.0.3)(@nestjs/core@10.0.3) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 @@ -1810,7 +1865,7 @@ packages: reflect-metadata: 0.1.13 dev: false - /@nestjs/microservices@10.0.3(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1): + /@nestjs/microservices@10.0.3(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(cache-manager@5.2.3)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1): resolution: {integrity: sha512-ySAdASLA2FLD0ScEmAHObNsg/IdnG4pYXWxQKLO3co3CAAwHCjF0D4kI2SeegBx89KU0D1wPLvLYzRI0pXh4jQ==} peerDependencies: '@grpc/grpc-js': '*' @@ -1848,6 +1903,7 @@ packages: dependencies: '@nestjs/common': 10.0.3(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': 10.0.3(@nestjs/common@10.0.3)(@nestjs/microservices@10.0.3)(@nestjs/platform-express@10.0.3)(reflect-metadata@0.1.13)(rxjs@7.8.1) + cache-manager: 5.2.3 iterare: 1.2.1 nats: 2.15.1 reflect-metadata: 0.1.13 @@ -1870,6 +1926,20 @@ packages: transitivePeerDependencies: - supports-color + /@nestjs/schedule@3.0.1(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-4CAFu4rE/QPYnz/icRg3GiuLmY1bXopG8bWTJ9d7bXzaHBaPKIjGvZ20wsK8P+MncrVCkmK0iYhQrNj0cwX9+A==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.12 + dependencies: + '@nestjs/common': 10.0.3(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.3(@nestjs/common@10.0.3)(@nestjs/microservices@10.0.3)(@nestjs/platform-express@10.0.3)(reflect-metadata@0.1.13)(rxjs@7.8.1) + cron: 2.3.1 + reflect-metadata: 0.1.13 + uuid: 9.0.0 + dev: false + /@nestjs/schematics@10.0.1(chokidar@3.5.3)(typescript@5.1.3): resolution: {integrity: sha512-buxpYtSwOmWyf0nUJWJCkCkYITwbOfIEKHTnGS7sDbcfaajrOFXb5pPAGD2E1CUb3C1+NkQIURPKzs0IouZTQg==} peerDependencies: @@ -1929,11 +1999,24 @@ packages: dependencies: '@nestjs/common': 10.0.3(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': 10.0.3(@nestjs/common@10.0.3)(@nestjs/microservices@10.0.3)(@nestjs/platform-express@10.0.3)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/microservices': 10.0.3(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/microservices': 10.0.3(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(cache-manager@5.2.3)(nats@2.15.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/platform-express': 10.0.3(@nestjs/common@10.0.3)(@nestjs/core@10.0.3) tslib: 2.5.3 dev: true + /@nestjs/throttler@4.2.1(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-wVPMuIyr0KdrK1RVVQceWVNesogCm9IgYC1V5EkaTZ+usIE4qxEyzdwU5IqQLgOO/Loiq98MLwReDxazX7i9Uw==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.13 + dependencies: + '@nestjs/common': 10.0.3(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.3(@nestjs/common@10.0.3)(@nestjs/microservices@10.0.3)(@nestjs/platform-express@10.0.3)(reflect-metadata@0.1.13)(rxjs@7.8.1) + md5: 2.3.0 + reflect-metadata: 0.1.13 + dev: false + /@nestjs/typeorm@10.0.0(@nestjs/common@10.0.3)(@nestjs/core@10.0.3)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typeorm@0.3.17): resolution: {integrity: sha512-WQU4HCDTz4UavsFzvGUKDHqi0MO5K47yFoPXdmh+Z/hCNO7SHCMmV9jLiLukM8n5nKUqJ3jDqiljkWBcZPdCtA==} peerDependencies: @@ -2044,6 +2127,55 @@ packages: - supports-color dev: false + /@redis/bloom@1.2.0(@redis/client@1.5.8): + resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + peerDependencies: + '@redis/client': ^1.0.0 + dependencies: + '@redis/client': 1.5.8 + dev: false + + /@redis/client@1.5.8: + resolution: {integrity: sha512-xzElwHIO6rBAqzPeVnCzgvrnBEcFL1P0w8P65VNLRkdVW8rOE58f52hdj0BDgmsdOm4f1EoXPZtH4Fh7M/qUpw==} + engines: {node: '>=14'} + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + dev: false + + /@redis/graph@1.1.0(@redis/client@1.5.8): + resolution: {integrity: sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==} + peerDependencies: + '@redis/client': ^1.0.0 + dependencies: + '@redis/client': 1.5.8 + dev: false + + /@redis/json@1.0.4(@redis/client@1.5.8): + resolution: {integrity: sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==} + peerDependencies: + '@redis/client': ^1.0.0 + dependencies: + '@redis/client': 1.5.8 + dev: false + + /@redis/search@1.1.3(@redis/client@1.5.8): + resolution: {integrity: sha512-4Dg1JjvCevdiCBTZqjhKkGoC5/BcB7k9j99kdMnaXFXg8x4eyOIVg9487CMv7/BUVkFLZCaIh8ead9mU15DNng==} + peerDependencies: + '@redis/client': ^1.0.0 + dependencies: + '@redis/client': 1.5.8 + dev: false + + /@redis/time-series@1.0.4(@redis/client@1.5.8): + resolution: {integrity: sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==} + peerDependencies: + '@redis/client': ^1.0.0 + dependencies: + '@redis/client': 1.5.8 + dev: false + /@sinclair/typebox@0.25.24: resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} dev: true @@ -2554,6 +2686,13 @@ packages: resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==} dev: true + /@types/cron@2.0.1: + resolution: {integrity: sha512-WHa/1rtNtD2Q/H0+YTTZoty+/5rcE66iAFX2IY+JuUoOACsevYyFkSYu/2vdw+G5LrmO7Lxowrqm0av4k3qWNQ==} + dependencies: + '@types/luxon': 3.3.1 + '@types/node': 20.3.2 + dev: true + /@types/eslint-scope@3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: @@ -2627,6 +2766,10 @@ packages: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true + /@types/luxon@3.3.1: + resolution: {integrity: sha512-XOS5nBcgEeP2PpcqJHjCWhUCAzGfXIU8ILOSLpx2FhxqMW9KdxgCGXNOEKGVBfveKtIpztHzKK5vSRVLyW/NqA==} + dev: true + /@types/mime@1.3.2: resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} dev: true @@ -3427,6 +3570,19 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + /cache-manager-redis-store@3.0.1: + resolution: {integrity: sha512-o560kw+dFqusC9lQJhcm6L2F2fMKobJ5af+FoR2PdnMVdpQ3f3Bz6qzvObTGyvoazQJxjQNWgMQeChP4vRTuXQ==} + engines: {node: '>= 16.18.0'} + dependencies: + redis: 4.6.7 + dev: false + + /cache-manager@5.2.3: + resolution: {integrity: sha512-9OErI8fksFkxAMJ8Mco0aiZSdphyd90HcKiOMJQncSlU1yq/9lHHxrT8PDayxrmr9IIIZPOAEfXuGSD7g29uog==} + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 9.1.2 + /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -3475,6 +3631,10 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true + /charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + dev: false + /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -3583,11 +3743,25 @@ packages: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + /clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + dev: false + /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} dev: true + /cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + dev: false + /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -3740,6 +3914,12 @@ packages: /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + /cron@2.3.1: + resolution: {integrity: sha512-1eRRlIT0UfIqauwbG9pkg3J6CX9A6My2ytJWqAXoK0T9oJnUZTzGBNPxao0zjodIbPgf8UQWjE62BMb9eVllSQ==} + dependencies: + luxon: 3.3.0 + dev: false + /cross-fetch@4.0.0: resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} dependencies: @@ -3756,6 +3936,10 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + dev: false + /data-uri-to-buffer@5.0.1: resolution: {integrity: sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==} engines: {node: '>= 14'} @@ -4484,6 +4668,11 @@ packages: wide-align: 1.1.5 dev: false + /generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + dev: false + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -4839,6 +5028,10 @@ packages: binary-extensions: 2.2.0 dev: true + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false + /is-core-module@2.12.1: resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} dependencies: @@ -4880,6 +5073,13 @@ packages: engines: {node: '>=8'} dev: true + /is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: false + /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -4896,6 +5096,11 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + dev: false + /istanbul-lib-coverage@3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} @@ -5464,6 +5669,11 @@ packages: safe-buffer: 5.2.1 dev: false + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: false + /kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -5558,6 +5768,9 @@ packages: p-locate: 5.0.0 dev: true + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} dev: true @@ -5598,6 +5811,15 @@ packages: engines: {node: '>=12'} dev: false + /lru-cache@9.1.2: + resolution: {integrity: sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==} + engines: {node: 14 || >=16.14} + + /luxon@3.3.0: + resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==} + engines: {node: '>=12'} + dev: false + /macos-release@2.5.1: resolution: {integrity: sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==} engines: {node: '>=6'} @@ -5625,6 +5847,14 @@ packages: tmpl: 1.0.5 dev: true + /md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + dev: false + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -6464,6 +6694,17 @@ packages: resolve: 1.22.2 dev: false + /redis@4.6.7: + resolution: {integrity: sha512-KrkuNJNpCwRm5vFJh0tteMxW8SaUzkm5fBH7eL5hd/D0fAkzvapxbfGPP/r+4JAXdQuX7nebsBkBqA2RHB7Usw==} + dependencies: + '@redis/bloom': 1.2.0(@redis/client@1.5.8) + '@redis/client': 1.5.8 + '@redis/graph': 1.1.0(@redis/client@1.5.8) + '@redis/json': 1.0.4(@redis/client@1.5.8) + '@redis/search': 1.1.3(@redis/client@1.5.8) + '@redis/time-series': 1.0.4(@redis/client@1.5.8) + dev: false + /reflect-metadata@0.1.13: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} @@ -6645,6 +6886,13 @@ packages: safe-buffer: 5.2.1 dev: false + /shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + dependencies: + kind-of: 6.0.3 + dev: false + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -7024,6 +7272,10 @@ packages: engines: {node: '>=0.2.6'} dev: false + /three@0.153.0: + resolution: {integrity: sha512-OCP2/uQR6GcDpSLnJt/3a4mdS0kNWcbfUXIwLoEMgLzEUIVIYsSDwskpmOii/AkDM+BBwrl6+CKgrjX9+E2aWg==} + dev: false + /three@0.154.0: resolution: {integrity: sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==} dev: false @@ -7416,6 +7668,14 @@ packages: /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + /webpack-merge@5.9.0: + resolution: {integrity: sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==} + engines: {node: '>=10.0.0'} + dependencies: + clone-deep: 4.0.1 + wildcard: 2.0.1 + dev: false + /webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} @@ -7525,6 +7785,10 @@ packages: string-width: 4.2.3 dev: false + /wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + dev: false + /windows-release@4.0.0: resolution: {integrity: sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==} engines: {node: '>=10'} diff --git a/webpack.config.js b/webpack.config.js index a64f8db..4b39358 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,23 +12,51 @@ const writeGlobEntry = (pattern, to) => { ); }; -module.exports = function (config) { - const appName = process.argv[process.argv.length - 1]; - config.entry = { - main: path.join(__dirname, 'apps', appName, 'src', 'main.ts'), - ...writeGlobEntry( - path.resolve('apps', appName, 'src', 'database', 'migrations', '*.ts'), - 'database/migrations', - ), - ...writeGlobEntry( - path.resolve('apps', appName, 'src', 'database', 'seeds', '*.ts'), - 'database/seeds', - ), +const appName = process.argv[process.argv.length - 1]; + +const configs = [ + function (config) { + config.entry = { + main: path.join(__dirname, 'apps', appName, 'src', 'main.ts'), + ...writeGlobEntry( + path.resolve('apps', appName, 'src', 'database', 'migrations', '*.ts'), + 'database/migrations', + ), + ...writeGlobEntry( + path.resolve('apps', appName, 'src', 'database', 'seeds', '*.ts'), + 'database/seeds', + ), + }; + config.output = { + path: `${__dirname}/dist/apps/${appName}`, + filename: '[name].js', + libraryTarget: 'commonjs', + }; + + return config; + }, +]; + +if (appName === 'render') { + const config2 = { + entry: { + renderer: + appName === 'render' + ? path.resolve('apps', appName, 'src', 'renderer', 'index.ts') + : undefined, + }, + target: 'web', + externalsPresets: {}, + externals: { + '@dimforge/rapier3d': 'RAPIER', + }, + output: { + path: `${__dirname}/dist/apps/${appName}`, + filename: '[name].js', + libraryTarget: '', + }, }; - config.output = { - path: `${__dirname}/dist/apps/${appName}`, - filename: '[name].js', - libraryTarget: 'commonjs', - }; - return config; -}; + configs.push(config2); +} + +module.exports = configs;