chore: update dependencies and enhance authentication mechanisms

- Removed legacy-peer-deps setting from .npmrc.
- Updated package-lock.json and package.json to reflect new versions for several dependencies, including @grammyjs/parse-mode (from ^1.10.0 to ^1.11.1) and @kastov/grammy-nestjs (from 0.4.1 to 0.4.2).
- Improved password hashing in BasicAuthMiddleware and AuthService by replacing bcrypt with scrypt for enhanced security.
- Added exit option to CLI actions for better user experience.
- Introduced custom log filtering in processors and scheduler for improved logging clarity.
This commit is contained in:
kastov 2025-03-18 06:37:28 +03:00
parent 0fc6170742
commit e39f76844f
No known key found for this signature in database
GPG key ID: 1B27BE29057F4C90
12 changed files with 492 additions and 672 deletions

2
.npmrc
View file

@ -1 +1 @@
legacy-peer-deps=true

1035
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -49,8 +49,8 @@
"@bull-board/express": "^6.7.10",
"@bull-board/nestjs": "^6.7.10",
"@cjs-exporter/p-map": "7.0.2",
"@grammyjs/parse-mode": "^1.10.0",
"@kastov/grammy-nestjs": "0.4.1",
"@grammyjs/parse-mode": "^1.11.1",
"@kastov/grammy-nestjs": "0.4.2",
"@nestjs-cls/transactional": "2.5.0",
"@nestjs-cls/transactional-adapter-prisma": "1.2.16",
"@nestjs/axios": "4.0.0",
@ -72,12 +72,11 @@
"@scalar/nestjs-api-reference": "^0.4.1",
"@supercharge/request-ip": "^1.2.0",
"@willsoto/nestjs-prometheus": "^6.0.2",
"axios": "^1.7.9",
"bcrypt": "^5.1.1",
"axios": "^1.8.3",
"bullmq": "^5.41.7",
"class-transformer": "^0.5.1",
"compression": "^1.8.0",
"consola": "^3.4.0",
"consola": "^3.4.1",
"country-code-emoji": "^2.3.0",
"dayjs": "^1.11.13",
"enhanced-ms": "^3.0.0",
@ -94,26 +93,25 @@
"passport": "0.7.0",
"passport-http": "^0.3.0",
"passport-jwt": "4.0.1",
"pkg-types": "^1.3.0",
"pkg-types": "1.3.1",
"pm2": "5.4.3",
"prisma": "^6.4.1",
"prom-client": "^15.1.3",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"scule": "^1.3.0",
"rxjs": "7.8.2",
"semver": "^7.6.3",
"superjson": "1.13.3",
"swagger-themes": "^1.4.3",
"systeminformation": "^5.23.5",
"table": "^6.9.0",
"winston": "^3.17.0",
"xbytes": "^1.9.1",
"yaml": "^2.7.0",
"zod": "^3.22.4"
"zod": "^3.24.2"
},
"devDependencies": {
"@nestjs/cli": "11.0.5",
"@nestjs/schematics": "11.0.2",
"@types/bcrypt": "^5.0.2",
"@types/compression": "^1.7.5",
"@types/express": "^5.0.0",
"@types/js-yaml": "^4.0.9",
@ -121,12 +119,11 @@
"@types/lodash": "^4.17.16",
"@types/morgan": "^1.9.9",
"@types/node": "^22.13.8",
"@types/nunjucks": "^3.2.6",
"@types/passport-http": "^0.3.11",
"@types/passport-jwt": "^4.0.1",
"@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "8.25.0",
"@typescript-eslint/parser": "8.25.0",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"eslint": "9.21.0",
"eslint-config-prettier": "^10.0.2",
"eslint-plugin-paths": "^1.1.0",
@ -139,6 +136,6 @@
"ts-node": "10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "~5.8.2",
"typescript-eslint": "^8.25.0"
"typescript-eslint": "^8.26.1"
}
}

View file

