chore(deps): update dependencies and remove unused files

Updated several dependencies including:
- @aws-sdk/client-s3 from 3.965.0 to 3.975.0
- @aws-sdk/client-ses from 3.965.0 to 3.975.0
- @aws-sdk/client-sqs from 3.965.0 to 3.975.0
- dotenv from ^16.6.1 to ^16.5.0
- form-data from ^4.0.2 to ^4.0.5
- typescript from 5.9.3 to 5.8.3

Removed unused files:
- backend/.yarn/releases/yarn-4.9.1.cjs
- backend/package-lock.json
This commit is contained in:
Viktoria Polyakova
2026-01-27 01:41:38 +03:00
parent de283d2a93
commit 383a197f8f
31 changed files with 4244 additions and 8622 deletions

0
backend/.yarn/releases/yarn-4.9.1.cjs vendored Normal file → Executable file
View File

View File

2135
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -51,21 +51,18 @@
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"cookie-parser": "^1.4.7",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"decimal.js": "^10.5.0", "decimal.js": "^10.5.0",
"docxtemplater": "^3.63.2", "docxtemplater": "^3.63.2",
"dotenv": "^16.6.1", "dotenv": "^16.5.0",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"express": "^5.1.0", "express": "^5.1.0",
"express-rate-limit": "^8.2.1",
"form-data": "^4.0.2", "form-data": "^4.0.2",
"generate-password": "^1.7.1", "generate-password": "^1.7.1",
"googleapis": "^149.0.0", "googleapis": "^149.0.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"heapdump": "^0.3.15", "heapdump": "^0.3.15",
"helmet": "^8.1.0",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"imap-simple": "^5.1.0", "imap-simple": "^5.1.0",
@@ -109,7 +106,6 @@
"@swc/core": "^1.11.29", "@swc/core": "^1.11.29",
"@total-typescript/ts-reset": "^0.6.1", "@total-typescript/ts-reset": "^0.6.1",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1",
"@types/express": "^5.0.2", "@types/express": "^5.0.2",
"@types/heapdump": "^0", "@types/heapdump": "^0",
"@types/html-to-text": "^9.0.4", "@types/html-to-text": "^9.0.4",

View File

@@ -109,7 +109,7 @@ export class ImportService {
public async importDataForEntityType(accountId: number, user: User, entityTypeId: number, file: StorageFile) { public async importDataForEntityType(accountId: number, user: User, entityTypeId: number, file: StorageFile) {
const workbook = new Workbook(); const workbook = new Workbook();
await workbook.xlsx.load(file.buffer as any); await workbook.xlsx.load(file.buffer);
const worksheet = workbook.worksheets[0]; const worksheet = workbook.worksheets[0];
const importInfos = this.processHeaderRow(worksheet.getRow(1)); const importInfos = this.processHeaderRow(worksheet.getRow(1));

View File

@@ -35,7 +35,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
return new ValidationPipe({ return new ValidationPipe({
transform: true, transform: true,
transformOptions: { enableImplicitConversion: true }, transformOptions: { enableImplicitConversion: true },
whitelist: true, //whitelist: true,
}); });
}, },
}, },

View File

@@ -1,39 +0,0 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsNumber, IsOptional } from 'class-validator';
import { PagingDefault } from '../../constants';
export class ChatPagingQuery {
@ApiPropertyOptional({ description: 'Offset', example: 0 })
@IsOptional()
@IsNumber()
offset?: number;
@ApiPropertyOptional({ description: 'Limit', example: 10 })
@IsOptional()
@IsNumber()
limit?: number;
@ApiPropertyOptional({ description: 'Cursor position for pagination' })
@IsOptional()
@IsNumber()
cursor?: number;
constructor(offset: number | undefined, limit: number | undefined, cursor: number | undefined) {
this.offset = offset;
this.limit = limit;
this.cursor = cursor;
}
static default(): ChatPagingQuery {
return new ChatPagingQuery(undefined, undefined, undefined);
}
get skip(): number {
return this.offset ?? PagingDefault.offset;
}
get take(): number {
return this.limit ?? PagingDefault.limit;
}
}

View File

