Skip to content

Common Components

The src/common directory contains reusable components, utilities, and services that are used across the application. These components provide core functionality such as authentication, authorization, encryption, and more.

Module Structure

flowchart TD
    CommonModule[CommonModule]
    EncryptionModule[EncryptionModule]
    GuardsModule[GuardsModule]

    EmailService[EmailService]
    AuditService[AuditService]
    EncryptionService[EncryptionService]

    CommonModule --> EmailService
    CommonModule --> AuditService

    EncryptionModule --> EncryptionService
    EncryptionModule --> EncryptionInterceptor[EncryptionInterceptor]

    GuardsModule --> EmailVerifiedGuard[EmailVerifiedGuard]
    GuardsModule --> ThrottlerGuard[ThrottlerGuard]

    JwtAuthGuard[JwtAuthGuard]
    AdminOnlyGuard[AdminOnlyGuard]
    RolesGuard[RolesGuard]
    ApiKeyGuard[ApiKeyGuard]

Core Modules

CommonModule

The CommonModule is a global module that provides shared services across the application:

@Global()
@Module({
  imports: [
    ConfigModule,
    CacheModule.register({
      ttl: 60000, // 1 minute default TTL
    }),
  ],
  providers: [EmailService, AuditService, PrismaService],
  exports: [EmailService, AuditService, CacheModule],
})
export class CommonModule {}

EncryptionModule

The EncryptionModule handles data encryption and decryption throughout the application:

@Global()
@Module({
  providers: [EncryptionService, EncryptionInterceptor],
  exports: [EncryptionService, EncryptionInterceptor],
})
export class EncryptionModule {}

GuardsModule

The GuardsModule provides authentication and authorization guards:

@Module({
  imports: [PrismaModule],
  providers: [EmailVerifiedGuard, ThrottlerGuard],
  exports: [EmailVerifiedGuard, ThrottlerGuard],
})
export class GuardsModule {}

Guards

Guards are used to control access to routes and endpoints based on certain conditions.

JwtAuthGuard

The JwtAuthGuard verifies JWT authentication tokens:

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    // Add custom logic here if needed
    return super.canActivate(context);
  }

  handleRequest(err, user, info) {
    if (err || !user) {
      throw err || new UnauthorizedException('Authentication required');
    }
    return user;
  }
}

AdminOnlyGuard

The AdminOnlyGuard restricts access to admin users only:

@Injectable()
export class AdminOnlyGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const user = request.user;

    // Ensure user exists and has a role
    if (!user || !user.role) {
      throw new ForbiddenException('Access denied');
    }

    // Check if user has admin permissions
    const isAdmin = user.role === Role.ADMIN || user.role === Role.SUPER_ADMIN;

    if (!isAdmin) {
      throw new ForbiddenException(
        'This action requires administrator privileges',
      );
    }

    return true;
  }
}

RolesGuard

The RolesGuard implements role-based access control:

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles) {
      return true;
    }

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user?.role === role);
  }
}

ThrottlerGuard

The ThrottlerGuard implements rate limiting to prevent abuse:

@Injectable()
export class ThrottlerGuard extends NestThrottlerGuard {
  // Override the getTracker method to customize how we track rate limits
  protected getTracker(context: ExecutionContext): Promise<string> {
    // Get the request object
    const request = context.switchToHttp().getRequest();

    // Use X-Forwarded-For header if available (for when behind a proxy/load balancer)
    // Otherwise fall back to the client's IP address
    const ip = request.headers['x-forwarded-for'] || request.ip;

    // Return a unique identifier for this client
    return Promise.resolve(ip);
  }
}

EmailVerifiedGuard

The EmailVerifiedGuard ensures that users have verified their email address:

@Injectable()
export class EmailVerifiedGuard implements CanActivate {
  constructor(private readonly prisma: PrismaService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const user = request.user;

    if (!user) {
      throw new UnauthorizedException('User not authenticated');
    }

    const userRecord = await this.prisma.user.findUnique({
      where: { id: user.userId || user.id },
    });

    if (!userRecord) {
      throw new UnauthorizedException('User not found');
    }

    if (!userRecord.emailVerified) {
      throw new UnauthorizedException(
        'Email verification required. Please verify your email address before performing this action.',
      );
    }

    return true;
  }
}

ApiKeyGuard

The ApiKeyGuard implements API key authentication:

@Injectable()
export class ApiKeyGuard implements CanActivate {
  constructor(private configService: ConfigService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const apiKey = request.headers['x-api-key'];
    const validApiKey = this.configService.get<string>('NEWSLETTER_API_KEY');

    if (!apiKey || apiKey !== validApiKey) {
      throw new UnauthorizedException('Invalid API key');
    }

    return true;
  }
}

