otp challenge

This commit is contained in:
Mamadou Khoussa [028918 DSI/DAC/DIF/DS] 2025-10-29 18:13:48 +00:00
parent 039e9f067d
commit 8406d79800
8 changed files with 438 additions and 58 deletions

View File

@ -31,6 +31,8 @@ import { SubscriptionsModule } from './modules/subscriptions/subscriptions.modul
redis: { redis: {
host: configService.get('app.redis.host'), host: configService.get('app.redis.host'),
port: configService.get('app.redis.port'), port: configService.get('app.redis.port'),
password: configService.get('app.redis.password'),
}, },
}), }),
inject: [ConfigService], inject: [ConfigService],
@ -41,7 +43,9 @@ import { SubscriptionsModule } from './modules/subscriptions/subscriptions.modul
store: redisStore, store: redisStore,
host: configService.get('app.redis.host'), host: configService.get('app.redis.host'),
port: configService.get('app.redis.port'), port: configService.get('app.redis.port'),
password: configService.get('app.redis.password'),
ttl: 600, // 10 minutes default ttl: 600, // 10 minutes default
}), }),
inject: [ConfigService], inject: [ConfigService],
isGlobal: true, isGlobal: true,

View File

@ -0,0 +1,59 @@
import { Module } from '@nestjs/common';
import Redis from 'ioredis';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisCacheService } from './services/cache.redis';
/**
* Module pour le challenge OTP
* Gère l'injection de dépendances et la configuration
*/
@Module({
imports: [
],
controllers: [],
providers: [
{
provide: 'REDIS_CLIENT',
useFactory: (configService: ConfigService) => {
const redisConfig = {
host: configService.get('REDIS_HOST', 'localhost'),
port: configService.get('REDIS_PORT', 6379),
password: configService.get('REDIS_PASSWORD'), // ⚠️ Important
db: configService.get('REDIS_DB', 0),
keyPrefix: configService.get('REDIS_KEY_PREFIX', 'app:'),
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
maxRetriesPerRequest: 3,
enableOfflineQueue: false,
lazyConnect: false, // Connexion immédiate
};
const redis = new Redis(redisConfig);
// Gestion des événements de connexion
redis.on('connect', () => {
console.log('✅ Redis connected successfully');
});
redis.on('error', (err) => {
console.error('❌ Redis connection error:', err.message);
});
redis.on('ready', () => {
console.log('✅ Redis is ready');
});
return redis;
},
inject: [ConfigService],
},
RedisCacheService,
],
exports: [RedisCacheService],
})
export class CommonModule {}

View File

@ -0,0 +1,252 @@
import { Injectable, Inject, Logger } from '@nestjs/common';
import { Redis } from 'ioredis';
export interface CacheOptions {
ttl?: number; // Time to live en secondes
prefix?: string; // Préfixe pour les clés
}
@Injectable()
export class RedisCacheService {
private readonly logger = new Logger(RedisCacheService.name);
private readonly DEFAULT_TTL = 300; // 5 minutes
constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {}
/**
* Sauvegarder une valeur dans le cache
*/
async set<T>(
key: string,
value: T,
options?: CacheOptions
): Promise<void> {
try {
const fullKey = this.buildKey(key, options?.prefix);
const serializedValue = JSON.stringify(value);
const ttl = options?.ttl || this.DEFAULT_TTL;
await this.redis.setex(fullKey, ttl, serializedValue);
this.logger.debug(`Cache set: ${fullKey} (TTL: ${ttl}s)`);
} catch (error) {
this.logger.error(`Failed to set cache for key ${key}:`, error);
throw error;
}
}
/**
* Récupérer une valeur depuis le cache
*/
async get<T>(key: string, prefix?: string): Promise<T | null> {
try {
const fullKey = this.buildKey(key, prefix);
const data = await this.redis.get(fullKey);
if (!data) {
this.logger.debug(`Cache miss: ${fullKey}`);
return null;
}
this.logger.debug(`Cache hit: ${fullKey}`);
return JSON.parse(data) as T;
} catch (error) {
this.logger.error(`Failed to get cache for key ${key}:`, error);
return null;
}
}
/**
* Supprimer une valeur du cache
*/
async delete(key: string, prefix?: string): Promise<boolean> {
try {
const fullKey = this.buildKey(key, prefix);
const result = await this.redis.del(fullKey);
this.logger.debug(`Cache delete: ${fullKey}`);
return result > 0;
} catch (error) {
this.logger.error(`Failed to delete cache for key ${key}:`, error);
return false;
}
}
/**
* Vérifier si une clé existe
*/
async exists(key: string, prefix?: string): Promise<boolean> {
try {
const fullKey = this.buildKey(key, prefix);
const result = await this.redis.exists(fullKey);
return result === 1;
} catch (error) {
this.logger.error(`Failed to check existence for key ${key}:`, error);
return false;
}
}
/**
* Mettre à jour le TTL d'une clé
*/
async updateTTL(key: string, ttl: number, prefix?: string): Promise<boolean> {
try {
const fullKey = this.buildKey(key, prefix);
const result = await this.redis.expire(fullKey, ttl);
return result === 1;
} catch (error) {
this.logger.error(`Failed to update TTL for key ${key}:`, error);
return false;
}
}
/**
* Récupérer le TTL restant d'une clé
*/
async getTTL(key: string, prefix?: string): Promise<number> {
try {
const fullKey = this.buildKey(key, prefix);
return await this.redis.ttl(fullKey);
} catch (error) {
this.logger.error(`Failed to get TTL for key ${key}:`, error);
return -1;
}
}
/**
* Supprimer toutes les clés avec un préfixe donné
*/
async deleteByPrefix(prefix: string): Promise<number> {
try {
const pattern = `${prefix}*`;
const keys = await this.redis.keys(pattern);
if (keys.length === 0) {
return 0;
}
const result = await this.redis.del(...keys);
this.logger.debug(`Deleted ${result} keys with prefix: ${prefix}`);
return result;
} catch (error) {
this.logger.error(`Failed to delete by prefix ${prefix}:`, error);
return 0;
}
}
/**
* Récupérer plusieurs valeurs en une fois
*/
async mget<T>(keys: string[], prefix?: string): Promise<(T | null)[]> {
try {
const fullKeys = keys.map(key => this.buildKey(key, prefix));
const values = await this.redis.mget(...fullKeys);
return values.map(value => {
if (!value) return null;
try {
return JSON.parse(value) as T;
} catch {
return null;
}
});
} catch (error) {
this.logger.error('Failed to get multiple cache values:', error);
return keys.map(() => null);
}
}
/**
* Sauvegarder plusieurs valeurs en une fois
*/
async mset<T>(
entries: Array<{ key: string; value: T }>,
options?: CacheOptions
): Promise<void> {
try {
const pipeline = this.redis.pipeline();
const ttl = options?.ttl || this.DEFAULT_TTL;
for (const entry of entries) {
const fullKey = this.buildKey(entry.key, options?.prefix);
const serializedValue = JSON.stringify(entry.value);
pipeline.setex(fullKey, ttl, serializedValue);
}
await pipeline.exec();
this.logger.debug(`Batch set ${entries.length} cache entries`);
} catch (error) {
this.logger.error('Failed to set multiple cache values:', error);
throw error;
}
}
/**
* Incrémenter une valeur numérique
*/
async increment(key: string, prefix?: string, amount: number = 1): Promise<number> {
try {
const fullKey = this.buildKey(key, prefix);
return await this.redis.incrby(fullKey, amount);
} catch (error) {
this.logger.error(`Failed to increment key ${key}:`, error);
throw error;
}
}
/**
* Obtenir ou définir (get-or-set pattern)
*/
async getOrSet<T>(
key: string,
factory: () => Promise<T>,
options?: CacheOptions
): Promise<T> {
try {
// Essayer de récupérer depuis le cache
const cached = await this.get<T>(key, options?.prefix);
if (cached !== null) {
return cached;
}
// Si pas en cache, exécuter la factory
const value = await factory();
// Sauvegarder dans le cache
await this.set(key, value, options);
return value;
} catch (error) {
this.logger.error(`Failed getOrSet for key ${key}:`, error);
throw error;
}
}
/**
* Construire la clé complète avec préfixe
*/
private buildKey(key: string, prefix?: string): string {
return prefix ? `${prefix}:${key}` : key;
}
/**
* Vider tout le cache (ATTENTION: à utiliser avec précaution)
*/
async flushAll(): Promise<void> {
try {
await this.redis.flushdb();
this.logger.warn('Cache flushed completely');
} catch (error) {
this.logger.error('Failed to flush cache:', error);
throw error;
}
}
/**
* Obtenir des informations sur Redis
*/
async info(): Promise<string> {
return await this.redis.info();
}
}