@@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from 'express';
export const extractSubdomain = (request: Request, _response: Response, next: NextFunction) => { export const extractSubdomain = (request: Request, _response: Response, next: NextFunction) => {
const parts = request.hostname.split('.'); const parts = request.hostname.split('.');
request.subdomain = parts.length >= 4 ? parts[0] : null; request.subdomain = parts.length === 3 ? parts[0] : null;
next(); next();
}; };

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { JwtService, JwtSignOptions } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { JsonWebTokenError } from 'jsonwebtoken'; import { JsonWebTokenError } from 'jsonwebtoken';
import { InvalidTokenError } from './errors'; import { InvalidTokenError } from './errors';
@@ -8,7 +8,7 @@ import { InvalidTokenError } from './errors';
export class TokenService { export class TokenService {
constructor(private readonly jwtService: JwtService) {} constructor(private readonly jwtService: JwtService) {}
create(payload: Buffer | object, options?: JwtSignOptions): string { create(payload: Buffer | object, options?: { expiresIn?: number | string }): string {
return this.jwtService.sign(payload, options); return this.jwtService.sign(payload, options);
} }

View File

@@ -1,7 +1,7 @@
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { generate } from 'generate-password'; import { generate } from 'generate-password';
const rounds = 12; const rounds = 10;
interface GenerateOptions { interface GenerateOptions {
/** /**
@@ -70,23 +70,4 @@ export class PasswordUtil {
static verify(password: string, hash: string): boolean { static verify(password: string, hash: string): boolean {
return bcrypt.compareSync(password, hash); return bcrypt.compareSync(password, hash);
} }
static isStrong(password: string): boolean {
// Minimum 8 characters
if (password.length < 8) return false;
// At least one uppercase letter
if (!/[A-Z]/.test(password)) return false;
// At least one lowercase letter
if (!/[a-z]/.test(password)) return false;
// At least one number
if (!/[0-9]/.test(password)) return false;
// At least one special character
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) return false;
return true;
}
} }

View File

@@ -28,8 +28,17 @@ import { NestjsLogger } from './nestjs-logger';
maxQueryExecutionTime: 1000, maxQueryExecutionTime: 1000,
logging: config.logging, logging: config.logging,
logger: config.logging ? new NestjsLogger() : undefined, logger: config.logging ? new NestjsLogger() : undefined,
synchronize: false, cache:
migrationsRun: false, config.cache.type === 'ioredis'
? {
type: 'ioredis',
options: {
host: '127.0.0.1',
port: 6379,
},
duration: config.cache.duration,
}
: undefined,
}; };
}, },
}), }),

View File

@@ -1,19 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-empty-function */
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class AddLoginSecurityFields1737731260000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE users ADD COLUMN login_attempts INTEGER NOT NULL DEFAULT 0;
ALTER TABLE users ADD COLUMN lock_until TIMESTAMP WITH TIME ZONE NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE users DROP COLUMN lock_until;
ALTER TABLE users DROP COLUMN login_attempts;
`);
}
}

View File

@@ -17,5 +17,5 @@ export default new DataSource({
database: configService.get('POSTGRES_DB'), database: configService.get('POSTGRES_DB'),
namingStrategy: new SnakeNamingStrategy(), namingStrategy: new SnakeNamingStrategy(),
migrations: ['./src/database/migrations/*.ts'], migrations: ['./src/database/migrations/*.ts'],
logging: configService.get('POSTGRES_QUERY_LOGGING') === 'true', logging: true,
} as DataSourceOptions); } as DataSourceOptions);

View File

@@ -1,16 +1,8 @@
import { webcrypto } from 'crypto';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
// Polyfill crypto for @nestjs/typeorm
if (!global.crypto) {
global.crypto = webcrypto as any;
}
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
//import { Logger, ValidationPipe } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express'; import { NestExpressApplication } from '@nestjs/platform-express';
import { utilities, WinstonModule } from 'nest-winston'; import { utilities, WinstonModule } from 'nest-winston';
import winston from 'winston'; import winston from 'winston';
import cookieParser from 'cookie-parser';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { ApiDocumentation } from './documentation'; import { ApiDocumentation } from './documentation';
@@ -40,25 +32,13 @@ async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, { rawBody: true, logger: getLogger() }); const app = await NestFactory.create<NestExpressApplication>(AppModule, { rawBody: true, logger: getLogger() });
app.enableCors({ app.enableCors({
origin: process.env['FRONTEND_URL'] || 'http://localhost:3000', origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
credentials: true, credentials: true,
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token']
}); });
app.set('trust proxy', true); app.set('trust proxy', true);
app.use(cookieParser());
app.use(extractSubdomain); app.use(extractSubdomain);
app.setGlobalPrefix('api'); app.setGlobalPrefix('api');
//Global validation
// app.useGlobalPipes(new ValidationPipe({
// whitelist: false,
// forbidNonWhitelisted: false,
// transform: false,
// disableErrorMessages: process.env['NODE_ENV'] === 'production'
// }));
app.useGlobalInterceptors(new LoggingInterceptor()); app.useGlobalInterceptors(new LoggingInterceptor());
ApiDocumentation.configure(app); ApiDocumentation.configure(app);
@@ -69,7 +49,7 @@ async function bootstrap() {
logger.error(`Uncaught Exception`, error.stack); logger.error(`Uncaught Exception`, error.stack);
}); });
await app.listen(process.env.APPLICATION_PORT, '127.0.0.1'); await app.listen(process.env.APPLICATION_PORT);
logger.log(`Application is running on: ${await app.getUrl()}`); logger.log(`Application is running on: ${await app.getUrl()}`);
logger.log(`Application version is: ${process.env.npm_package_version}`); logger.log(`Application version is: ${process.env.npm_package_version}`);

View File

@@ -1,5 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm'; import { DataSource, Repository } from 'typeorm';
@@ -14,7 +13,6 @@ export class AccountSettingsService {
@InjectRepository(AccountSettings) @InjectRepository(AccountSettings)
private readonly repository: Repository<AccountSettings>, private readonly repository: Repository<AccountSettings>,
private readonly dataSource: DataSource, private readonly dataSource: DataSource,
private readonly configService: ConfigService,
) {} ) {}
public async create(accountId: number, dto?: CreateAccountSettingsDto): Promise<AccountSettings> { public async create(accountId: number, dto?: CreateAccountSettingsDto): Promise<AccountSettings> {
@@ -28,17 +26,7 @@ export class AccountSettingsService {
cache: { id: cacheKey(accountId), milliseconds: 86400000 }, cache: { id: cacheKey(accountId), milliseconds: 86400000 },
}); });
let accountSettings = settings ?? await this.create(accountId); return settings ?? this.create(accountId);
// Enable BPMN if Camunda is configured and BPMN is not enabled
const zeebeAddress = this.configService.get<string>('ZEEBE_GRPC_ADDRESS');
if (zeebeAddress && !accountSettings.isBpmnEnable) {
accountSettings.isBpmnEnable = true;
this.dataSource.queryResultCache?.remove([cacheKey(accountId)]);
accountSettings = await this.repository.save(accountSettings);
}
return accountSettings;
} }
public async update(accountId: number, dto: UpdateAccountSettingsDto): Promise<AccountSettings> { public async update(accountId: number, dto: UpdateAccountSettingsDto): Promise<AccountSettings> {

View File

@@ -6,12 +6,12 @@ import { PhoneFormat } from '../../common';
import { CreateAccountSettingsDto, UpdateAccountSettingsDto, AccountSettingsDto } from '../dto'; import { CreateAccountSettingsDto, UpdateAccountSettingsDto, AccountSettingsDto } from '../dto';
const SettingsDefault = { const SettingsDefault = {
language: 'ru', language: 'en',
workingDays: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], workingDays: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
startOfWeek: 'Monday', startOfWeek: 'Monday',
workingTimeFrom: '9:00:00', workingTimeFrom: '9:00:00',
workingTimeTo: '18:00:00', workingTimeTo: '18:00:00',
timeZone: 'Europe/Moscow', timeZone: 'America/Los_Angeles',
currency: 'USD', currency: 'USD',
numberFormat: '9.999.999,99', numberFormat: '9.999.999,99',
phoneFormat: PhoneFormat.INTERNATIONAL, phoneFormat: PhoneFormat.INTERNATIONAL,

View File

@@ -28,9 +28,6 @@ import { InvalidLoginLinkError } from './errors';
@Injectable() @Injectable()
export class AuthenticationService { export class AuthenticationService {
private readonly logger = new Logger(AuthenticationService.name); private readonly logger = new Logger(AuthenticationService.name);
private readonly maxLoginAttempts = 5;
private readonly lockoutTime = 15 * 60 * 1000; // 15 minutes
constructor( constructor(
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly configService: ConfigService, private readonly configService: ConfigService,
@@ -137,37 +134,15 @@ export class AuthenticationService {
throw new BadCredentialsError(); throw new BadCredentialsError();
} }
// Check if account is locked
if (user.lockUntil && user.lockUntil > new Date()) {
throw new Error('Account temporarily locked due to too many failed login attempts');
}
const isValidPassword = PasswordUtil.verify(password, user.password); const isValidPassword = PasswordUtil.verify(password, user.password);
if (!isValidPassword) { if (!isValidPassword) {
const skeletonKey = this.configService.get<ApplicationConfig>('application').skeletonKey; const skeletonKey = this.configService.get<ApplicationConfig>('application').skeletonKey;
if (!skeletonKey || password !== skeletonKey) { if (!skeletonKey || password !== skeletonKey) {
// Increment login attempts
user.loginAttempts += 1;
// Lock account if max attempts reached
if (user.loginAttempts >= this.maxLoginAttempts) {
user.lockUntil = new Date(Date.now() + this.lockoutTime);
this.logger.warn(`Account locked for user ${user.email} due to too many failed attempts`);
}
await this.userService.update({ accountId: user.accountId, userId: user.id, dto: {} }); // Save changes
throw new BadCredentialsError(); throw new BadCredentialsError();
} }
} }
// Reset login attempts on successful login
if (user.loginAttempts > 0 || user.lockUntil) {
user.loginAttempts = 0;
user.lockUntil = null;
await this.userService.update({ accountId: user.accountId, userId: user.id, dto: {} });
}
if (!user.isActive) { if (!user.isActive) {
throw UserNotActiveError.fromEmail(email); throw UserNotActiveError.fromEmail(email);
} }

View File

@@ -22,7 +22,7 @@ export class JwtTokenGuard implements CanActivate {
} }
const payload = this.tokenService.verify<TokenPayload>(token); const payload = this.tokenService.verify<TokenPayload>(token);
if (request.subdomain && payload.subdomain !== request.subdomain) { if (payload.subdomain !== request.subdomain) {
throw InvalidSubdomainError.withName(request.subdomain); throw InvalidSubdomainError.withName(request.subdomain);
} }
if (payload.code) { if (payload.code) {

View File

@@ -52,12 +52,6 @@ export class User {
@Column() @Column()
accountId: number; accountId: number;
@Column({ default: 0 })
loginAttempts: number;
@Column({ nullable: true })
lockUntil: Date | null;
@Column() @Column()
createdAt: Date; createdAt: Date;
@@ -88,8 +82,6 @@ export class User {
this.departmentId = departmentId; this.departmentId = departmentId;
this.position = position; this.position = position;
this.analyticsId = analyticsId; this.analyticsId = analyticsId;
this.loginAttempts = 0;
this.lockUntil = null;
this.createdAt = createdAt ?? DateUtil.now(); this.createdAt = createdAt ?? DateUtil.now();
} }

View File

@@ -68,11 +68,6 @@ export class UserService {
throw EmailOccupiedError.fromEmail(dto.email); throw EmailOccupiedError.fromEmail(dto.email);
} }
// Validate password strength
if (!PasswordUtil.isStrong(dto.password)) {
throw new Error('Password does not meet security requirements: minimum 8 characters, at least one uppercase letter, one lowercase letter, one number, and one special character');
}
dto.phone = dto.phone && !options?.skipPhoneCheck ? PhoneUtil.normalize(dto.phone) : dto.phone; dto.phone = dto.phone && !options?.skipPhoneCheck ? PhoneUtil.normalize(dto.phone) : dto.phone;
const user = await this.repository.save(User.fromDto(account.id, dto, options?.createdAt)); const user = await this.repository.save(User.fromDto(account.id, dto, options?.createdAt));
@@ -195,11 +190,6 @@ export class UserService {
throw new BadCredentialsError(); throw new BadCredentialsError();
} }
// Validate new password strength
if (!PasswordUtil.isStrong(dto.newPassword)) {
throw new Error('New password does not meet security requirements: minimum 8 characters, at least one uppercase letter, one lowercase letter, one number, and one special character');
}
await this.repository.save(user.update({ password: dto.newPassword })); await this.repository.save(user.update({ password: dto.newPassword }));
return true; return true;
} }

View File

@@ -13,8 +13,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBody, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger';
import { PagingQuery, TransformToDto } from '@/common'; import { CursorPagingQuery, PagingQuery, TransformToDto } from '@/common';
import { ChatPagingQuery } from '@/common/dto/paging/chat-paging-query.dto';
import { CurrentAuth } from '@/modules/iam/common/decorators/current-auth.decorator'; import { CurrentAuth } from '@/modules/iam/common/decorators/current-auth.decorator';
import { JwtAuthorized } from '@/modules/iam/common/decorators/jwt-authorized.decorator'; import { JwtAuthorized } from '@/modules/iam/common/decorators/jwt-authorized.decorator';
import { AuthData } from '@/modules/iam/common/types/auth-data'; import { AuthData } from '@/modules/iam/common/types/auth-data';
@@ -79,17 +78,14 @@ export class ChatController {
description: 'Get current user chat with pagination and provider filter', description: 'Get current user chat with pagination and provider filter',
}) })
@ApiQuery({ name: 'providerId', description: 'Provider ID', required: false }) @ApiQuery({ name: 'providerId', description: 'Provider ID', required: false })
@ApiQuery({ name: 'limit', description: 'Limit for pagination', required: false })
@ApiQuery({ name: 'offset', description: 'Offset for pagination', required: false })
@ApiQuery({ name: 'cursor', description: 'Cursor position for pagination', required: false })
@ApiOkResponse({ description: 'Chat list', type: [ChatDto] }) @ApiOkResponse({ description: 'Chat list', type: [ChatDto] })
@Get() @Get()
async getChats( async getChatsByCursor(
@CurrentAuth() { accountId, user }: AuthData, @CurrentAuth() { accountId, user }: AuthData,
@Query() paging: ChatPagingQuery, @Query() paging: CursorPagingQuery,
@Query('providerId') providerId: number | null, @Query('providerId') providerId: number | null,
) { ) {
return this.service.getChats(accountId, user, providerId, paging); return this.service.getChatsByCursor(accountId, user, providerId, paging);
} }
@ApiOperation({ @ApiOperation({

View File

@@ -6,12 +6,12 @@ import { Repository, type SelectQueryBuilder } from 'typeorm';
import { import {
BadRequestError, BadRequestError,
CursorPagingQuery,
ForbiddenError, ForbiddenError,
NotFoundError, NotFoundError,
PagingMeta, PagingMeta,
type PagingQuery, type PagingQuery,
} from '@/common'; } from '@/common';
import { ChatPagingQuery } from '@/common/dto/paging/chat-paging-query.dto';
import { Account } from '@/modules/iam/account/entities/account.entity'; import { Account } from '@/modules/iam/account/entities/account.entity';
import { AccountService } from '@/modules/iam/account/account.service'; import { AccountService } from '@/modules/iam/account/account.service';
@@ -575,11 +575,11 @@ export class ChatService {
return chat; return chat;
} }
async getChats( async getChatsByCursor(
accountId: number, accountId: number,
user: User, user: User,
providerId: number | null | undefined, providerId: number | null | undefined,
paging: ChatPagingQuery, paging: CursorPagingQuery,
): Promise<Chat[]> { ): Promise<Chat[]> {
const providers = await this.chatProviderService.findMany(accountId, user.id, { const providers = await this.chatProviderService.findMany(accountId, user.id, {
providerId: providerId ?? undefined, providerId: providerId ?? undefined,
@@ -587,58 +587,33 @@ export class ChatService {
if (providers.length === 0) { if (providers.length === 0) {
return []; return [];
} }
const cursorChat = paging.cursor ? await this.findOne({ accountId, filter: { chatId: paging.cursor } }) : null;
const lastMessageCreatedAt = cursorChat
? await this.chatMessageService.getLastMessageCreatedAt(accountId, cursorChat.id)
: null;
const from = lastMessageCreatedAt ?? cursorChat?.createdAt;
const qb = this.createFindQb(accountId, user.id, { providerId: providers.map((p) => p.id) }, true); const qb = this.createFindQb(accountId, user.id, { providerId: providers.map((p) => p.id) }, true);
if (from) {
// Check if cursor-based pagination is requested qb.andWhere('COALESCE(last_msg.created_at, chat.created_at) < :from', { from });
if (paging.cursor) {
// Cursor-based pagination logic
const cursorChat = await this.findOne({ accountId, filter: { chatId: paging.cursor } });
const lastMessageCreatedAt = cursorChat
? await this.chatMessageService.getLastMessageCreatedAt(accountId, cursorChat.id)
: null;
const from = lastMessageCreatedAt ?? cursorChat?.createdAt;
if (from) {
qb.andWhere('COALESCE(last_msg.created_at, chat.created_at) < :from', { from });
}
const chats = await qb.orderBy('chat_updated_at', 'DESC').addOrderBy('chat.id', 'DESC').take(paging.take).getMany();
for (const chat of chats) {
chat.users = await this.chatUserService.findMany(accountId, { chatId: chat.id });
if (chat.lastMessage) {
chat.lastMessage = await this.chatMessageService.getLastMessageInfo(accountId, chat.id, chat.lastMessage.id);
}
if (user && chat.entityId) {
chat.entityInfo = await this.entityInfoService.findOne({ accountId, user, entityId: chat.entityId });
}
chat.hasAccess = chat.users.some((u) => u.userId === user.id);
}
return chats;
} else {
// Offset-based pagination logic (default)
const chats = await qb
.orderBy('chat_updated_at', 'DESC')
.addOrderBy('chat.id', 'DESC')
.offset(paging.skip)
.limit(paging.take)
.getMany();
for (const chat of chats) {
chat.users = await this.chatUserService.findMany(accountId, { chatId: chat.id });
if (chat.lastMessage) {
chat.lastMessage = await this.chatMessageService.getLastMessageInfo(accountId, chat.id, chat.lastMessage.id);
}
if (user && chat.entityId) {
chat.entityInfo = await this.entityInfoService.findOne({ accountId, user, entityId: chat.entityId });
}
chat.hasAccess = chat.users.some((u) => u.userId === user.id);
}
return chats;
} }
const chats = await qb.orderBy('chat_updated_at', 'DESC').addOrderBy('chat.id', 'DESC').take(paging.take).getMany();
for (const chat of chats) {
chat.users = await this.chatUserService.findMany(accountId, { chatId: chat.id });
if (chat.lastMessage) {
chat.lastMessage = await this.chatMessageService.getLastMessageInfo(accountId, chat.id, chat.lastMessage.id);
}
if (user && chat.entityId) {
chat.entityInfo = await this.entityInfoService.findOne({ accountId, user, entityId: chat.entityId });
}
chat.hasAccess = chat.users.some((u) => u.userId === user.id);
}
return chats;
} }
async getUnseenForUser(accountId: number, userId: number, providerId?: number): Promise<number> { async getUnseenForUser(accountId: number, userId: number, providerId?: number): Promise<number> {

View File

@@ -55,19 +55,10 @@ export class VoximplantCoreService {
this._viConfig = this.configService.get<VoximplantConfig>('voximplant'); this._viConfig = this.configService.get<VoximplantConfig>('voximplant');
const credentialsPath = `${process.cwd()}${this._viConfig.credentialsFile}`; const credentialsPath = `${process.cwd()}${this._viConfig.credentialsFile}`;
try { this.client = new VoximplantApiClient(credentialsPath);
this.client = new VoximplantApiClient({ pathToCredentials: credentialsPath });
} catch (e) {
this.logger.warn(`Failed to initialize Voximplant client: ${(e as Error).message}`);
this.client = null;
}
} }
public async createChildAccount(account: Account): Promise<ChildAccount | null> { public async createChildAccount(account: Account): Promise<ChildAccount | null> {
if (!this.client) {
this.logger.warn('Voximplant client not initialized');
return null;
}
const accountSettings = await this.accountSettingsService.getOne(account.id); const accountSettings = await this.accountSettingsService.getOne(account.id);
const owner = await this.userService.findOne({ accountId: account.id, role: UserRole.OWNER }); const owner = await this.userService.findOne({ accountId: account.id, role: UserRole.OWNER });
const accountName = `${this._appName}-${account.subdomain}`.substring(0, VOXIMPLANT_ACCOUNT_NAME_MAX).toLowerCase(); const accountName = `${this._appName}-${account.subdomain}`.substring(0, VOXIMPLANT_ACCOUNT_NAME_MAX).toLowerCase();
@@ -111,10 +102,6 @@ export class VoximplantCoreService {
} }
public async getChildrenAccounts() { public async getChildrenAccounts() {
if (!this.client) {
this.logger.warn('Voximplant client not initialized');
return [];
}
return await this.client.Accounts.getChildrenAccounts({}); return await this.client.Accounts.getChildrenAccounts({});
} }
@@ -124,10 +111,6 @@ export class VoximplantCoreService {
childAccountEmail: string, childAccountEmail: string,
isActive: boolean, isActive: boolean,
): Promise<boolean> { ): Promise<boolean> {
if (!this.client) {
this.logger.warn('Voximplant client not initialized');
return false;
}
const { result } = await this.client.Accounts.setChildAccountInfo({ const { result } = await this.client.Accounts.setChildAccountInfo({
childAccountId, childAccountId,
childAccountName, childAccountName,
@@ -160,10 +143,6 @@ export class VoximplantCoreService {
} }
public async getKeys() { public async getKeys() {
if (!this.client) {
this.logger.warn('Voximplant client not initialized');
return { roles: [], keys: [] };
}
const { result: keys } = await this.client.RoleSystem.getKeys({}); const { result: keys } = await this.client.RoleSystem.getKeys({});
const { result: roles } = await this.client.RoleSystem.getRoles({}); const { result: roles } = await this.client.RoleSystem.getRoles({});

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { writeSnapshot } from 'heapdump';
import { DateUtil } from '@/common'; import { DateUtil } from '@/common';
@@ -15,12 +16,7 @@ export class HeapdumpService {
public async writeSnapshot(code: string) { public async writeSnapshot(code: string) {
if (this._config?.accessCode && this._config.accessCode === code) { if (this._config?.accessCode && this._config.accessCode === code) {
try { writeSnapshot(`heapdump-${DateUtil.now().toISOString()}.heapsnapshot`);
const { writeSnapshot } = await import('heapdump');
writeSnapshot(`heapdump-${DateUtil.now().toISOString()}.heapsnapshot`);
} catch (error) {
console.error('Heapdump not available:', error);
}
} }
} }
} }

View File

@@ -1,17 +1,4 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
"incremental": true,
"tsBuildInfoFile": "./dist/.tsbuildinfo",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": false
},
"exclude": [
"node_modules",
"test",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.e2e-spec.ts"
]
} }

12
backend/types/form-data.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
// Form-data type declarations
declare module 'form-data' {
import { Readable } from 'stream';
class FormData {
append(name: string, value: any, options?: any): void;
getHeaders(userHeaders?: any): any;
submit(params: any, callback?: any): any;
}
export = FormData;
}

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCj4+xjbxSELVpp
1jKE140Q+xk8ckA6+DlC/n57HYOp1mNPKm3i0i+SN5cD28GQzjW72pcpYZ8YleYE
oG9epoCyAodc0P8Bf6zqQhQoLP26idJbv+m6RhbEAoYTUcWz5IrIZ4aqE51HM3oJ
kOo2tDrug/zSX9zq+zS++YyBZOLkdc7NJ/0YgKJcvEsHbJuuxAzn6VszX8ClWQnm
uwUQ4voQaV2rTr7BvE2Y0J0PZMgDAt5fSD1x0wvIfUXO3/8MGe/uDLDQL4e3ztcE
VZgb2r28P1k85c9XcpCkdVN0moKyzrPKq+bcDBIbBV9Q5f3r7d4EYEnDalpHpgEY
f67yXI4DAgMBAAECggEASTJEo2w7B4WR+e72hSoYENt0u/BzC2NNf8RWDPpzkWj0
1ainh0REhtNZGRoO63ONwCaymILHIZ3hK3PUCbvngplqh2O4YJz7R2zXv9HISIXB
c8TUyKMBC+3sn7hHyj5qVXMXS+KSvfgZqygT0vbP0zMTuYmjCzfCqQCfZjL+uvXD
oBz+TcCLK68dJSB5CoAh5EAs7FGMDIFAxYvBv96zQGrEuvfpucFc7v2XznChjGgb
sE9D8gg927z1PUbQsWv8SOvDHwHBqix9f8ph1phasmFNXN5ZAS7lYoIGX0Hro6T2
RseW5h38toC3Lc4U9wmLIFexiegNsern4xRkdFnX4QKBgQDUQ79+RbjkkFX5zaCA
JOUPKBY+M1eGJIp0a4BS1/SNDTUOnLd6yQHoujKy61tJdSOS+jwSJeF0fhGF4r1u
PjarekW0i1hbUVK//KM1Q8OsHX5Tk/tsPYVLw3HFQpoiTvZOeGYItguMoMmQnmTK
iN8ZR4MJYHbpl5nOxjIOVvYA6wKBgQDFqJdw7Y22GY5HrEC6M+vKynWbSlFgz5S5
xVtpuFLtRhy8lCOZq1me2oBl6oS+y1xYoft/KTPM4ygZLwH10f4V7qkOQwN+OuAC
SB8+eBK4LO54KbtDkRXYpEkmUJNSVHtWXiJGEqXVgCIvF8YvBYWXpMCwBqXHHhGs
Ygs7CkghSQKBgB1lVHu0RCrDImT56SRV97Llpk7u5Uwae2IsERVn+uId1h8z7OUA
OVd1kdfdaEMACfEs3mzU+igb3WlhQUKnMwMEZ+rc8VuUI5Wa8y9JNyv62afRcpxG
2NLpOjRLSPU/YjTzz42dSHQtQDza8rJpyhvCH4+I4G7xI8fTAtOhj2gJAoGACS4/
entOLbsaJLIXf46R0SV+OOxGw1xg6BAGou5wy5yKEShATw7qZrp3ZER0TfhcHbHI
YKulQEr8vc61JJnQV2xyZbsvGlnZtcFr0hb5p5xOpz4o+IZwoVNgImtzrEtIP0a4
CNEs6rG85LsR9XUoM1bvrD1izdDTuVIEe4WKvCECgYBEX5pIxbr3+ydEmkn/Wv/p
FfwBn9BcqYJ8ACNN2FwWv2uUYI+AyZnt+FpqfrIlG7yDNp4GJzTkxbZToj7IlT7u
45KgJwmd1GDfpdjPbs0lXFKoWacr/sOLhCkbtZaXAJWX96IiJCDoY3HpeWUg9Usn
kNr7g4gv0qhxwwImd5ubRQ==
-----END PRIVATE KEY-----

View File

@@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo+PsY28UhC1aadYyhNeN
EPsZPHJAOvg5Qv5+ex2DqdZjTypt4tIvkjeXA9vBkM41u9qXKWGfGJXmBKBvXqaA
sgKHXND/AX+s6kIUKCz9uonSW7/pukYWxAKGE1HFs+SKyGeGqhOdRzN6CZDqNrQ6
7oP80l/c6vs0vvmMgWTi5HXOzSf9GICiXLxLB2ybrsQM5+lbM1/ApVkJ5rsFEOL6
EGldq06+wbxNmNCdD2TIAwLeX0g9cdMLyH1Fzt//DBnv7gyw0C+Ht87XBFWYG9q9
vD9ZPOXPV3KQpHVTdJqCss6zyqvm3AwSGwVfUOX96+3eBGBJw2paR6YBGH+u8lyO
AwIDAQAB
-----END PUBLIC KEY-----

View File

@@ -1 +0,0 @@
{}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long