Update dependencies

This commit is contained in:
Денис 2026-02-17 15:51:33 +03:00
parent 87224f0816
commit f69ffd1baf
10 changed files with 1127 additions and 1046 deletions

View File

@ -10,7 +10,7 @@ DB_PASSWORD=Strong_Password_123456
# Back
BACK_PORT=4000
JWT_SECRET=SECRET_STRING # How to generate: require('crypto').randomBytes(32).toString('hex')
TELEGRAM_BOT_TOKEN=...
TELEGRAM_BOT_TOKEN=1234567890:AAbbCCddEeffGG5hhj2kOpqqRRSssttuvv
# Front
FRONT_PORT=3000

2
.gitignore vendored
View File

@ -4,3 +4,5 @@
.env.test.local
.env.production.local
.env.local
.pnpm-store

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
22.13

View File

@ -3,10 +3,14 @@
A starter template for building a Telegram bot with a web-based admin panel.
**Stack:**
- **Backend:** NestJS, TypeORM, PostgreSQL, Grammy (Telegram Bot)
- **Backend:** NestJS 11, TypeORM, PostgreSQL, Grammy (Telegram Bot)
- **Frontend:** React, Vite, Mantine UI
- **Infrastructure:** Docker Compose
**Requirements:**
- Node.js v20+ (v22 recommended)
- pnpm 9+
## Getting Started
### 1. Set up environment

View File

@ -18,6 +18,6 @@ RUN \
else echo "pnpm-lock.yaml not found." && exit 1; \
fi
RUN pnpm add -g @nestjs/cli
RUN pnpm add -g @nestjs/cli@^11.0.0
COPY . .

View File

@ -27,18 +27,17 @@
"console": "node dist/console.js"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/common": "^11.0.0",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/core": "^11.0.0",
"@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "^2.0.5",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-express": "^11.0.0",
"@nestjs/schedule": "^6.0.0",
"@nestjs/serve-static": "^4.0.2",
"@nestjs/typeorm": "^10.0.2",
"@nestjs/typeorm": "^11.0.0",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"class-validator": "^0.14.3",
"grammy": "^1.36.1",
"helmet": "^7.1.0",
"multer": "1.4.5-lts.2",
@ -50,14 +49,14 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.0",
"@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.17",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/multer": "^1.4.12",
"@types/node": "^20.3.1",
"@types/node": "^22.0.0",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,6 @@ import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { CommonModule } from './common/common.module';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { validate } from './config/env/validate';
import { DatabaseModule } from './database/database.module';
import { AdminsModule } from './admins/admins.module';
@ -15,10 +13,6 @@ import { BotModule } from './bot/bot.module';
@Module({
imports: [
ConfigModule.forRoot({ validate }),
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'uploads'),
serveRoot: '/uploads',
}),
CommonModule,
DatabaseModule,
AdminsModule,

View File

@ -1,12 +1,26 @@
import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common';
import { IsNull, Not, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual, ILike, In, Between } from "typeorm";
import {
BadRequestException,
createParamDecorator,
ExecutionContext,
} from '@nestjs/common';
import { Request } from 'express';
import {
Between,
ILike,
In,
IsNull,
LessThan,
LessThanOrEqual,
MoreThan,
MoreThanOrEqual,
Not,
} from 'typeorm';
import { ErrorCode } from '../enums/error-code.enum';
export type EntityFields<T> = (keyof T)[];
export interface IFiltering {
[field: string]: string;
[key: string]: unknown;
}
interface IFilteringParams {
@ -37,46 +51,115 @@ export const getWhere = (filter: IFilteringParams) => {
if (filter.rule == FilterRule.IS_NULL) return IsNull();
if (filter.rule == FilterRule.IS_NOT_NULL) return Not(IsNull());
if (filter.rule == FilterRule.EQUALS) return filter.value;
if (filter.rule == FilterRule.NOT_EQUALS) return Not(filter.value);
if (filter.rule == FilterRule.EQUALS) {
if (!filter.value || filter.value.trim() === '') return {};
return filter.value;
}
if (filter.rule == FilterRule.NOT_EQUALS) {
if (!filter.value || filter.value.trim() === '') return {};
return Not(filter.value);
}
if (filter.rule == FilterRule.GREATER_THAN) return MoreThan(filter.value);
if (filter.rule == FilterRule.GREATER_THAN_OR_EQUALS) return MoreThanOrEqual(filter.value);
if (filter.rule == FilterRule.LESS_THAN) return LessThan(filter.value);
if (filter.rule == FilterRule.LESS_THAN_OR_EQUALS) return LessThanOrEqual(filter.value);
if (filter.rule == FilterRule.LIKE) return ILike(`%${filter.value}%`);
if (filter.rule == FilterRule.NOT_LIKE) return Not(ILike(`%${filter.value}%`));
if (filter.rule == FilterRule.IN) return In(filter.value.split(','));
if (filter.rule == FilterRule.NOT_IN) return Not(In(filter.value.split(',')));
if (filter.rule == FilterRule.BETWEEN) return Between(...(filter.value.split(',') as [string, string]));
}
export const FilteringParams = createParamDecorator((data, ctx: ExecutionContext): IFiltering => {
const req: Request = ctx.switchToHttp().getRequest();
const queryFilters = req.query.filters as string[];
if (!queryFilters || !Array.isArray(queryFilters)) return null;
if (typeof data !== 'object') throw new BadRequestException(ErrorCode.InvalidFilterParams);
let filters: { [field: string]: any } = {};
for (const filter of queryFilters) {
const [fieldPath, rule, value] = filter.split(':');
const fieldParts = fieldPath.split('.');
const field = fieldParts.pop();
let nestedFilters = filters;
for (const part of fieldParts) {
nestedFilters[part] = nestedFilters[part] || {};
nestedFilters = nestedFilters[part];
}
if (!data.includes(fieldPath)) throw new BadRequestException(`${ErrorCode.FilterFieldNotAllowed}:${field}`);
if (!Object.values(FilterRule).includes(rule as FilterRule)) throw new BadRequestException(`${ErrorCode.InvalidFilterParams}:${rule}`);
const whereClause = getWhere({ field, rule, value });
nestedFilters[field] = whereClause;
if (filter.rule == FilterRule.IN) {
const values = filter.value.split(',').filter((v) => v.trim() !== '');
return values.length > 0 ? In(values) : {};
}
if (filter.rule == FilterRule.NOT_IN) {
const values = filter.value.split(',').filter((v) => v.trim() !== '');
return values.length > 0 ? Not(In(values)) : {};
}
if (filter.rule == FilterRule.BETWEEN) {
const [from, to] = filter.value.split(',') as [string?, string?];
if (!from || !to) return {};
return Between(from, to);
}
return filters;
});
return {};
}
function ensureObjectRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export const FilteringParams = createParamDecorator(
(data: readonly string[], ctx: ExecutionContext): IFiltering | null => {
const req: Request = ctx.switchToHttp().getRequest();
const rawFilters = req.query.filters;
const queryFilters = Array.isArray(rawFilters)
? rawFilters
: typeof rawFilters === 'string'
? [rawFilters]
: null;
if (!queryFilters) return null;
if (!Array.isArray(data)) throw new BadRequestException(ErrorCode.InvalidFilterParams);
const filters: IFiltering = {};
for (const filter of queryFilters) {
if (typeof filter !== 'string') {
throw new BadRequestException(ErrorCode.InvalidFilterParams);
}
const [fieldPath, rule, value] = filter.split(':');
if (!fieldPath || !rule || value === undefined) {
throw new BadRequestException(ErrorCode.InvalidFilterParams);
}
const fieldParts = fieldPath.split('.');
const field = fieldParts.pop();
if (!field) {
throw new BadRequestException(ErrorCode.InvalidFilterParams);
}
let nestedFilters: Record<string, unknown> = filters;
for (const part of fieldParts) {
const existing = nestedFilters[part];
if (!ensureObjectRecord(existing)) {
nestedFilters[part] = {};
}
nestedFilters = nestedFilters[part] as Record<string, unknown>;
}
if (!data.includes(fieldPath)) {
throw new BadRequestException(`${ErrorCode.FilterFieldNotAllowed}:${field}`);
}
if (!Object.values(FilterRule).includes(rule as FilterRule)) {
throw new BadRequestException(`${ErrorCode.InvalidFilterParams}:${rule}`);
}
const isEmptyValueRule = [
FilterRule.EQUALS,
FilterRule.NOT_EQUALS,
FilterRule.IN,
FilterRule.NOT_IN,
].includes(rule as FilterRule) && (!value || value.trim() === '');
if (isEmptyValueRule) {
continue;
}
const whereClause = getWhere({ field, rule, value });
if (ensureObjectRecord(whereClause) && Object.keys(whereClause).length === 0) {
continue;
}
nestedFilters[field] = whereClause;
}
return filters;
},
);

View File

@ -1,12 +1,16 @@
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory, Reflector } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import helmet from 'helmet';
import express from 'express';
import { join } from 'path';
import { AppModule } from './app.module';
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
const expressApp = express();
const app = await NestFactory.create(AppModule, new ExpressAdapter(expressApp), {
// logger: process.env.NODE_ENV === 'production'
// ? ['error', 'warn', 'log']
// : ['error', 'warn', 'log', 'debug', 'verbose'],
@ -17,6 +21,10 @@ async function bootstrap() {
const PORT = configService.get<number>('PORT');
const isDevelop = configService.get<string>('NODE_ENV');
// Configure static file serving for uploads
const uploadsPath = join(__dirname, '..', 'uploads');
expressApp.use('/uploads', express.static(uploadsPath));
const helmetConfig = helmet({
contentSecurityPolicy: {
directives: {