421 lines
11 KiB
TypeScript
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
|
|
}
|
|
} |