All files / src/platform/qr-barcode code.service.ts

0% Statements 0/36
0% Branches 0/34
0% Functions 0/4
0% Lines 0/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119                                                                                                                                                                                                                                             
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import {
  CodeDefinition,
  CodeGeneration,
  CodeScan,
} from './schemas/code.schema';
import * as crypto from 'crypto';
 
@Injectable()
export class CodeService {
  private readonly logger = new Logger(CodeService.name);
 
  constructor(
    @InjectModel(CodeDefinition.name)
    private readonly definitionModel: Model<CodeDefinition>,
    @InjectModel(CodeGeneration.name)
    private readonly generationModel: Model<CodeGeneration>,
    @InjectModel(CodeScan.name) private readonly scanModel: Model<CodeScan>,
  ) {}
 
  private getSigningSecret(): string {
    return (
      process.env.CODE_SIGNING_SECRET ||
      'be-vision-qr-default-secret-change-in-production'
    );
  }
 
  async generateCode(
    tenantId: string,
    definitionId: string,
    payload: string,
    expiresInSeconds?: number,
  ): Promise<{ payload: string; signature: string; qrDataUrl: string }> {
    const def = await this.definitionModel
      .findOne({ tenantId, _id: definitionId, isActive: true })
      .exec();
    if (!def) {
      throw new BadRequestException('Invalid or inactive code definition');
    }
 
    const expiresAt = expiresInSeconds
      ? new Date(Date.now() + expiresInSeconds * 1000)
      : null;
 
    // Cryptographic signature for tamper verification
    const secret = this.getSigningSecret();
    const signature = crypto
      .createHmac('sha256', secret)
      .update(`${tenantId}:${payload}:${expiresAt ? expiresAt.getTime() : ''}`)
      .digest('hex');
 
    await this.generationModel.create({
      tenantId,
      definitionId,
      payload,
      signature,
      expiresAt,
    });
 
    // In production we would use standard qr-image or qrcode packages to build data urls.
    // For local VM/VPS, we export target signed payloads cleanly:
    const qrDataUrl = `https://api.bevision.io/v1/scan?tenantId=${tenantId}&payload=${encodeURIComponent(payload)}&sig=${signature}`;
 
    return {
      payload,
      signature,
      qrDataUrl,
    };
  }
 
  async validateScan(
    tenantId: string,
    payload: string,
    signature: string,
    scannedBy?: string,
  ): Promise<{ isValid: boolean; status: string }> {
    const gen = await this.generationModel
      .findOne({ tenantId, payload })
      .exec();
    let status = 'valid';
 
    if (!gen) {
      status = 'tampered';
    } else if (gen.isRevoked) {
      status = 'revoked';
    } else if (gen.expiresAt && gen.expiresAt < new Date()) {
      status = 'expired';
    } else {
      // HMAC signature validation verification
      const secret = this.getSigningSecret();
      const calculatedSig = crypto
        .createHmac('sha256', secret)
        .update(
          `${tenantId}:${payload}:${gen.expiresAt ? gen.expiresAt.getTime() : ''}`,
        )
        .digest('hex');
 
      if (calculatedSig !== signature) {
        status = 'tampered';
      }
    }
 
    await this.scanModel.create({
      tenantId,
      payload,
      scannedBy: scannedBy || 'system',
      scannedAt: new Date(),
      status,
    });
 
    return {
      isValid: status === 'valid',
      status,
    };
  }
}