diff --git a/src/common/services/cache.redis.ts b/src/common/services/cache.redis.ts index f0982e9..2d73ae2 100644 --- a/src/common/services/cache.redis.ts +++ b/src/common/services/cache.redis.ts @@ -41,10 +41,11 @@ export class RedisCacheService { async get(key: string, prefix?: string): Promise { try { const fullKey = this.buildKey(key, prefix); + this.logger.debug(`Cache fullkey: ${fullKey}`); const data = await this.redis.get(fullKey); if (!data) { - this.logger.debug(`Cache miss: ${fullKey}`); + this.logger.debug(`Error Cache miss: ${fullKey}`); return null; } diff --git a/src/modules/challenge/adaptor/dtos/orange.challenge.dto.ts b/src/modules/challenge/adaptor/dtos/orange.challenge.dto.ts index 5136a02..a00d5c7 100644 --- a/src/modules/challenge/adaptor/dtos/orange.challenge.dto.ts +++ b/src/modules/challenge/adaptor/dtos/orange.challenge.dto.ts @@ -34,6 +34,28 @@ export interface OrangeChallengeResponse { }; } +export interface OrangeVerifyResponse { + challenge: { + method: string, + country: string, + service: string, + partnerId:string, + inputs: [ ] + result:any[ ],/*[ + { + type: 'ise2', + value: 'PDKSUB-200-KzIyMTc3MTcxNzE3MS1TRU4tMTc2MTc4MzI2NjAy' + } + ]*/ + } + + error?: { + code: number | string; + message: string; + description?: string; + }; +} + /** * Builder pour construire des requêtes Orange Challenge */ @@ -147,6 +169,16 @@ export class OrangeChallengeRequestBuilder { return this; } + withInfo(infoValue: string): this { + this.request.challenge.inputs?.push( + { + "type": "info", + "value": infoValue + } + ) + return this; + } + /** * Construire la requête finale */ diff --git a/src/modules/challenge/adaptor/orange.adaptor.ts b/src/modules/challenge/adaptor/orange.adaptor.ts index da11204..f310566 100644 --- a/src/modules/challenge/adaptor/orange.adaptor.ts +++ b/src/modules/challenge/adaptor/orange.adaptor.ts @@ -2,7 +2,8 @@ import axios, { AxiosInstance, AxiosError } from 'axios'; import { OrangeChallengeRequest, OrangeChallengeResponse, - OrangeChallengeRequestBuilder + OrangeChallengeRequestBuilder, + OrangeVerifyResponse } from './dtos/orange.challenge.dto' import { OrangeConfig, @@ -13,7 +14,7 @@ import { //import { OtpChallengeResponseDto, OtpChallengeStatusEnum } from '../../dtos/otp-challenge-response.dto'; import { OtpChallengeRequestDto } from '../dto/challenge.request.dto'; -import { OtpChallengeResponseDto, OtpChallengeStatusEnum } from '../dto/challenge.response.dto'; +import { OtpChallengeResponseDto, OtpChallengeStatusEnum, OtpVerifResponseDto } from '../dto/challenge.response.dto'; import { Logger } from '@nestjs/common'; import { log } from 'console'; @@ -90,7 +91,49 @@ export class OrangeAdapter { /** * Convertir la requête générique en format Orange */ - private mapToOrangeRequest(request: OtpChallengeRequestDto): OrangeChallengeRequest { + private mapToOrangeRequestChallenge(request: OtpChallengeRequestDto): OrangeChallengeRequest { + const builder = new OrangeChallengeRequestBuilder(); + + // Mapper le pays + const orangeCountry = COUNTRY_CODE_MAPPING[request.country] || request.country; + builder.withCountry(orangeCountry); + + // Mapper la méthode + const orangeMethod = OTP_METHOD_MAPPING[request.method] || 'OTP-SMS-AUTH'; + builder.withMethod(orangeMethod); + + // Ajouter les informations de base + builder + .withService(request.service) + .withPartnerId(this.config.partnerId); + + // Ajouter l'identifiant + builder.withIdentifier(request.identifier.type, request.identifier.value); + + // Ajouter le code de confirmation si présent + /* todo voir si mandatory + if (request.confirmationCode) { + builder.withConfirmationCode(request.confirmationCode); + } else { + builder.withConfirmationCode(''); // Orange requiert ce champ même vide + }*/ + + // Configuration du message OTP + const message = request.config?.message || this.config.defaultOtpMessage; + builder.withMessage(message); + + // Longueur de l'OTP + const otpLength = request.config?.length || this.config.defaultOtpLength; + builder.withOtpLength(otpLength); + + // Nom de l'expéditeur + const senderName = request.config?.senderName || this.config.defaultSenderName; + builder.withSenderName(senderName); + + return builder.build(); + } + + private mapToOrangeRequestVerify(request: OtpChallengeRequestDto): OrangeChallengeRequest { const builder = new OrangeChallengeRequestBuilder(); // Mapper le pays @@ -116,17 +159,11 @@ export class OrangeAdapter { builder.withConfirmationCode(''); // Orange requiert ce champ même vide } - // Configuration du message OTP - const message = request.config?.message || this.config.defaultOtpMessage; - builder.withMessage(message); - - // Longueur de l'OTP - const otpLength = request.config?.length || this.config.defaultOtpLength; - builder.withOtpLength(otpLength); + // Nom de l'expéditeur const senderName = request.config?.senderName || this.config.defaultSenderName; - builder.withSenderName(senderName); + builder.withInfo("ise2"); return builder.build(); } @@ -134,7 +171,7 @@ export class OrangeAdapter { /** * Convertir la réponse Orange en format générique */ - private mapFromOrangeResponse( + private mapFromOrangeChallengeResponse( orangeResponse: OrangeChallengeResponse, request: OtpChallengeRequestDto ): OtpChallengeResponseDto { @@ -167,6 +204,36 @@ export class OrangeAdapter { return response; } + private mapFromOrangeVerifyResponse( + orangeResponse: OrangeVerifyResponse, + request: OtpChallengeRequestDto + ): OtpVerifResponseDto { + console.log('mapFromOrangeVerifyResponse',orangeResponse.challenge.result) + + const response: OtpVerifResponseDto = { + merchantId: request.merchantId, + status: this.mapOrangeResponseStatus(orangeResponse), + userAlias: orangeResponse.challenge.result?.[0]['value'] || 'not presenter', + metadata: { + provider: 'orange', + country: request.country, + method: request.method + } + }; + + // Ajouter l'erreur si présente + if (orangeResponse.error) { + response.error = { + code: orangeResponse.error.code.toString(), + message: orangeResponse.error.message, + description: orangeResponse.error.description + }; + response.status = OtpChallengeStatusEnum.FAILED; + } + + return response; + } + /** * Mapper le statut Orange vers le statut générique */ @@ -182,6 +249,16 @@ export class OrangeAdapter { return OtpChallengeStatusEnum.PENDING; } + private mapOrangeResponseStatus(orangeResponse: OrangeVerifyResponse): OtpChallengeStatusEnum { + if (orangeResponse.error) { + return OtpChallengeStatusEnum.FAILED; + }else{ + return OtpChallengeStatusEnum.VERIFIED; + } + + + } + /** * Gérer les erreurs HTTP */ @@ -208,7 +285,7 @@ export class OrangeAdapter { //this.logger.debug(`initiateChallenge --> acces token ${token}`); // Mapper la requête - const orangeRequest = this.mapToOrangeRequest(request); + const orangeRequest = this.mapToOrangeRequestChallenge(request); this.logger.debug( `[request to orange ]: ${JSON.stringify(orangeRequest, null, 2)}`, @@ -226,7 +303,7 @@ export class OrangeAdapter { ); // Mapper la réponse - return this.mapFromOrangeResponse(response.data, request); + return this.mapFromOrangeChallengeResponse(response.data, request); } catch (error) { // En cas d'erreur, retourner une réponse avec le statut FAILED return { @@ -249,7 +326,7 @@ export class OrangeAdapter { challengeId: string, otpCode: string, originalRequest: OtpChallengeRequestDto - ): Promise { + ): Promise { try { // Créer une nouvelle requête avec le code de confirmation const verifyRequest: OtpChallengeRequestDto = { @@ -261,11 +338,15 @@ export class OrangeAdapter { const token = await this.getAccessToken(); // Mapper la requête - const orangeRequest = this.mapToOrangeRequest(verifyRequest); + const orangeRequest = this.mapToOrangeRequestVerify(verifyRequest); + this.logger.debug( + `[request to orange (verify) ]: ${JSON.stringify(orangeRequest, null, 2)}`, + ) - // Appeler l'API Orange pour vérification - const response = await this.axiosInstance.post( - this.config.challengeEndpoint, + // Appeler l'API Orange pour vérification todo use request otp challenge + // + const response = await this.axiosInstance.post( + `${this.config.challengeEndpoint}/${challengeId}`, orangeRequest, { headers: { @@ -273,9 +354,17 @@ export class OrangeAdapter { } } ); + //${JSON.stringify(response, null, 2)} + this.logger.debug( + `[response from orange (verify) ${JSON.stringify(response.data, null, 2)} ]: `, + ) // Mapper la réponse - const mappedResponse = this.mapFromOrangeResponse(response.data, verifyRequest); + const mappedResponse = this.mapFromOrangeVerifyResponse(response.data, verifyRequest); + + this.logger.debug( + `[response parsed from orange (verify) ${JSON.stringify(mappedResponse, null, 2)} ]: `, + ) // Si pas d'erreur, c'est vérifié if (!mappedResponse.error) { @@ -284,8 +373,8 @@ export class OrangeAdapter { return mappedResponse; } catch (error) { - return { - challengeId, + return { + userAlias:'undefined', merchantId: originalRequest.merchantId, status: OtpChallengeStatusEnum.FAILED, error: { diff --git a/src/modules/challenge/dto/challenge.response.dto.ts b/src/modules/challenge/dto/challenge.response.dto.ts index ce9c5c9..012409a 100644 --- a/src/modules/challenge/dto/challenge.response.dto.ts +++ b/src/modules/challenge/dto/challenge.response.dto.ts @@ -40,6 +40,36 @@ export class OtpChallengeResponseDto { @IsBoolean() requiresConfirmation?: boolean; + @IsOptional() + metadata?: Record; + + @IsOptional() + error?: { + code: string; + message: string; + description?: string; + }; +} + +export class OtpVerifResponseDto { + @IsString() + @IsNotEmpty() + merchantId: string; + + @IsString() + @IsNotEmpty() + userAlias: string; + + + @IsEnum(OtpChallengeStatusEnum) + @IsNotEmpty() + status: OtpChallengeStatusEnum; + + @IsOptional() + @IsString() + message?: string; + + @IsOptional() metadata?: Record; diff --git a/src/modules/challenge/otp.challenge.controller.ts b/src/modules/challenge/otp.challenge.controller.ts index 56804a2..5969713 100644 --- a/src/modules/challenge/otp.challenge.controller.ts +++ b/src/modules/challenge/otp.challenge.controller.ts @@ -23,6 +23,7 @@ import { import { OtpChallengeResponseDto, OtpChallengeStatusEnum, + OtpVerifResponseDto, } from './dto/challenge.response.dto'; import { OtpChallengeRequestDto } from './dto/challenge.request.dto'; import { OtpChallengeService } from './otp.challenge.service'; @@ -177,7 +178,7 @@ export class OtpChallengeController { @Body('otpCode') otpCode: string, @Headers('X-Merchant-ID') merchantId: string, @Headers('x-API-KEY') apiKey: string, - ): Promise { + ): Promise { this.logger.log( `[VERIFY] Merchant: ${merchantId}, Challenge: ${challengeId}`, ); @@ -193,6 +194,7 @@ export class OtpChallengeController { otpCode, merchantId, ); + this.logger.log(`[VERIFY] Result - object: ${response}`); // Logger le résultat this.logger.log(`[VERIFY] Result - Status: ${response.status}`); diff --git a/src/modules/challenge/otp.challenge.service.ts b/src/modules/challenge/otp.challenge.service.ts index b9aaa59..554a9be 100644 --- a/src/modules/challenge/otp.challenge.service.ts +++ b/src/modules/challenge/otp.challenge.service.ts @@ -3,7 +3,7 @@ import type { OrangeConfig } from './adaptor/orange.config'; import { OrangeAdapter } from './adaptor/orange.adaptor'; import { OtpChallengeRequestDto } from './dto/challenge.request.dto'; import { IOtpChallengeService } from './otp.challenge.interface'; -import { OtpChallengeResponseDto, OtpChallengeStatusEnum } from './dto/challenge.response.dto'; +import { OtpChallengeResponseDto, OtpChallengeStatusEnum, OtpVerifResponseDto } from './dto/challenge.response.dto'; import { RedisCacheService } from 'src/common/services/cache.redis'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; @@ -58,14 +58,15 @@ export class OtpChallengeService implements IOtpChallengeService { challengeId: string, otpCode: string, merchantId: string - ): Promise { + ): Promise { try { // Récupérer le challenge depuis le cache - const cached:any = this.cacheService.get(challengeId); + const cached:any =await this.cacheService.get(challengeId,this.CACHE_PREFIX,); + this.logger.debug(`cache retrieve , ${cached}`) if (!cached) { return { - challengeId, + userAlias:"", merchantId, status: OtpChallengeStatusEnum.FAILED, error: { @@ -79,7 +80,7 @@ export class OtpChallengeService implements IOtpChallengeService { // Vérifier que le merchantId correspond if (cached.request.merchantId !== merchantId) { return { - challengeId, + userAlias:"", merchantId, status: OtpChallengeStatusEnum.FAILED, error: { @@ -105,7 +106,7 @@ export class OtpChallengeService implements IOtpChallengeService { return response; } catch (error) { return { - challengeId, + userAlias:"", merchantId, status: OtpChallengeStatusEnum.FAILED, error: {