@ -12,6 +12,7 @@ const prisma = new PrismaClient({
});
const enum CLI_ACTIONS {
EXIT = 'exit',
RESET_SUPERADMIN = 'reset-superadmin',
}
@ -77,6 +78,10 @@ async function main() {
label: 'Reset superadmin',
hint: 'Fully reset superadmin',
},
{
value: CLI_ACTIONS.EXIT,
label: 'Exit',
},
],
initial: CLI_ACTIONS.RESET_SUPERADMIN,
});
@ -85,6 +90,9 @@ async function main() {
case CLI_ACTIONS.RESET_SUPERADMIN:
await resetSuperadmin();
break;
case CLI_ACTIONS.EXIT:
consola.info('👋 Exiting...');
process.exit(0);
}
}
main()

View file

@ -6,6 +6,7 @@ import { NestFactory } from '@nestjs/core';
import { NotFoundExceptionFilter } from '@common/exception/not-found-exception.filter';
import { WorkerRoutesGuard } from '@common/guards/worker-routes/worker-routes.guard';
import { customLogFilter } from '@common/utils/filter-logs/filter-logs';
import { isDevelopment } from '@common/utils/startup-app';
import { AxiosService } from '@common/axios';
import { METRICS_ROOT } from '@libs/contracts/api';
@ -27,6 +28,7 @@ const instanceId = process.env.INSTANCE_ID || '0';
const logger = createLogger({
transports: [new winston.transports.Console()],
format: winston.format.combine(
customLogFilter(),
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss.SSS',
}),

View file

@ -10,6 +10,7 @@ import { NestFactory } from '@nestjs/core';
import { NotFoundExceptionFilter } from '@common/exception/not-found-exception.filter';
import { WorkerRoutesGuard } from '@common/guards/worker-routes/worker-routes.guard';
import { customLogFilter } from '@common/utils/filter-logs/filter-logs';
import { isDevelopment } from '@common/utils/startup-app';
import { AxiosService } from '@common/axios';
import { BULLBOARD_ROOT, METRICS_ROOT } from '@libs/contracts/api';
@ -31,6 +32,7 @@ const instanedId = process.env.INSTANCE_ID || '0';
const logger = createLogger({
transports: [new winston.transports.Console()],
format: winston.format.combine(
customLogFilter(),
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss.SSS',
}),

View file

@ -1,5 +1,6 @@
import { randomBytes, scrypt, scryptSync, timingSafeEqual } from 'node:crypto';
import { NextFunction, Request, Response } from 'express';
import * as bcrypt from 'bcrypt';
import { promisify } from 'node:util';
import { Injectable, NestMiddleware } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@ -9,11 +10,19 @@ export class BasicAuthMiddleware implements NestMiddleware {
private readonly username: string;
private readonly password: string;
private readonly passwordHash: string;
private readonly salt: Buffer;
private readonly scryptAsync = promisify(scrypt);
constructor(private readonly configService: ConfigService) {
this.username = this.configService.get<string>('METRICS_USER') || '';
this.password = this.configService.get<string>('METRICS_PASS') || '';
this.passwordHash = bcrypt.hashSync(this.password, 10);
this.salt = randomBytes(16);
this.passwordHash = this.hashPassword(this.password);
}
private hashPassword(password: string): string {
const hash = scryptSync(password, this.salt, 64).toString('hex');
return `${this.salt.toString('hex')}:${hash}`;
}
async use(req: Request, res: Response, next: NextFunction): Promise<void> {
@ -33,7 +42,7 @@ export class BasicAuthMiddleware implements NestMiddleware {
return;
}
const isPasswordValid = await bcrypt.compare(password, this.passwordHash);
const isPasswordValid = await this.verifyPassword(password);
if (!isPasswordValid) {
this.sendUnauthorizedResponse(res);
@ -43,6 +52,14 @@ export class BasicAuthMiddleware implements NestMiddleware {
next();
}
private async verifyPassword(password: string): Promise<boolean> {
const [saltHex, storedHash] = this.passwordHash.split(':');
const salt = Buffer.from(saltHex, 'hex');
const hash = (await this.scryptAsync(password, salt, 64)) as Buffer;
return timingSafeEqual(Buffer.from(storedHash), Buffer.from(hash.toString('hex')));
}
private sendUnauthorizedResponse(res: Response): void {
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted Area", charset="UTF-8"');
res.sendStatus(401);

View file

@ -0,0 +1,13 @@
import winston from 'winston';
const contextsToIgnore = ['InstanceLoader', 'RoutesResolver', 'RouterExplorer'];
export const customLogFilter = winston.format((info) => {
if (info.context) {
const contextValue = String(info.context);
if (contextsToIgnore.some((ctx) => contextValue === ctx)) {
return false;
}
}
return info;
});

View file

@ -0,0 +1 @@
export * from './filter-logs';

View file

@ -0,0 +1,28 @@
import { getBorderCharacters, table } from 'table';
import { readPackageJSON } from 'pkg-types';
export async function getStartMessage() {
const pkg = await readPackageJSON();
return table(
[
['Docs → https://remna.st\nCommunity → https://t.me/remnawave'],
['Rescue CLI → docker exec -it remnawave remnawave'],
],
{
header: {
content: `Remnawave Backend v${pkg.version}`,
alignment: 'center',
},
columnDefault: {
width: 60,
},
columns: {
0: { alignment: 'center' },
1: { alignment: 'center' },
},
drawVerticalLine: () => false,
border: getBorderCharacters('ramac'),
},
);
}

View file

@ -14,6 +14,7 @@ import { NestFactory } from '@nestjs/core';
import { getDocs, isDevelopment, isProduction } from '@common/utils/startup-app';
import { ProxyCheckGuard } from '@common/guards/proxy-check/proxy-check.guard';
import { getStartMessage } from '@common/utils/startup-app/get-start-message';
import { getRealIp } from '@common/middlewares/get-real-ip';
import { AxiosService } from '@common/axios';
@ -121,5 +122,7 @@ async function bootstrap(): Promise<void> {
// await app.close();
// });
logger.info('\n' + (await getStartMessage()) + '\n');
}
void bootstrap();

View file

@ -1,5 +1,5 @@
import { createHmac } from 'node:crypto';
import * as bcrypt from 'bcrypt';
import { createHmac, randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node:util';
import { Injectable, Logger } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
@ -18,11 +18,12 @@ import { AdminEntity } from '@modules/admin/entities/admin.entity';
import { GetStatusResponseModel } from './model/get-status.response.model';
import { ILogin, IRegister } from './interfaces';
const scryptAsync = promisify(scrypt);
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
private readonly jwtSecret: string;
private readonly saltRounds: number;
constructor(
private readonly jwtService: JwtService,
@ -31,7 +32,6 @@ export class AuthService {
private readonly commandBus: CommandBus,
) {
this.jwtSecret = this.configService.getOrThrow<string>('JWT_AUTH_SECRET');
this.saltRounds = 10;
}
public async login(dto: ILogin): Promise<
@ -244,13 +244,23 @@ export class AuthService {
private async hashPassword(plainPassword: string): Promise<string> {
const hmacResult = this.applySecretHmac(plainPassword, this.jwtSecret);
return bcrypt.hash(hmacResult.toString('hex'), this.saltRounds);
const salt = randomBytes(16).toString('hex');
const derivedKey = (await scryptAsync(hmacResult.toString('hex'), salt, 64)) as Buffer;
const hash = derivedKey.toString('hex');
return `${salt}:${hash}`;
}
private async verifyPassword(plainPassword: string, storedHash: string): Promise<boolean> {
const hmacResult = this.applySecretHmac(plainPassword, this.jwtSecret);
return bcrypt.compare(hmacResult.toString('hex'), storedHash);
const [salt, hash] = storedHash.split(':');
const derivedKey = (await scryptAsync(hmacResult.toString('hex'), salt, 64)) as Buffer;
const calculatedHash = derivedKey.toString('hex');
return timingSafeEqual(Buffer.from(calculatedHash), Buffer.from(hash));
}
private async createAdmin(dto: CreateAdminCommand): Promise<ICommandResponse<AdminEntity>> {