dcb-service-core-api/src/modules/challenge/otp.challenge.controller.ts
Mamadou Khoussa [028918 DSI/DAC/DIF/DS] f87650dcc2 fix it otp challenge
2025-10-24 22:39:02 +00:00

421 lines
11 KiB
TypeScript

import {
Controller,
Post,
Get,
Body,
Param,
Query,
HttpStatus,
HttpException,
UseGuards,
Headers,
Logger
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiParam,
ApiQuery,
ApiHeader,
ApiBearerAuth
} from '@nestjs/swagger';
import { OtpChallengeResponseDto, OtpChallengeStatusEnum } from './dto/challenge.response.dto';
import { OtpChallengeRequestDto } from './dto/challenge.request.dto';
/**
* Controller pour les endpoints de challenge OTP
* Architecture: Merchant -> Hub (ce controller) -> Orange (via adapter)
*/
@ApiTags('OTP Challenge')
@Controller('api/v1/otp-challenge')
export class OtpChallengeController {
private readonly logger = new Logger(OtpChallengeController.name);
constructor(private readonly otpChallengeService: OtpChallengeService) {}
/**
* Endpoint 1: Initier un challenge OTP
* POST /api/v1/otp-challenge/initiate
*/
@Post('initiate')
@ApiOperation({
summary: 'Initier un challenge OTP',
description: 'Envoie un code OTP au numéro de téléphone spécifié via SMS, USSD ou IVR'
})
@ApiResponse({
status: HttpStatus.CREATED,
description: 'Challenge OTP initié avec succès',
type: OtpChallengeResponseDto
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Requête invalide'
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: 'Erreur serveur'
})
@ApiHeader({
name: 'X-Merchant-ID',
description: 'Identifiant du merchant',
required: true
})
@ApiHeader({
name: 'X-API-Key',
description: 'Clé API du merchant',
required: true
})
async initiateChallenge(
@Body() request: OtpChallengeRequestDto,
@Headers('x-merchant-id') merchantId: string,
@Headers('x-api-key') apiKey: string
): Promise<OtpChallengeResponseDto> {
this.logger.log(`[INITIATE] Merchant: ${merchantId}, Transaction: ${request.transactionId}`);
try {
// Valider les headers
this.validateMerchantHeaders(merchantId, apiKey);
// S'assurer que le merchantId dans le body correspond au header
if (request.merchantId && request.merchantId !== merchantId) {
throw new HttpException(
'Merchant ID in body does not match header',
HttpStatus.BAD_REQUEST
);
}
// Définir le merchantId depuis le header si non présent
request.merchantId = merchantId;
// Appeler le service
const response = await this.otpChallengeService.initiateChallenge(request);
// Logger le résultat
this.logger.log(
`[INITIATE] Result - ChallengeId: ${response.challengeId}, Status: ${response.status}`
);
// Si échec, retourner une erreur HTTP appropriée
if (response.status === OtpChallengeStatusEnum.FAILED && response.error) {
throw new HttpException(
{
statusCode: HttpStatus.BAD_REQUEST,
message: response.error.message,
error: response.error
},
HttpStatus.BAD_REQUEST
);
}
return response;
} catch (error) {
this.logger.error(`[INITIATE] Error: ${error.message}`, error.stack);
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
{
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'Failed to initiate OTP challenge',
error: error.message
},
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
/**
* Endpoint 2: Vérifier un code OTP
* POST /api/v1/otp-challenge/:challengeId/verify
*/
@Post(':challengeId/verify')
@ApiOperation({
summary: 'Vérifier un code OTP',
description: 'Vérifie le code OTP entré par l\'utilisateur'
})
@ApiParam({
name: 'challengeId',
description: 'Identifiant du challenge',
type: String
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Code OTP vérifié avec succès',
type: OtpChallengeResponseDto
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Code OTP invalide'
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Challenge non trouvé'
})
@ApiHeader({
name: 'X-Merchant-ID',
description: 'Identifiant du merchant',
required: true
})
@ApiHeader({
name: 'X-API-Key',
description: 'Clé API du merchant',
required: true
})
async verifyOtp(
@Param('challengeId') challengeId: string,
@Body('otpCode') otpCode: string,
@Headers('x-merchant-id') merchantId: string,
@Headers('x-api-key') apiKey: string
): Promise<OtpChallengeResponseDto> {
this.logger.log(`[VERIFY] Merchant: ${merchantId}, Challenge: ${challengeId}`);
try {
// Valider les headers
this.validateMerchantHeaders(merchantId, apiKey);
// Valider le code OTP
if (!otpCode || otpCode.trim().length === 0) {
throw new HttpException('OTP code is required', HttpStatus.BAD_REQUEST);
}
// Appeler le service
const response = await this.otpChallengeService.verifyOtp(
challengeId,
otpCode,
merchantId
);
// Logger le résultat
this.logger.log(`[VERIFY] Result - Status: ${response.status}`);
// Si échec, retourner une erreur appropriée
if (response.status === OtpChallengeStatusEnum.FAILED && response.error) {
const statusCode =
response.error.code === 'CHALLENGE_NOT_FOUND'
? HttpStatus.NOT_FOUND
: HttpStatus.BAD_REQUEST;
throw new HttpException(
{
statusCode,
message: response.error.message,
error: response.error
},
statusCode
);
}
return response;
} catch (error) {
this.logger.error(`[VERIFY] Error: ${error.message}`, error.stack);
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
{
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'Failed to verify OTP code',
error: error.message
},
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
/**
* Endpoint 3: Obtenir le statut d'un challenge
* GET /api/v1/otp-challenge/:challengeId/status
*/
@Get(':challengeId/status')
@ApiOperation({
summary: 'Obtenir le statut d\'un challenge',
description: 'Récupère le statut actuel d\'un challenge OTP'
})
@ApiParam({
name: 'challengeId',
description: 'Identifiant du challenge',
type: String
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Statut du challenge récupéré avec succès',
type: OtpChallengeResponseDto
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Challenge non trouvé'
})
@ApiHeader({
name: 'X-Merchant-ID',
description: 'Identifiant du merchant',
required: true
})
@ApiHeader({
name: 'X-API-Key',
description: 'Clé API du merchant',
required: true
})
async getChallengeStatus(
@Param('challengeId') challengeId: string,
@Headers('x-merchant-id') merchantId: string,
@Headers('x-api-key') apiKey: string
): Promise<OtpChallengeResponseDto> {
this.logger.log(`[STATUS] Merchant: ${merchantId}, Challenge: ${challengeId}`);
try {
// Valider les headers
this.validateMerchantHeaders(merchantId, apiKey);
// Appeler le service
const response = await this.otpChallengeService.getChallengeStatus(
challengeId,
merchantId
);
// Si non trouvé
if (response.status === OtpChallengeStatusEnum.EXPIRED ||
(response.error && response.error.code === 'CHALLENGE_NOT_FOUND')) {
throw new HttpException(
{
statusCode: HttpStatus.NOT_FOUND,
message: 'Challenge not found or expired',
error: response.error
},
HttpStatus.NOT_FOUND
);
}
this.logger.log(`[STATUS] Result - Status: ${response.status}`);
return response;
} catch (error) {
this.logger.error(`[STATUS] Error: ${error.message}`, error.stack);
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
{
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'Failed to get challenge status',
error: error.message
},
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
/**
* Endpoint 4: Renvoyer un code OTP
* POST /api/v1/otp-challenge/:challengeId/resend
*/
@Post(':challengeId/resend')
@ApiOperation({
summary: 'Renvoyer un code OTP',
description: 'Renvoie un nouveau code OTP pour un challenge existant'
})
@ApiParam({
name: 'challengeId',
description: 'Identifiant du challenge',
type: String
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Code OTP renvoyé avec succès',
type: OtpChallengeResponseDto
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Challenge non trouvé'
})
@ApiHeader({
name: 'X-Merchant-ID',
description: 'Identifiant du merchant',
required: true
})
@ApiHeader({
name: 'X-API-Key',
description: 'Clé API du merchant',
required: true
})
async resendOtp(
@Param('challengeId') challengeId: string,
@Headers('x-merchant-id') merchantId: string,
@Headers('x-api-key') apiKey: string
): Promise<OtpChallengeResponseDto> {
this.logger.log(`[RESEND] Merchant: ${merchantId}, Challenge: ${challengeId}`);
try {
// Valider les headers
this.validateMerchantHeaders(merchantId, apiKey);
// Appeler le service
const response = await this.otpChallengeService.resendOtp(
challengeId,
merchantId
);
// Si échec
if (response.status === OtpChallengeStatusEnum.FAILED && response.error) {
const statusCode =
response.error.code === 'CHALLENGE_NOT_FOUND'
? HttpStatus.NOT_FOUND
: HttpStatus.BAD_REQUEST;
throw new HttpException(
{
statusCode,
message: response.error.message,
error: response.error
},
statusCode
);
}
this.logger.log(`[RESEND] Result - New ChallengeId: ${response.challengeId}`);
return response;
} catch (error) {
this.logger.error(`[RESEND] Error: ${error.message}`, error.stack);
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
{
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'Failed to resend OTP code',
error: error.message
},
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
/**
* Valider les headers du merchant
*/
private validateMerchantHeaders(merchantId: string, apiKey: string): void {
if (!merchantId || merchantId.trim().length === 0) {
throw new HttpException(
'X-Merchant-ID header is required',
HttpStatus.UNAUTHORIZED
);
}
if (!apiKey || apiKey.trim().length === 0) {
throw new HttpException(
'X-API-Key header is required',
HttpStatus.UNAUTHORIZED
);
}
// TODO: Implémenter la validation de l'API Key
// Cette logique devrait vérifier que l'API Key est valide pour le merchant
}
}