Decorators

Decorators provide a way to add metadata to classes and class members.

GetUser

The GetUser decorator extracts the user from the request object:

export const GetUser = createParamDecorator(
  (data: string | undefined, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;

    // If data is provided, return that specific property from user
    return data ? user[data] : user;
  },
);

Roles

The Roles decorator specifies required roles for access control:

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

Throttle

The Throttle decorators provide rate limiting options:

export const Throttle = (limit: number, ttl: number, name?: string) => {
  const options = name
    ? { [name]: { limit, ttl } }
    : { default: { limit, ttl } };

  return NestThrottle(options);
};

export const SensitiveThrottle = () =>
  NestThrottle({
    sensitive: { limit: 5, ttl: 900000 },
  });

Interceptors

Interceptors provide a way to intercept and modify requests and responses.

EncryptionInterceptor

The EncryptionInterceptor handles encryption and decryption of sensitive data:

@Injectable()
export class EncryptionInterceptor implements NestInterceptor {
  constructor(private readonly encryptionService: EncryptionService) {}

  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const isRegisterEndpoint = request.path === '/api/v1/auth/register';

    // For registration endpoint, let validation happen first
    if (!isRegisterEndpoint && request.body) {
      // Decrypt incoming sensitive data for non-registration endpoints
      await this.decryptSensitiveData(request.body);
    }

    return next.handle().pipe(
      map(async (data) => {
        if (!data) return data;

        // For all responses, encrypt sensitive data
        const processedData = await this.encryptSensitiveData(data);
        return processedData;
      }),
    );
  }
}

Services

Services implement the business logic of the application.

EmailService

The EmailService handles sending emails:

@Injectable()
export class EmailService {
  private transporter: nodemailer.Transporter;
  private readonly logger = new Logger(EmailService.name);
  private testAccount: any;

  constructor(private configService: ConfigService) {
    this.initializeTransporter();
  }

  async sendPasswordResetEmail(email: string, token: string): Promise<void> {
    const resetLink = `${this.configService.get<string>('FRONTEND_URL') || 'http://localhost:3000'}/reset-password?token=${token}`;

    const info = await this.transporter.sendMail({
      from:
        this.configService.get<string>('SMTP_FROM') || 'noreply@curiopay.com',
      to: email,
      subject: 'Password Reset Request',
      html: `
        <h1>Password Reset Request</h1>
        <p>You have requested to reset your password. Click the link below to proceed:</p>
        <a href="${resetLink}">Reset Password</a>
        <p>If you did not request this password reset, please ignore this email.</p>
        <p>This link will expire in 1 hour.</p>
      `,
    });
  }
}

EncryptionService

The EncryptionService provides methods for encrypting and decrypting data:

@Injectable()
export class EncryptionService {
  private readonly algorithm = 'aes-256-gcm';
  private readonly keyLength = 32;
  private readonly ivLength = 16;
  private readonly saltLength = 32;
  private readonly authTagLength = 16;

  constructor() {
    // Ensure encryption key is set
    if (!process.env.ENCRYPTION_KEY) {
      throw new Error('ENCRYPTION_KEY environment variable must be set');
    }
  }

  async encrypt(data: string): Promise<string> {
    try {
      // Generate a random salt
      const salt = randomBytes(this.saltLength);

      // Generate key using scrypt
      const key = (await promisify(scrypt)(
        process.env.ENCRYPTION_KEY as BinaryLike,
        salt,
        this.keyLength,
      )) as CipherKey;

      // Generate IV
      const iv = randomBytes(this.ivLength);

      // Create cipher
      const cipher = createCipheriv(this.algorithm, key, iv);

      // Encrypt the data
      const encryptedData = Buffer.concat([
        cipher.update(data, 'utf8'),
        cipher.final(),
      ]);

      // Get auth tag
      const authTag = cipher.getAuthTag();

      // Combine all components
      const combined = Buffer.concat([salt, iv, authTag, encryptedData]);

      // Return as base64 string
      return combined.toString('base64');
    } catch (error) {
      throw new Error(`Encryption failed: ${error.message}`);
    }
  }
}

Base Classes

BaseController

The BaseController provides common functionality for controllers:

@ApiBearerAuth('JWT-auth')
@ApiUnauthorizedResponse({
  description: 'Unauthorized - Valid JWT token is required',
  schema: {
    type: 'object',
    properties: {
      message: {
        type: 'string',
        example: 'Unauthorized',
      },
      statusCode: {
        type: 'number',
        example: 401,
      },
      error: {
        type: 'string',
        example: 'Unauthorized',
      },
    },
  },
})
export class BaseController {}