mirror of
https://github.com/remnawave/backend.git
synced 2026-05-13 04:09:01 +00:00
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:
parent
0fc6170742
commit
e39f76844f
12 changed files with 492 additions and 672 deletions
2
.npmrc
2
.npmrc
|
|
@ -1 +1 @@
|
|||
legacy-peer-deps=true
|
||||
|
||||
|
|
|
|||
1035
package-lock.json
generated
1035
package-lock.json
generated
File diff suppressed because it is too large
Load diff
25
package.json
25
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
13
src/common/utils/filter-logs/filter-logs.ts
Normal file
13
src/common/utils/filter-logs/filter-logs.ts
Normal 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;
|
||||
});
|
||||
1
src/common/utils/filter-logs/index.ts
Normal file
1
src/common/utils/filter-logs/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './filter-logs';
|
||||
28
src/common/utils/startup-app/get-start-message.ts
Normal file
28
src/common/utils/startup-app/get-start-message.ts
Normal 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'),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue