otp challenge
This commit is contained in:
parent
039e9f067d
commit
8406d79800
@ -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,
|
||||||
|
|||||||
59
src/common/commonde.module.ts
Normal file
59
src/common/commonde.module.ts
Normal 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 {}
|
||||||
252
src/common/services/cache.redis.ts
Normal file
252
src/common/services/cache.redis.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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'),
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user