@boarteam/boar-pack-common-backend 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boarteam/boar-pack-common-backend",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "NestJS Users module including permissions system, authentication strategies etc",
5
5
  "repository": "git@github.com:boarteam/boar-pack.git",
6
6
  "author": "Andrew Balakirev <balakirev.andrey@gmail.com>",
@@ -8,7 +8,6 @@
8
8
  "main": "dist/index",
9
9
  "types": "dist/index.d.ts",
10
10
  "files": [
11
- "src",
12
11
  "dist"
13
12
  ],
14
13
  "publishConfig": {
@@ -19,7 +18,7 @@
19
18
  "build": "rm -rf dist && tsc --project tsconfig.build.json",
20
19
  "yalc:push": "yalc push"
21
20
  },
22
- "gitHead": "311a9d2be89fb756e01331f4a70fc79af411e6f9",
21
+ "gitHead": "db759a35be3528d9970b7d2408d3b90d19bd7741",
23
22
  "dependencies": {
24
23
  "@nestjs/websockets": "^11.1.8",
25
24
  "lossless-json": "^4.3.0",
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- export * from './modules/scrypt/scrypt.module';
2
- export * from './modules/scrypt/scrypt.service';
3
- export * from './modules/schedule/schedule.module';
4
- export * from './modules/cluster';
5
- export * from './modules/websockets';
6
- export * as Tools from './tools';
@@ -1,38 +0,0 @@
1
- import { Injectable } from '@nestjs/common';
2
- import { ConfigService } from '@nestjs/config';
3
- import cluster from "node:cluster";
4
-
5
- export type TClusterConfig = {
6
- port: number;
7
- worker?: string;
8
- disableCluster?: boolean;
9
- };
10
-
11
- @Injectable()
12
- export class ClusterConfigService {
13
- constructor(private configService: ConfigService) {
14
- }
15
-
16
- get config(): TClusterConfig {
17
- const port = Number.parseInt(this.configService.get<string>('PORT') || '');
18
- const worker = this.configService.get<string>('WORKER');
19
- const disableCluster = this.configService.get<string>('DISABLE_CLUSTER') === 'true';
20
-
21
- if (!disableCluster) {
22
- if (!port) {
23
- throw new Error('PORT env variable is not set');
24
- }
25
-
26
- if (cluster.isWorker && !worker) {
27
- throw new Error('WORKER env variable is not set');
28
- }
29
-
30
- }
31
-
32
- return {
33
- port,
34
- worker,
35
- disableCluster,
36
- };
37
- }
38
- }
@@ -1,25 +0,0 @@
1
- import { Cluster } from "node:cluster";
2
- import { Worker } from "cluster";
3
-
4
- export interface WorkerSettings {
5
- workerId: string;
6
- workerName?: string;
7
- portIncrement: number | null;
8
- port?: number | null;
9
- extraEnv?: Record<string, string>;
10
- }
11
-
12
- export interface ClusterSettings {
13
- clusterId: string;
14
- appRole: string;
15
- restartDelay?: number;
16
- }
17
-
18
- export interface ClusterInterface {
19
- getSettings(): Promise<ClusterSettings>;
20
- getWorkersSettings(): Promise<WorkerSettings[]>;
21
- onWorkerRun?(worker: WorkerSettings, vars: Record<string, string>): void;
22
- onWorkerExit?(worker: WorkerSettings, code: number, signal: string): void;
23
- onWorkerListening?(worker: WorkerSettings): void;
24
- onClusterMessage?(cluster: Cluster, worker: Worker, message: any): void;
25
- }
@@ -1,47 +0,0 @@
1
- import { Logger, Module, OnApplicationBootstrap } from '@nestjs/common';
2
- import { ClusterService } from "./cluster.service";
3
- import { nextTick } from "process";
4
- import { ConfigModule } from "@nestjs/config";
5
- import { ClusterConfigService, TClusterConfig } from "./cluster.config";
6
-
7
- @Module({
8
- imports: [
9
- ConfigModule,
10
- ],
11
- providers: [
12
- ClusterConfigService,
13
- ClusterService,
14
- ],
15
- exports: [
16
- ClusterService,
17
- ClusterConfigService,
18
- ],
19
- })
20
- export class ClusterModule implements OnApplicationBootstrap {
21
- private readonly logger = new Logger(ClusterModule.name);
22
- private config: TClusterConfig;
23
-
24
- constructor(
25
- private readonly clusterService: ClusterService,
26
- private readonly configService: ClusterConfigService,
27
- ) {
28
- this.config = configService.config;
29
- }
30
-
31
- async onApplicationBootstrap() {
32
- if (this.config.disableCluster) {
33
- this.logger.debug('Cluster is disabled');
34
- return;
35
- }
36
-
37
- // We need to wait for event emitter to be initialized else broadcast will not work
38
- nextTick(async () => {
39
- try {
40
- await this.clusterService.runClusters();
41
- } catch (e) {
42
- this.logger.error('Error while running clusters');
43
- this.logger.error(e, e.stack);
44
- }
45
- });
46
- }
47
- }
@@ -1,178 +0,0 @@
1
- import { Injectable, Logger, NotFoundException } from "@nestjs/common";
2
- import { ClusterInterface, ClusterSettings, WorkerSettings } from "./cluster.interface";
3
- import cluster from "node:cluster";
4
- import { ClusterConfigService, TClusterConfig } from "./cluster.config";
5
- import { Worker } from "cluster";
6
-
7
- type TWorkerVars = {
8
- APP_ROLE: string;
9
- WORKER: string;
10
- WORKER_NAME: string;
11
- PORT?: string;
12
- } & {
13
- [key: string]: string;
14
- }
15
-
16
- @Injectable()
17
- export class ClusterService {
18
- private readonly logger = new Logger(ClusterService.name);
19
- private readonly STOP_WORKER = 'SIGKILL';
20
- private readonly config: TClusterConfig;
21
-
22
- private workersByClusters: Map<ClusterInterface, Map<Worker['id'], {
23
- worker: Worker,
24
- workerVars: TWorkerVars,
25
- }>> = new Map();
26
-
27
- constructor(
28
- private readonly configService: ClusterConfigService,
29
- ) {
30
- this.config = this.configService.config;
31
- }
32
-
33
- public addCluster(cluster: ClusterInterface) {
34
- this.workersByClusters.set(cluster, new Map());
35
- }
36
-
37
- public async runClusters(): Promise<void> {
38
- const clusters = Array.from(this.workersByClusters.keys());
39
- await Promise.allSettled(clusters.map(cluster => this.runCluster(cluster)));
40
- }
41
-
42
- private async runCluster(runningCluster: ClusterInterface) {
43
- const clusterSettings = await runningCluster.getSettings();
44
- const workersSettings = await runningCluster.getWorkersSettings();
45
- workersSettings.forEach(settings => this.runWorker({
46
- runningCluster,
47
- clusterSettings,
48
- workerSettings: settings
49
- }));
50
- }
51
-
52
- private runWorker({
53
- runningCluster,
54
- clusterSettings,
55
- workerSettings,
56
- callback,
57
- }: {
58
- runningCluster: ClusterInterface,
59
- clusterSettings: ClusterSettings,
60
- workerSettings: WorkerSettings,
61
- callback?: () => void,
62
- }) {
63
- const clusterWorkers = this.workersByClusters.get(runningCluster);
64
-
65
- if (!clusterWorkers) {
66
- throw new Error(`Cluster ${runningCluster.constructor.name} is not found`);
67
- }
68
-
69
- const workerName = workerSettings.workerName || workerSettings.workerId;
70
- this.logger.log(`Starting worker ${workerName}...`);
71
-
72
- const vars: TWorkerVars = {
73
- APP_ROLE: clusterSettings.appRole,
74
- WORKER: workerSettings.workerId,
75
- WORKER_NAME: workerName,
76
- ...workerSettings.extraEnv,
77
- }
78
-
79
- if (workerSettings.port) {
80
- vars.PORT = String(workerSettings.port);
81
- } else if (workerSettings.portIncrement !== null) {
82
- vars.PORT = String(this.config.port + workerSettings.portIncrement);
83
- }
84
-
85
- runningCluster.onWorkerRun?.(workerSettings, vars);
86
-
87
- const workerProcess = cluster.fork(vars);
88
- clusterWorkers.set(workerProcess.id, {
89
- worker: workerProcess,
90
- workerVars: vars,
91
- });
92
-
93
- cluster.on('message', (worker, message) => {
94
- runningCluster.onClusterMessage?.(cluster, worker, message);
95
- })
96
-
97
- workerProcess.on('exit', (code, signal) => {
98
- clusterWorkers.delete(workerProcess.id);
99
- runningCluster.onWorkerExit?.(workerSettings, code, signal);
100
-
101
- this.logger.error(`Worker ${workerName} died with code ${code} and signal ${signal}`);
102
-
103
- if (signal === this.STOP_WORKER) {
104
- this.logger.log(`Worker ${workerName} was stopped and will not be restarted`);
105
- return;
106
- }
107
-
108
- if (typeof clusterSettings.restartDelay === 'undefined') {
109
- this.logger.log(`Restarting worker ${workerName} without delay...`);
110
- this.startWorker(runningCluster, workerSettings.workerId).catch(err => {
111
- this.logger.error(`Failed to restart worker ${workerName}: ${err}`);
112
- });
113
- } else {
114
- this.logger.log(`Worker ${workerName} will be restarted in ${clusterSettings.restartDelay}ms...`);
115
- setTimeout(() => {
116
- this.logger.log(`Restarting worker ${workerName}...`);
117
- this.startWorker(runningCluster, workerSettings.workerId).catch(err => {
118
- this.logger.error(`Failed to restart worker ${workerName}: ${err}`);
119
- });
120
- }, clusterSettings.restartDelay);
121
- }
122
- });
123
-
124
- workerProcess.once('listening', () => {
125
- this.logger.log(`Worker ${workerName} started`);
126
- callback?.();
127
- runningCluster.onWorkerListening?.(workerSettings);
128
- });
129
- }
130
-
131
- public async setWorkerState(cluster: ClusterInterface, workerId: string, state: boolean) {
132
- const workers = this.workersByClusters.get(cluster);
133
- if (!workers) {
134
- throw new NotFoundException(`Cluster ${cluster.constructor.name} is not found`);
135
- }
136
-
137
- const workerDescriptor = Array.from(workers.values()).find(worker => worker.workerVars.WORKER === workerId);
138
- if (!workerDescriptor) {
139
- if (state) {
140
- await this.startWorker(cluster, workerId);
141
- } else {
142
- this.logger.log(`Worker ${workerId} is not found and should be stopped - doing nothing`);
143
- }
144
- } else {
145
- if (state) {
146
- await this.updateWorker(workerDescriptor.worker, workerId);
147
- } else {
148
- await this.stopWorker(workerDescriptor.worker, workerId);
149
- }
150
- }
151
- }
152
-
153
- private async startWorker(runningCluster: ClusterInterface, workerId: string) {
154
- const clusterSettings = await runningCluster.getSettings();
155
- const workersSettings = await runningCluster.getWorkersSettings();
156
- const workerSettings = workersSettings.find(settings => settings.workerId === workerId);
157
-
158
- if (!workerSettings) {
159
- throw new NotFoundException(`Settings for worker ${workerId} are not found`);
160
- }
161
-
162
- this.runWorker({
163
- runningCluster,
164
- clusterSettings,
165
- workerSettings,
166
- });
167
- }
168
-
169
- private async stopWorker(worker: Worker, workerId: string) {
170
- this.logger.log(`Stopping worker ${workerId}...`);
171
- worker.kill(this.STOP_WORKER);
172
- }
173
-
174
- private async updateWorker(worker: Worker, workerId: string) {
175
- this.logger.log(`Updating worker ${workerId} by restarting...`);
176
- worker.kill();
177
- }
178
- }
@@ -1,4 +0,0 @@
1
- export * from './cluster.config';
2
- export * from './cluster.interface';
3
- export * from './cluster.module';
4
- export * from './cluster.service';
@@ -1,18 +0,0 @@
1
- import { Module } from '@nestjs/common';
2
- import { ScheduleModule as NestScheduleModule } from "@nestjs/schedule";
3
- import ScheduleService from "./schedule.service";
4
-
5
- @Module({
6
- imports: [
7
- NestScheduleModule.forRoot(),
8
- ],
9
- providers: [
10
- ScheduleService
11
- ],
12
- exports: [
13
- ScheduleService,
14
- ],
15
- })
16
- export class ScheduleModule {
17
- constructor() {}
18
- }
@@ -1,43 +0,0 @@
1
- import { Injectable, OnApplicationBootstrap } from "@nestjs/common";
2
- import { SchedulerRegistry } from "@nestjs/schedule";
3
-
4
- @Injectable()
5
- export class ScheduleService implements OnApplicationBootstrap {
6
- constructor(private schedulerRegistry: SchedulerRegistry) {
7
- }
8
-
9
- onApplicationBootstrap() {
10
- const isDisabled = process.env.SWAGGER === 'true';
11
- // Disable all cron jobs, intervals and timeouts
12
- if (isDisabled) {
13
- this.stopAllCronJobs();
14
- this.deleteAllIntervals();
15
- this.deleteAllTimeouts();
16
- }
17
- }
18
-
19
- private stopAllCronJobs() {
20
- const jobs = this.schedulerRegistry.getCronJobs();
21
-
22
- jobs.forEach(job => {
23
- job.stop();
24
- }
25
- )
26
- }
27
-
28
- private deleteAllIntervals() {
29
- const intervals = this.schedulerRegistry.getIntervals();
30
- intervals.forEach(key => {
31
- this.schedulerRegistry.deleteInterval(key);
32
- });
33
- }
34
-
35
- private deleteAllTimeouts() {
36
- const timeouts = this.schedulerRegistry.getTimeouts();
37
- timeouts.forEach(key => {
38
- this.schedulerRegistry.deleteTimeout(key);
39
- });
40
- }
41
- }
42
-
43
- export default ScheduleService;
@@ -1,36 +0,0 @@
1
- import { Injectable } from '@nestjs/common';
2
- import { ConfigService } from '@nestjs/config';
3
- import Joi from "joi";
4
-
5
- export type TScryptConfig = {
6
- salt: string;
7
- iv: string;
8
- };
9
-
10
- @Injectable()
11
- export class ScryptConfigService {
12
- private ivSchema = Joi.string().length(16).hex();
13
-
14
- constructor(private configService: ConfigService) {
15
- }
16
-
17
- get config(): TScryptConfig {
18
- const salt = this.configService.get<string>('SCRYPT_SALT');
19
- const iv = this.configService.get<string>('SCRYPT_IV');
20
-
21
- if (!salt || !iv) {
22
- throw new Error('SCRYPT_SALT or SCRYPT_IV env variables are not set');
23
- }
24
-
25
- const { error } = this.ivSchema.validate(iv);
26
- if (error) {
27
- console.error('IV is invalid:', error.details[0].message);
28
- throw new Error('IV is invalid, check SCRYPT_IV env variable');
29
- }
30
-
31
- return {
32
- salt,
33
- iv,
34
- };
35
- }
36
- }
@@ -1,20 +0,0 @@
1
- import { Module } from '@nestjs/common';
2
- import ScryptService from "./scrypt.service";
3
- import { ScryptConfigService } from "./scrypt.config";
4
- import { ConfigModule } from "@nestjs/config";
5
-
6
- @Module({
7
- imports: [
8
- ConfigModule,
9
- ],
10
- controllers: [],
11
- providers: [
12
- ScryptService,
13
- ScryptConfigService,
14
- ],
15
- exports: [
16
- ScryptService,
17
- ],
18
- })
19
- export class ScryptModule {
20
- }
@@ -1,41 +0,0 @@
1
- import { Injectable } from "@nestjs/common";
2
- import { ScryptConfigService, TScryptConfig } from "./scrypt.config";
3
- import { promisify } from "util";
4
- import { createCipheriv, scrypt } from "crypto";
5
-
6
- @Injectable()
7
- export class ScryptService {
8
- private config: TScryptConfig;
9
-
10
- constructor(
11
- private scryptConfig: ScryptConfigService,
12
- ) {
13
- this.config = this.scryptConfig.config;
14
- }
15
-
16
- async encrypt(str: string): Promise<string> {
17
- const key = (await promisify(scrypt)(this.config.salt, 'salt', 32)) as Buffer;
18
- const cipher = createCipheriv('aes-256-ctr', key, this.config.iv);
19
-
20
- const encrypted = Buffer.concat([
21
- cipher.update(str),
22
- cipher.final(),
23
- ]);
24
-
25
- return encrypted.toString('base64');
26
- }
27
-
28
- async decrypt(str: string): Promise<string> {
29
- const key = (await promisify(scrypt)(this.config.salt, 'salt', 32)) as Buffer;
30
- const decipher = createCipheriv('aes-256-ctr', key, this.config.iv);
31
-
32
- const decrypted = Buffer.concat([
33
- decipher.update(str, 'base64'),
34
- decipher.final(),
35
- ]);
36
-
37
- return decrypted.toString();
38
- }
39
- }
40
-
41
- export default ScryptService;
@@ -1,12 +0,0 @@
1
- export interface WebsocketsEventDto {
2
- event: string;
3
- data: any;
4
- }
5
-
6
- export class WebsocketsErrorEventDto implements WebsocketsEventDto {
7
- event: 'error';
8
- data: {
9
- message: string;
10
- details?: any;
11
- };
12
- }
@@ -1,2 +0,0 @@
1
- export * from './websockets.exception-filter';
2
- export * from './dto/websockets.dto';
@@ -1,164 +0,0 @@
1
- import { Injectable, Logger, Scope } from "@nestjs/common";
2
- import WebSocket from "ws";
3
- import { isSafeNumber, parse } from "lossless-json";
4
-
5
- export type TBaseConfig<EventType> = {
6
- url: string;
7
- onOpen?: () => void;
8
- onMessage?: (event: EventType) => void;
9
- onClose?: () => void;
10
- getEventError?: (event: EventType) => string | null | undefined;
11
- reconnectTimeout?: number;
12
- ignoreInvalidJson?: boolean;
13
- };
14
-
15
- export enum WsErrorCodes {
16
- ConnectionClosed = 1000,
17
- InvalidJson = 4000,
18
- ErrorMessage = 4001,
19
- Unauthorized = 4003,
20
- }
21
-
22
- @Injectable({ scope: Scope.TRANSIENT })
23
- export class WebsocketsClients<IncomeEventType,
24
- OutgoingEventType = any,
25
- ConfigType extends TBaseConfig<IncomeEventType> = TBaseConfig<IncomeEventType>,
26
- > {
27
- private readonly logger = new Logger(WebsocketsClients.name);
28
- private readonly clients = new WeakMap<WebSocket, ConfigType>();
29
-
30
- constructor() {
31
- }
32
-
33
- private toSafeNumberOrString(value: string): number | string {
34
- return isSafeNumber(value) ? parseFloat(value) : value;
35
- }
36
-
37
- public connect(config: ConfigType): WebSocket {
38
- this.logger.log(`Connecting to ${config.url}...`);
39
- const ws = new WebSocket(config.url);
40
-
41
- this.clients.set(ws, config);
42
-
43
- ws.on('open', async () => {
44
- this.logger.log(`Connected to ${config.url}`);
45
- try {
46
- await config.onOpen?.();
47
- } catch (e) {
48
- this.logger.error(`Error, while calling onOpen for ${config.url} socket`);
49
- this.logger.error(e);
50
- ws.close(WsErrorCodes.ErrorMessage, e.toString());
51
- }
52
- });
53
-
54
- ws.on('error', (error) => {
55
- this.logger.error(`Error for ${config.url} socket:`);
56
- this.logger.error(error);
57
- ws.close();
58
- });
59
-
60
- ws.on('close', (code, reason) => {
61
- this.onClose(ws, config, code, reason);
62
- });
63
-
64
- ws.on('message', (msg: Buffer) => {
65
- let event: IncomeEventType;
66
- try {
67
- const str = String(msg);
68
- this.logger.verbose(str);
69
- event = parse(str, null, this.toSafeNumberOrString) as IncomeEventType;
70
- } catch (e) {
71
- this.logger.error(`Error, while parsing message from WS server ${msg}`);
72
- this.logger.error(e, e.stack);
73
- if (!config.ignoreInvalidJson) {
74
- ws?.close(WsErrorCodes.InvalidJson, "Invalid JSON");
75
- }
76
- return;
77
- }
78
-
79
- const errorMessage = config.getEventError?.(event);
80
- if (errorMessage) {
81
- this.logger.error(`Error from WS server: ${errorMessage}`);
82
- ws.close(WsErrorCodes.ErrorMessage, errorMessage);
83
- return;
84
- }
85
-
86
- config.onMessage?.(event);
87
- });
88
-
89
- return ws;
90
- }
91
-
92
- private onClose(ws: WebSocket, config: ConfigType | undefined, code: number, reason: Buffer) {
93
- this.logger.log(`${config?.url} socket closed with code ${code} and reason: ${reason.toString()}`);
94
- ws.removeAllListeners();
95
- config?.onClose?.();
96
- this.clients.delete(ws);
97
- }
98
-
99
- public close(client: WebSocket): Promise<void> {
100
- const config = this.clients.get(client);
101
- if (typeof config?.reconnectTimeout === "number") {
102
- clearTimeout(config.reconnectTimeout);
103
- }
104
-
105
- if (client.readyState === WebSocket.CLOSED) {
106
- this.onClose(client, config, WsErrorCodes.ConnectionClosed, Buffer.from("Closed by client"));
107
- return Promise.resolve();
108
- }
109
-
110
- return new Promise((resolve) => {
111
- client.once('close', () => resolve());
112
- client.close();
113
- });
114
- }
115
-
116
- public reconnect(client: WebSocket, timeout: number) {
117
- const config = this.clients.get(client);
118
- if (!config) {
119
- throw new Error(`Can't reconnect, config not found`);
120
- }
121
-
122
- config.reconnectTimeout = setTimeout(() => {
123
- this.connect(config);
124
- }, timeout) as unknown as number;
125
- }
126
-
127
- public send(client: WebSocket, data: OutgoingEventType): Promise<void> {
128
- return new Promise((resolve, reject) => {
129
- const send = () => {
130
- client.send(JSON.stringify(data), (err) => {
131
- if (err) {
132
- this.logger.error(`Error sending data to WS server`);
133
- this.logger.error(err);
134
- reject(err);
135
- return;
136
- }
137
-
138
- resolve();
139
- });
140
- }
141
-
142
- if (client.readyState !== WebSocket.OPEN) {
143
- const timeout = setTimeout(() => {
144
- reject(new Error(`Can't send data to WS server, because client is not connected`));
145
- }, 1000);
146
-
147
- client.once('open', () => {
148
- clearTimeout(timeout);
149
- send();
150
- });
151
-
152
- return;
153
- }
154
-
155
- try {
156
- send();
157
- } catch (e) {
158
- this.logger.error(`Error, while sending data to WS server`);
159
- this.logger.error(e);
160
- reject(e);
161
- }
162
- });
163
- }
164
- }
@@ -1,19 +0,0 @@
1
- import { Catch, ArgumentsHost } from '@nestjs/common';
2
- import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets';
3
- import { WebsocketsErrorEventDto } from "./dto/websockets.dto";
4
-
5
- @Catch()
6
- export class WebsocketsExceptionFilter extends BaseWsExceptionFilter {
7
- catch(exception: Error | WsException, host: ArgumentsHost) {
8
- const client = host.switchToWs().getClient();
9
- client.send(JSON.stringify({
10
- event: 'error',
11
- data: {
12
- message: exception.message,
13
- details: exception instanceof WsException ? (exception.getError() as any)?.details : undefined,
14
- },
15
- } as WebsocketsErrorEventDto));
16
-
17
- super.catch(exception, host);
18
- }
19
- }