Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
854 changes: 854 additions & 0 deletions CODE_ANALYSIS_REPORT.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/api/src/app/activity/activity.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ValidateMongoId } from '@shared/validations/valid-mongo-id.validation';

import { UploadSummary, UploadHistory, RetryUpload, WebhookLogs } from './usecases';
import { ACCESS_KEY_NAME, Defaults } from '@impler/shared';
import { JwtAuthGuard } from '@shared/framework/auth.gaurd';
import { JwtAuthGuard } from '@shared/framework/auth.guard';
import { isDateString } from '@shared/helpers/common.helper';

@Controller('/activity')
Expand Down
43 changes: 26 additions & 17 deletions apps/api/src/app/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ClassSerializerInterceptor,
Controller,
Get,
Logger,
Post,
Put,
Res,
Expand Down Expand Up @@ -47,6 +48,8 @@ import {
@ApiExcludeController()
@UseInterceptors(ClassSerializerInterceptor)
export class AuthController {
private readonly logger = new Logger(AuthController.name);

constructor(
private verify: Verify,
private resendOTP: ResendOTP,
Expand Down Expand Up @@ -78,25 +81,31 @@ export class AuthController {
@Res({ passthrough: true }) response: Response,
@StrategyUser() strategyUser: IStrategyResponse
) {
if (!strategyUser || !strategyUser.token) {
return response.redirect(`${process.env.WEB_BASE_URL}/auth/signin?error=AuthenticationError`);
}

let url = process.env.WEB_BASE_URL + '/auth/signin';
const queryObj: Record<string, any> = {
token: strategyUser.token,
};
if (strategyUser.showAddProject) {
queryObj.showAddProject = true;
}
url += constructQueryString(queryObj);
try {
if (!strategyUser || !strategyUser.token) {
return response.redirect(`${process.env.WEB_BASE_URL}/auth/signin?error=AuthenticationError`);
}

let url = process.env.WEB_BASE_URL + '/auth/signin';
const queryObj: Record<string, any> = {
token: strategyUser.token,
};
if (strategyUser.showAddProject) {
queryObj.showAddProject = true;
}
url += constructQueryString(queryObj);

response.cookie(CONSTANTS.AUTH_COOKIE_NAME, strategyUser.token, {
...COOKIE_CONFIG,
domain: process.env.COOKIE_DOMAIN,
});

response.cookie(CONSTANTS.AUTH_COOKIE_NAME, strategyUser.token, {
...COOKIE_CONFIG,
domain: process.env.COOKIE_DOMAIN,
});
return response.redirect(url);
} catch (error) {
this.logger.error(`GitHub OAuth callback failed: ${error?.message || error}`);

return response.redirect(url);
return response.redirect(`${process.env.WEB_BASE_URL}/auth/signin?error=AuthenticationError`);
}
}

@Get('/me')
Expand Down
5 changes: 4 additions & 1 deletion apps/api/src/app/auth/dtos/login-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDefined, IsString, IsEmail, IsOptional } from 'class-validator';
import { IsDefined, IsString, IsEmail, IsOptional, MinLength, MaxLength } from 'class-validator';

export class LoginUserDto {
@ApiProperty({
description: 'Email of the user',
})
@IsEmail()
@IsDefined()
@MaxLength(255)
email: string;

@ApiProperty({
description: 'Password of the user',
})
@IsString()
@IsDefined()
@MinLength(1)
@MaxLength(128)
password: string;

@ApiProperty({
Expand Down
9 changes: 8 additions & 1 deletion apps/api/src/app/auth/dtos/register-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDefined, IsString, IsEmail, IsOptional } from 'class-validator';
import { IsDefined, IsString, IsEmail, IsOptional, MinLength, MaxLength } from 'class-validator';

export class RegisterUserDto {
@ApiProperty({
description: 'First name of the user',
})
@IsString()
@IsDefined()
@MinLength(1)
@MaxLength(100)
firstName: string;

@ApiProperty({
description: 'Last name of the user',
})
@IsString()
@IsDefined()
@MinLength(1)
@MaxLength(100)
lastName: string;

@ApiProperty({
description: 'Email of the user',
})
@IsEmail()
@IsDefined()
@MaxLength(255)
email: string;

@ApiProperty({
description: 'Password of the user',
})
@IsString()
@IsDefined()
@MinLength(8, { message: 'Password must be at least 8 characters long' })
@MaxLength(128)
password: string;

@ApiProperty({
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/column/column.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Controller, Put, Param, Body, UseGuards, Post, Delete } from '@nestjs/c
import { ValidateMongoId } from '@shared/validations/valid-mongo-id.validation';

import { ACCESS_KEY_NAME } from '@impler/shared';
import { JwtAuthGuard } from '@shared/framework/auth.gaurd';
import { JwtAuthGuard } from '@shared/framework/auth.guard';
import { ColumnRequestDto } from './dtos/column-request.dto';
import { ColumnResponseDto } from './dtos/column-response.dto';
import { AddColumn, UpdateColumn, DeleteColumn } from './usecases';
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/common/common.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '@nestjs/common';

import { ACCESS_KEY_NAME, IImportConfig } from '@impler/shared';
import { JwtAuthGuard } from '@shared/framework/auth.gaurd';
import { JwtAuthGuard } from '@shared/framework/auth.guard';
import { ValidRequestDto, SignedUrlDto } from './dtos';
import { ValidImportFile } from '@shared/validations/valid-import-file.validation';
import { ValidRequestCommand, GetSignedUrl, ValidRequest, GetImportConfig, GetSheetNames } from './usecases';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { expect } from 'chai';
import * as crypto from 'crypto';

describe('GenerateUniqueApiKey - Cryptographic Security', () => {
describe('API key generation with crypto.randomBytes', () => {
it('should generate 256-bit (64 hex character) keys', () => {
const key = crypto.randomBytes(32).toString('hex');
expect(key).to.have.length(64);
});

it('should generate unique keys on each call', () => {
const keys = new Set<string>();
for (let i = 0; i < 1000; i++) {
keys.add(crypto.randomBytes(32).toString('hex'));
}
// All 1000 keys should be unique
expect(keys.size).to.equal(1000);
});

it('should generate hex-only characters', () => {
const key = crypto.randomBytes(32).toString('hex');
expect(key).to.match(/^[a-f0-9]+$/);
});

it('should have sufficient entropy (256 bits)', () => {
const key = crypto.randomBytes(32);
// 32 bytes = 256 bits of entropy
expect(key.length).to.equal(32);
});

it('should be significantly stronger than hat() (128 bits)', () => {
// crypto.randomBytes(32) = 256 bits vs hat() = ~128 bits
// 2^128 more combinations, practically impossible to brute force
const keyBytes = 32;
const keyBits = keyBytes * 8;
expect(keyBits).to.equal(256);
expect(keyBits).to.be.greaterThan(128);
});
});

describe('API key collision resistance', () => {
it('should not generate duplicate keys in 10000 iterations', () => {
const keys = new Set<string>();
const iterations = 10000;

for (let i = 0; i < iterations; i++) {
keys.add(crypto.randomBytes(32).toString('hex'));
}

expect(keys.size).to.equal(iterations);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as crypto from 'crypto';
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { EnvironmentRepository } from '@impler/dal';
import * as hat from 'hat';
import { VARIABLES } from '@shared/constants';

const API_KEY_GENERATION_MAX_RETRIES = 3;
const API_KEY_GENERATION_MAX_RETRIES = 10;

@Injectable()
export class GenerateUniqueApiKey {
Expand All @@ -29,12 +29,10 @@ export class GenerateUniqueApiKey {
}

/**
* Extracting the generation functionality so it can be stubbed for functional testing
*
* @requires hat
* @todo Dependency is no longer accessible to source code due of removal from GitHub. Consider look for an alternative.
* Generates a cryptographically secure API key with 256-bit entropy.
* Replaces the deprecated `hat` library with Node.js native crypto.
*/
private generateApiKey(): string {
return hat();
return crypto.randomBytes(32).toString('hex');
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

import { QueuesEnum } from '@impler/shared';
Expand All @@ -7,6 +7,7 @@ import { FailedWebhookRetryRequestsEntity, FailedWebhookRetryRequestsRepository

@Injectable()
export class FailedWebhookRetry {
private readonly logger = new Logger(FailedWebhookRetry.name);
constructor(
private failedWebhookRetryRequestsRepository: FailedWebhookRetryRequestsRepository = new FailedWebhookRetryRequestsRepository(),
private queueService: QueueService
Expand All @@ -18,20 +19,20 @@ export class FailedWebhookRetry {
const memUsageStart = process.memoryUsage();
const cpuUsageStart = process.cpuUsage();

console.log('========================================');
console.log(`[FAILED-WEBHOOK-RETRY] Cron Started at ${startTime.toISOString()}`);
console.log(`[FAILED-WEBHOOK-RETRY] Memory Usage (Start): RSS=${(memUsageStart.rss / 1024 / 1024).toFixed(2)}MB, Heap=${(memUsageStart.heapUsed / 1024 / 1024).toFixed(2)}MB`);
console.log('========================================');
this.logger.log('========================================');
this.logger.log(`[FAILED-WEBHOOK-RETRY] Cron Started at ${startTime.toISOString()}`);
this.logger.log(`[FAILED-WEBHOOK-RETRY] Memory Usage (Start): RSS=${(memUsageStart.rss / 1024 / 1024).toFixed(2)}MB, Heap=${(memUsageStart.heapUsed / 1024 / 1024).toFixed(2)}MB`);
this.logger.log('========================================');

try {
const failedWebhooks: FailedWebhookRetryRequestsEntity[] = await this.failedWebhookRetryRequestsRepository.find({
nextRequestTime: { $lt: new Date() },
});

console.log(`[FAILED-WEBHOOK-RETRY] Found ${failedWebhooks.length} failed webhooks to retry`);
this.logger.log(`[FAILED-WEBHOOK-RETRY] Found ${failedWebhooks.length} failed webhooks to retry`);

if (!failedWebhooks.length) {
console.log(`[FAILED-WEBHOOK-RETRY] No webhooks to process, exiting`);
this.logger.log(`[FAILED-WEBHOOK-RETRY] No webhooks to process, exiting`);
return;
}

Expand All @@ -47,49 +48,49 @@ export class FailedWebhookRetry {
const memUsageEnd = process.memoryUsage();
const cpuUsageEnd = process.cpuUsage(cpuUsageStart);

console.log('========================================');
console.log(`[FAILED-WEBHOOK-RETRY] Cron Completed at ${endTime.toISOString()}`);
console.log(`[FAILED-WEBHOOK-RETRY] Results - Successful: ${successful}, Failed: ${failed}, Total: ${failedWebhooks.length}`);
console.log(`[FAILED-WEBHOOK-RETRY] Processing Duration: ${processDuration}ms`);
console.log(`[FAILED-WEBHOOK-RETRY] Total Duration: ${duration}ms`);
console.log(`[FAILED-WEBHOOK-RETRY] Memory Usage (End): RSS=${(memUsageEnd.rss / 1024 / 1024).toFixed(2)}MB, Heap=${(memUsageEnd.heapUsed / 1024 / 1024).toFixed(2)}MB`);
console.log(`[FAILED-WEBHOOK-RETRY] Memory Delta: RSS=${((memUsageEnd.rss - memUsageStart.rss) / 1024 / 1024).toFixed(2)}MB, Heap=${((memUsageEnd.heapUsed - memUsageStart.heapUsed) / 1024 / 1024).toFixed(2)}MB`);
console.log(`[FAILED-WEBHOOK-RETRY] CPU Usage: User=${(cpuUsageEnd.user / 1000).toFixed(2)}ms, System=${(cpuUsageEnd.system / 1000).toFixed(2)}ms`);
this.logger.log('========================================');
this.logger.log(`[FAILED-WEBHOOK-RETRY] Cron Completed at ${endTime.toISOString()}`);
this.logger.log(`[FAILED-WEBHOOK-RETRY] Results - Successful: ${successful}, Failed: ${failed}, Total: ${failedWebhooks.length}`);
this.logger.log(`[FAILED-WEBHOOK-RETRY] Processing Duration: ${processDuration}ms`);
this.logger.log(`[FAILED-WEBHOOK-RETRY] Total Duration: ${duration}ms`);
this.logger.log(`[FAILED-WEBHOOK-RETRY] Memory Usage (End): RSS=${(memUsageEnd.rss / 1024 / 1024).toFixed(2)}MB, Heap=${(memUsageEnd.heapUsed / 1024 / 1024).toFixed(2)}MB`);
this.logger.log(`[FAILED-WEBHOOK-RETRY] Memory Delta: RSS=${((memUsageEnd.rss - memUsageStart.rss) / 1024 / 1024).toFixed(2)}MB, Heap=${((memUsageEnd.heapUsed - memUsageStart.heapUsed) / 1024 / 1024).toFixed(2)}MB`);
this.logger.log(`[FAILED-WEBHOOK-RETRY] CPU Usage: User=${(cpuUsageEnd.user / 1000).toFixed(2)}ms, System=${(cpuUsageEnd.system / 1000).toFixed(2)}ms`);

if (duration > 5000) {
console.warn(`[FAILED-WEBHOOK-RETRY] ⚠️ WARNING: Cron execution took ${duration}ms (>5s threshold)`);
this.logger.warn(`[FAILED-WEBHOOK-RETRY] ⚠️ WARNING: Cron execution took ${duration}ms (>5s threshold)`);
}

if (failedWebhooks.length > 100) {
console.warn(`[FAILED-WEBHOOK-RETRY] ⚠️ WARNING: Processing large batch of ${failedWebhooks.length} webhooks`);
this.logger.warn(`[FAILED-WEBHOOK-RETRY] ⚠️ WARNING: Processing large batch of ${failedWebhooks.length} webhooks`);
}

console.log('========================================');
this.logger.log('========================================');
} catch (error) {
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();

console.error('========================================');
console.error(`[FAILED-WEBHOOK-RETRY] ❌ ERROR at ${endTime.toISOString()}`);
console.error(`[FAILED-WEBHOOK-RETRY] Duration before error: ${duration}ms`);
console.error('[FAILED-WEBHOOK-RETRY] Error details:', error);
console.error('========================================');
this.logger.error('========================================');
this.logger.error(`[FAILED-WEBHOOK-RETRY] ❌ ERROR at ${endTime.toISOString()}`);
this.logger.error(`[FAILED-WEBHOOK-RETRY] Duration before error: ${duration}ms`);
this.logger.error('[FAILED-WEBHOOK-RETRY] Error details:', error);
this.logger.error('========================================');
throw error;
}
}

private async processWebhook(webhook: FailedWebhookRetryRequestsEntity) {
const webhookStartTime = Date.now();
try {
console.log(`[FAILED-WEBHOOK-RETRY] Processing webhook - ID: ${webhook._id}, Time: ${new Date().toISOString()}`);
this.logger.log(`[FAILED-WEBHOOK-RETRY] Processing webhook - ID: ${webhook._id}, Time: ${new Date().toISOString()}`);

this.queueService.publishToQueue(QueuesEnum.SEND_FAILED_WEBHOOK_DATA, webhook._id as string);

const webhookDuration = Date.now() - webhookStartTime;
console.log(`[FAILED-WEBHOOK-RETRY] Webhook queued - ID: ${webhook._id}, Duration: ${webhookDuration}ms`);
this.logger.log(`[FAILED-WEBHOOK-RETRY] Webhook queued - ID: ${webhook._id}, Duration: ${webhookDuration}ms`);
} catch (error) {
const webhookDuration = Date.now() - webhookStartTime;
console.error(`[FAILED-WEBHOOK-RETRY] ❌ Error processing webhook - ID: ${webhook._id}, Duration: ${webhookDuration}ms, Time: ${new Date().toISOString()}`, error);
this.logger.error(`[FAILED-WEBHOOK-RETRY] ❌ Error processing webhook - ID: ${webhook._id}, Duration: ${webhookDuration}ms, Time: ${new Date().toISOString()}`, error);
throw error;
}
}
Expand Down
10 changes: 9 additions & 1 deletion apps/api/src/app/import-jobs/dtos/create-userjob.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { IsNotEmpty, IsOptional, IsString, IsUrl, MaxLength, Matches } from 'class-validator';

export class CreateUserJobDto {
@IsString()
Expand All @@ -7,6 +7,8 @@ export class CreateUserJobDto {

@IsString()
@IsNotEmpty()
@IsUrl({ require_tld: false }, { message: 'url must be a valid URL' })
@MaxLength(2048)
url: string;

@IsString()
Expand All @@ -15,13 +17,19 @@ export class CreateUserJobDto {

@IsString()
@IsOptional()
@MaxLength(100000)
extra?: string;

@IsString()
@IsOptional()
@MaxLength(2048)
authHeaderValue?: string;

@IsString()
@IsOptional()
@Matches(
/^(\*|[0-9]|[1-5][0-9])(\/[0-9]+)?\s+(\*|[0-9]|1[0-9]|2[0-3])(\/[0-9]+)?\s+(\*|[1-9]|[12][0-9]|3[01])(\/[0-9]+)?\s+(\*|[1-9]|1[0-2])(\/[0-9]+)?\s+(\*|[0-6])(\/[0-9]+)?$/,
{ message: 'cron must be a valid cron expression' }
)
cron?: string;
}
Loading
Loading