Update dependencies
This commit is contained in:
parent
87224f0816
commit
f69ffd1baf
@ -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
2
.gitignore
vendored
@ -4,3 +4,5 @@
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
.pnpm-store
|
||||
@ -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
|
||||
|
||||
@ -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 . .
|
||||
|
||||
@ -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",
|
||||
|
||||
1962
back/pnpm-lock.yaml
1962
back/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
|
||||
@ -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;
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user