View File

@ -12,5 +12,6 @@ export default registerAs('app', () => ({
redis: { redis: {
host: process.env.REDIS_HOST || 'localhost', host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT?? "6379", 10) || 6379, port: parseInt(process.env.REDIS_PORT?? "6379", 10) || 6379,
password: process.env.REDIS_PASSWORD || undefined,
}, },
})); }));

View File

@ -1,29 +1,32 @@
/** /**
* Structure de la requête pour l'API Orange DCB Challenge v2 * Structure de la requête pour l'API Orange DCB Challenge v2
*/ */
export interface OrangeChallengeRequest { export interface OrangeChallengeRequest {
country: string; challenge:{
method: string; country: string;
service: string; method: string;
partnerId: string; service: string;
identifier: { partnerId: string;
type: string; inputs: any[];
value: string; }
};
confirmationCode: string;
message: string;
otpLength: number;
senderName: string;
} }
/** /**
* Structure de la réponse de l'API Orange DCB Challenge * Structure de la réponse de l'API Orange DCB Challenge
*/ */
export interface OrangeChallengeResponse { export interface OrangeChallengeResponse {
challengeId?: string; challenge: {
message?: string; method: string,
expiresIn?: number; result: any[],
sessionId?: string; country: string,
service: string,
partnerId:string,
inputs: [ ]
}
location:string; // "/challenge/v1/challenges/c87d3360-c7bc-488f-86aa-02a537eaf1cc"
error?: { error?: {
code: number | string; code: number | string;
message: string; message: string;
@ -35,13 +38,20 @@ export interface OrangeChallengeResponse {
* Builder pour construire des requêtes Orange Challenge * Builder pour construire des requêtes Orange Challenge
*/ */
export class OrangeChallengeRequestBuilder { export class OrangeChallengeRequestBuilder {
private request: Partial<OrangeChallengeRequest> = {}; private request: OrangeChallengeRequest = {
challenge:{
country:'',
method: '',
service: '',
partnerId: '',
inputs:[]}
};
/** /**
* Définir le pays * Définir le pays
*/ */
withCountry(country: string): this { withCountry(country: string): this {
this.request.country = country; this.request.challenge.country = country;
return this; return this;
} }
@ -49,7 +59,7 @@ export class OrangeChallengeRequestBuilder {
* Définir la méthode d'authentification * Définir la méthode d'authentification
*/ */
withMethod(method: string): this { withMethod(method: string): this {
this.request.method = method; this.request.challenge.method = method;
return this; return this;
} }
@ -57,7 +67,7 @@ export class OrangeChallengeRequestBuilder {
* Définir le service * Définir le service
*/ */
withService(service: string): this { withService(service: string): this {
this.request.service = service; this.request.challenge.service = service;
return this; return this;
} }
@ -65,7 +75,7 @@ export class OrangeChallengeRequestBuilder {
* Définir l'ID du partenaire * Définir l'ID du partenaire
*/ */
withPartnerId(partnerId: string): this { withPartnerId(partnerId: string): this {
this.request.partnerId = partnerId; this.request.challenge.partnerId = partnerId;
return this; return this;
} }
@ -73,10 +83,12 @@ export class OrangeChallengeRequestBuilder {
* Définir l'identifiant (numéro de téléphone, etc.) * Définir l'identifiant (numéro de téléphone, etc.)
*/ */
withIdentifier(type: string, value: string): this { withIdentifier(type: string, value: string): this {
this.request.identifier = { this.request.challenge.inputs?.push({
type, "type": type,//, or “ISE2”
value "value": value// or “PDKSUB-XXXXXX”
}; },
)
return this; return this;
} }
@ -84,23 +96,41 @@ export class OrangeChallengeRequestBuilder {
* Définir le code de confirmation (OTP) * Définir le code de confirmation (OTP)
*/ */
withConfirmationCode(code: string): this { withConfirmationCode(code: string): this {
this.request.confirmationCode = code; this.request.challenge.inputs?.push(
{
"type": "confirmationCode",
"value": code
},
);
return this; return this;
} }
/** /**
* Définir le message OTP * Définir le message OTP
*/ */
//todo voir value par defaut
withMessage(message: string): this { withMessage(message: string): this {
this.request.message = message; this.request.challenge.inputs?.push(
{
"type": "message",
"value": message
},
)
return this; return this;
} }
/** /**
* Définir la longueur de l'OTP * Définir la longueur de l'OTP
*/ */
//todo mettre la valeur par defaut
withOtpLength(length: number): this { withOtpLength(length: number): this {
this.request.otpLength = length; this.request.challenge.inputs?.push(
{
"type": "otpLength",
"value": length
},
)
return this; return this;
} }
@ -108,7 +138,12 @@ export class OrangeChallengeRequestBuilder {
* Définir le nom de l'expéditeur * Définir le nom de l'expéditeur
*/ */
withSenderName(senderName: string): this { withSenderName(senderName: string): this {
this.request.senderName = senderName; this.request.challenge.inputs?.push(
{
"type": "senderName",
"value": senderName
}
)
return this; return this;
} }
@ -117,20 +152,20 @@ export class OrangeChallengeRequestBuilder {
*/ */
build(): OrangeChallengeRequest { build(): OrangeChallengeRequest {
// Validation des champs obligatoires // Validation des champs obligatoires
if (!this.request.country) { if (!this.request.challenge.country) {
throw new Error('Country is required'); throw new Error('Country is required');
} }
if (!this.request.method) { if (!this.request.challenge.method) {
throw new Error('Method is required'); throw new Error('Method is required');
} }
if (!this.request.service) { if (!this.request.challenge.service) {
throw new Error('Service is required'); throw new Error('Service is required');
} }
if (!this.request.partnerId) { if (!this.request.challenge.partnerId) {
throw new Error('Partner ID is required'); throw new Error('Partner ID is required');
} }
if (!this.request.identifier) { if (!this.request.challenge.inputs) {
throw new Error('Identifier is required'); throw new Error('inputs is required');
} }
return this.request as OrangeChallengeRequest; return this.request as OrangeChallengeRequest;
@ -140,7 +175,14 @@ export class OrangeChallengeRequestBuilder {
* Réinitialiser le builder * Réinitialiser le builder
*/ */
reset(): this { reset(): this {
this.request = {}; this.request ={
challenge:{
country:'',
method: '',
service: '',
partnerId: '',
inputs:[]}
};
return this; return this;
} }
} }

View File

@ -14,12 +14,16 @@ import {
//import { OtpChallengeResponseDto, OtpChallengeStatusEnum } from '../../dtos/otp-challenge-response.dto'; //import { OtpChallengeResponseDto, OtpChallengeStatusEnum } from '../../dtos/otp-challenge-response.dto';
import { OtpChallengeRequestDto } from '../dto/challenge.request.dto'; import { OtpChallengeRequestDto } from '../dto/challenge.request.dto';
import { OtpChallengeResponseDto, OtpChallengeStatusEnum } from '../dto/challenge.response.dto'; import { OtpChallengeResponseDto, OtpChallengeStatusEnum } from '../dto/challenge.response.dto';
import { Logger } from '@nestjs/common';
import { log } from 'console';
/** /**
* Adaptateur pour l'API Orange DCB v2 * Adaptateur pour l'API Orange DCB v2
* Gère l'authentification OAuth2 et les appels à l'API Challenge * Gère l'authentification OAuth2 et les appels à l'API Challenge
*/ */
export class OrangeAdapter { export class OrangeAdapter {
private readonly logger = new Logger(OrangeAdapter.name);
private axiosInstance: AxiosInstance; private axiosInstance: AxiosInstance;
private config: OrangeConfig; private config: OrangeConfig;
private accessToken: string | null = null; private accessToken: string | null = null;
@ -58,6 +62,9 @@ export class OrangeAdapter {
`${this.config.clientId}:${this.config.clientSecret}` `${this.config.clientId}:${this.config.clientSecret}`
).toString('base64'); ).toString('base64');
//this.logger.debug( `request to get acces token , ${this.config.baseUrl}${this.config.tokenEndpoint}`)
const response = await axios.post( const response = await axios.post(
`${this.config.baseUrl}${this.config.tokenEndpoint}`, `${this.config.baseUrl}${this.config.tokenEndpoint}`,
'grant_type=client_credentials', 'grant_type=client_credentials',
@ -131,13 +138,14 @@ export class OrangeAdapter {
orangeResponse: OrangeChallengeResponse, orangeResponse: OrangeChallengeResponse,
request: OtpChallengeRequestDto request: OtpChallengeRequestDto
): OtpChallengeResponseDto { ): OtpChallengeResponseDto {
const partsChallengeLocation= orangeResponse.location.split('/');
const response: OtpChallengeResponseDto = { const response: OtpChallengeResponseDto = {
challengeId: orangeResponse.challengeId || '', challengeId: partsChallengeLocation[partsChallengeLocation.length - 1],
merchantId: request.merchantId, merchantId: request.merchantId,
status: this.mapOrangeStatus(orangeResponse), status: this.mapOrangeStatus(orangeResponse),
message: orangeResponse.message, message: orangeResponse.challenge.result+"",
expiresIn: orangeResponse.expiresIn, expiresIn: new Date().getTime(),
sessionId: orangeResponse.sessionId, //sessionId: orangeResponse.sessionId,
requiresConfirmation: true, requiresConfirmation: true,
metadata: { metadata: {
provider: 'orange', provider: 'orange',
@ -167,7 +175,7 @@ export class OrangeAdapter {
return OtpChallengeStatusEnum.FAILED; return OtpChallengeStatusEnum.FAILED;
} }
if (orangeResponse.challengeId) { if (orangeResponse.location) {
return OtpChallengeStatusEnum.SENT; return OtpChallengeStatusEnum.SENT;
} }
@ -197,10 +205,15 @@ export class OrangeAdapter {
try { try {
// Obtenir le token // Obtenir le token
const token = await this.getAccessToken(); const token = await this.getAccessToken();
//this.logger.debug(`initiateChallenge --> acces token ${token}`);
// Mapper la requête // Mapper la requête
const orangeRequest = this.mapToOrangeRequest(request); const orangeRequest = this.mapToOrangeRequest(request);
this.logger.debug(
`[request to orange ]: ${JSON.stringify(orangeRequest, null, 2)}`,
)
// Appeler l'API Orange // Appeler l'API Orange
const response = await this.axiosInstance.post<OrangeChallengeResponse>( const response = await this.axiosInstance.post<OrangeChallengeResponse>(
this.config.challengeEndpoint, this.config.challengeEndpoint,

View File

@ -3,19 +3,21 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { OtpChallengeController } from './otp.challenge.controller'; import { OtpChallengeController } from './otp.challenge.controller';
import { OrangeConfig } from './adaptor/orange.config'; import { OrangeConfig } from './adaptor/orange.config';
import { OtpChallengeService } from './otp.challenge.service'; import { OtpChallengeService } from './otp.challenge.service';
import { CommonModule } from 'src/common/commonde.module';
/** /**
* Module pour le challenge OTP * Module pour le challenge OTP
* Gère l'injection de dépendances et la configuration * Gère l'injection de dépendances et la configuration
*/ */
@Module({ @Module({
imports: [ConfigModule], imports: [ConfigModule, CommonModule],
controllers: [OtpChallengeController], controllers: [OtpChallengeController],
providers: [ providers: [
{ {
provide: 'ORANGE_CONFIG', provide: 'ORANGE_CONFIG',
useFactory: (configService: ConfigService): OrangeConfig => ({ useFactory: (configService: ConfigService): OrangeConfig => ({
baseUrl: configService.get<string>('ORANGE_BASE_URL', 'https://webhook.site/69ce9344-f87b-421a-a494-c59eca7c54ce'), baseUrl: configService.get<string>('ORANGE_BASE_URL', 'https://webhook.site/69ce9344-f87b-421a-a494-c59eca7c54ce'),
//tokenUrl: configService.get<string>('ORANGE_BASE_URL', 'https://webhook.site/69ce9344-f87b-421a-a494-c59eca7c54ce'),
partnerId: configService.get<string>('ORANGE_PARTNER_ID', 'PDKSUB'), partnerId: configService.get<string>('ORANGE_PARTNER_ID', 'PDKSUB'),
clientId: configService.get<string>('ORANGE_CLIENT_ID', 'admin'), clientId: configService.get<string>('ORANGE_CLIENT_ID', 'admin'),
clientSecret: configService.get<string>('ORANGE_CLIENT_SECRET', 'admin'), clientSecret: configService.get<string>('ORANGE_CLIENT_SECRET', 'admin'),

View File

@ -1,9 +1,11 @@
import { Injectable, Inject } from '@nestjs/common'; import { Injectable, Inject, Logger } from '@nestjs/common';
import type { OrangeConfig } from './adaptor/orange.config'; import type { OrangeConfig } from './adaptor/orange.config';
import { OrangeAdapter } from './adaptor/orange.adaptor'; import { OrangeAdapter } from './adaptor/orange.adaptor';
import { OtpChallengeRequestDto } from './dto/challenge.request.dto'; import { OtpChallengeRequestDto } from './dto/challenge.request.dto';
import { IOtpChallengeService } from './otp.challenge.interface'; import { IOtpChallengeService } from './otp.challenge.interface';
import { OtpChallengeResponseDto, OtpChallengeStatusEnum } from './dto/challenge.response.dto'; import { OtpChallengeResponseDto, OtpChallengeStatusEnum } from './dto/challenge.response.dto';
import { RedisCacheService } from 'src/common/services/cache.redis';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
/** /**
* Service Hub pour gérer les challenges OTP * Service Hub pour gérer les challenges OTP
@ -11,12 +13,17 @@ import { OtpChallengeResponseDto, OtpChallengeStatusEnum } from './dto/challenge
*/ */
@Injectable() @Injectable()
export class OtpChallengeService implements IOtpChallengeService { export class OtpChallengeService implements IOtpChallengeService {
private readonly logger = new Logger(OtpChallengeService.name);
private readonly CACHE_PREFIX = 'otp:challenge';
private readonly DEFAULT_TTL = 300; // 5 minutes
private orangeAdapter: OrangeAdapter; private orangeAdapter: OrangeAdapter;
private challengeCache: Map<string, { request: OtpChallengeRequestDto; response: OtpChallengeResponseDto }>;
constructor(@Inject('ORANGE_CONFIG') private readonly orangeConfig: OrangeConfig) { constructor(
this.orangeAdapter = new OrangeAdapter(orangeConfig); //@Inject(CACHE_MANAGER) private cacheManager: Cache,
this.challengeCache = new Map(); @Inject('ORANGE_CONFIG') private readonly orangeConfig: OrangeConfig,
private readonly cacheService: RedisCacheService) {
this.orangeAdapter = new OrangeAdapter(this.orangeConfig);
} }
/** /**
@ -26,16 +33,16 @@ export class OtpChallengeService implements IOtpChallengeService {
try { try {
// Appeler l'adaptateur Orange // Appeler l'adaptateur Orange
const response = await this.orangeAdapter.initiateChallenge(request); const response = await this.orangeAdapter.initiateChallenge(request);
if (response.challengeId || true) {
this.challengeCache.set(response.challengeId, { request, response });
// Nettoyer le cache après expiration (par défaut 5 minutes) if (response.challengeId || true) {
const expirationTime = (response.expiresIn || 300) * 1000; // this.cacheManager
setTimeout(() => { await this.cacheService.set(response.challengeId , {request:request,response:response}, {
this.challengeCache.delete(response.challengeId); prefix: this.CACHE_PREFIX,
}, expirationTime); ttl: this.DEFAULT_TTL,
});
} }
return response; return response;
@ -54,7 +61,7 @@ export class OtpChallengeService implements IOtpChallengeService {
): Promise<OtpChallengeResponseDto> { ): Promise<OtpChallengeResponseDto> {
try { try {
// Récupérer le challenge depuis le cache // Récupérer le challenge depuis le cache
const cached = this.challengeCache.get(challengeId); const cached:any = this.cacheService.get(challengeId);
if (!cached) { if (!cached) {
return { return {
@ -92,7 +99,7 @@ export class OtpChallengeService implements IOtpChallengeService {
// Mettre à jour le cache // Mettre à jour le cache
if (response.status === OtpChallengeStatusEnum.VERIFIED) { if (response.status === OtpChallengeStatusEnum.VERIFIED) {
this.challengeCache.set(challengeId, { request: cached.request, response }); this.cacheService.set(challengeId, { request: cached.request, response });
} }
return response; return response;