Kaynağa Gözat

(feat securité api) ajout des roles et de la securité api

Brin AMONKONAN 1 yıl önce
ebeveyn
işleme
50ec83a16e
25 değiştirilmiş dosya ile 387 ekleme ve 21 silme
  1. 45 7
      app_transport_front/src/components/TripSearchForm.vue
  2. 16 1
      docker-compose.yml
  3. 23 0
      node_app/prisma/migrations/20250507220708_update_transaction_relations/migration.sql
  4. 11 0
      node_app/prisma/migrations/20250507223029_add_qrcode_reservation_relation/migration.sql
  5. 10 0
      node_app/prisma/migrations/20250516081748_add_qrcode_reservation_relation/migration.sql
  6. 2 0
      node_app/prisma/migrations/20250516082418_add_user_role/migration.sql
  7. 52 8
      node_app/prisma/schema.prisma
  8. 10 2
      node_app/src/application/dto/responses/user-response.dto.ts
  9. 14 0
      node_app/src/domain/entities/user.entity.ts
  10. 3 1
      node_app/src/infrastructure/mappers/user.mapper.ts
  11. 1 1
      node_app/src/infrastructure/services/auth.service.ts
  12. 2 1
      node_app/src/infrastructure/services/mocks/auth.service.mock.ts
  13. 9 0
      node_app/src/presentation/controllers/auth.controller.ts
  14. 1 0
      node_app/src/presentation/controllers/user.controller.spec.ts
  15. 6 0
      node_app/src/presentation/controllers/user.controller.ts
  16. 4 0
      node_app/src/presentation/decorators/roles.decorator.ts
  17. 15 0
      node_app/src/presentation/guards/auth.guard.ts
  18. 8 0
      notification_service/Dockerfile
  19. 22 0
      notification_service/package.json
  20. 35 0
      notification_service/service/cinetpay.ts
  21. 13 0
      notification_service/tsconfig.json
  22. 3 0
      payement_service/.env.dev
  23. 36 0
      payement_service/Dockerfile
  24. 5 0
      payement_service/package.json
  25. 41 0
      payement_service/src/index.ts

+ 45 - 7
app_transport_front/src/components/TripSearchForm.vue

@@ -1,15 +1,25 @@
 <template>
-  <div class="trip-search-form-container">
+  <div class="trip-search-form-container w-full">
     <h3>Trouvez votre prochain voyage</h3>
     <form @submit.prevent="handleSearch" class="search-form">
       <div class="form-row">
         <div class="form-group">
           <label for="departure">Ville de départ</label>
-          <input type="text" id="departure" v-model="searchCriteria.departure" placeholder="Ex: Abidjan" required />
+          <select id="departure" class="flex w-full" v-model="searchCriteria.departure" required>
+            <option value="" disabled>Sélectionner une ville</option>
+            <option v-for="city in cities" :key="city.name" :value="city.name">
+              {{ city.name }}
+            </option>
+          </select>
         </div>
         <div class="form-group">
           <label for="arrival">Ville d'arrivée</label>
-          <input type="text" id="arrival" v-model="searchCriteria.arrival" placeholder="Ex: Yamoussoukro" required />
+          <select id="arrival" class="flex w-full" v-model="searchCriteria.arrival" required>
+            <option value="" disabled>Sélectionner une ville</option>
+            <option v-for="city in cities" :key="city.name" :value="city.name">
+              {{ city.name }}
+            </option>
+          </select>
         </div>
       </div>
 
@@ -39,7 +49,15 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
+import { ref, onMounted } from 'vue';
+
+interface City {
+  name: string;
+  // Vous pouvez ajouter d'autres propriétés ici si elles existent dans votre JSON
+  // par exemple: id: number;
+}
+
+const cities = ref<City[]>([]);
 
 const searchCriteria = ref({
   departure: '',
@@ -55,6 +73,24 @@ const handleSearch = () => {
   // Valider les données ici si nécessaire avant d'émettre
   emit('search-trips', { ...searchCriteria.value });
 };
+
+onMounted(async () => {
+  try {
+    // Assurez-vous que civ.cities.json est dans le dossier public/datas/
+    const response = await fetch('/datas/civ.cities.json');
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    }
+    const data = await response.json();
+    // Si votre JSON est un tableau d'objets ayant une propriété "name"
+    // Si la structure est [{"nom": "Abidjan"}, ...] utilisez data.map(c => ({ name: c.nom }))
+    // Si c'est un tableau de chaînes ["Abidjan", ...], utilisez data.map(name => ({ name }))
+    cities.value = data; 
+  } catch (error) {
+    console.error("Impossible de charger les villes:", error);
+    // Gérer l'erreur, par exemple, afficher un message à l'utilisateur
+  }
+});
 </script>
 
 <style scoped>
@@ -63,7 +99,6 @@ const handleSearch = () => {
   padding: 25px;
   border-radius: 10px;
   box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
-  max-width: 600px;
   margin: 20px auto;
 }
 
@@ -95,17 +130,20 @@ const handleSearch = () => {
 
 .search-form input[type="text"],
 .search-form input[type="date"],
-.search-form input[type="number"] {
+.search-form input[type="number"],
+.search-form select { /* Ajout de select ici */
   padding: 10px;
   border: 1px solid #ddd;
   border-radius: 5px;
   font-size: 1em;
   transition: border-color 0.3s;
+  background-color: white; /* Assurer un fond blanc pour les select */
 }
 
 .search-form input[type="text"]:focus,
 .search-form input[type="date"]:focus,
-.search-form input[type="number"]:focus {
+.search-form input[type="number"]:focus,
+.search-form select:focus { /* Ajout de select:focus ici */
   border-color: #4b8ef1;
   outline: none;
 }

+ 16 - 1
docker-compose.yml

@@ -52,7 +52,22 @@ services:
       - SMTP_USER=ton_email
       - SMTP_PASS=mot_de_passe
       - SMS_API_KEY=ta_clé_api_sms
-      
+
+  payment_service:
+    build:
+      context: ./payment_service
+      dockerfile: Dockerfile
+    container_name: payment_service
+    ports:
+      - "4002:4002"
+    environment:
+      - PAYMENT_PROVIDER=cinetpay
+      - CINETPAY_API_KEY=xxx
+      - CINETPAY_SITE_ID=xxx
+      - CALLBACK_URL=http://localhost:4002/webhook
+    networks:
+      - gogs-network
+
   redis:
     image: redis:7
     container_name: redis_cache

+ 23 - 0
node_app/prisma/migrations/20250507220708_update_transaction_relations/migration.sql

@@ -0,0 +1,23 @@
+-- CreateTable
+CREATE TABLE `Transaction` (
+    `id` VARCHAR(191) NOT NULL,
+    `amount` DOUBLE NOT NULL,
+    `currency` VARCHAR(191) NOT NULL,
+    `status` VARCHAR(191) NOT NULL,
+    `provider` VARCHAR(191) NOT NULL,
+    `reference` VARCHAR(191) NOT NULL,
+    `metadata` JSON NULL,
+    `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+    `updatedAt` DATETIME(3) NOT NULL,
+    `userId` INTEGER NOT NULL,
+    `reservationId` INTEGER NOT NULL,
+
+    UNIQUE INDEX `Transaction_reservationId_key`(`reservationId`),
+    PRIMARY KEY (`id`)
+) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+-- AddForeignKey
+ALTER TABLE `Transaction` ADD CONSTRAINT `Transaction_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE `Transaction` ADD CONSTRAINT `Transaction_reservationId_fkey` FOREIGN KEY (`reservationId`) REFERENCES `Reservation`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

+ 11 - 0
node_app/prisma/migrations/20250507223029_add_qrcode_reservation_relation/migration.sql

@@ -0,0 +1,11 @@
+-- CreateTable
+CREATE TABLE `Qrcode` (
+    `id` VARCHAR(191) NOT NULL,
+    `reservationId` INTEGER NOT NULL,
+
+    UNIQUE INDEX `Qrcode_reservationId_key`(`reservationId`),
+    PRIMARY KEY (`id`)
+) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+-- AddForeignKey
+ALTER TABLE `Qrcode` ADD CONSTRAINT `Qrcode_reservationId_fkey` FOREIGN KEY (`reservationId`) REFERENCES `Reservation`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

+ 10 - 0
node_app/prisma/migrations/20250516081748_add_qrcode_reservation_relation/migration.sql

@@ -0,0 +1,10 @@
+/*
+  Warnings:
+
+  - Added the required column `value` to the `Qrcode` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE `Qrcode` ADD COLUMN `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+    ADD COLUMN `scannedAt` DATETIME(3) NULL,
+    ADD COLUMN `value` VARCHAR(191) NOT NULL;

+ 2 - 0
node_app/prisma/migrations/20250516082418_add_user_role/migration.sql

@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE `User` ADD COLUMN `role` ENUM('USER', 'ADMIN', 'DRIVER', 'MANAGER', 'EMPLOYEE') NOT NULL DEFAULT 'USER';

+ 52 - 8
node_app/prisma/schema.prisma

@@ -11,6 +11,15 @@ datasource mysql {
   url      = env("DATABASE_URL")
 }
 
+// Définition de l'Enum pour les rôles utilisateurs
+enum UserRole {
+  USER    // Utilisateur standard
+  ADMIN   // Administrateur système
+  DRIVER  // Chauffeur
+  MANAGER // Manager de compagnie
+  EMPLOYEE // Employee de la compagnie
+}
+
 model Company {
   id        Int        @id @default(autoincrement())
   name      String     @unique
@@ -41,9 +50,11 @@ model User {
   telephone      String?       // Rendu optionnel
   email          String        @unique // Reste obligatoire
   password       String        // Reste obligatoire
+  role           UserRole      @default(USER) // Ajout du champ role avec une valeur par défaut
   reservations   Reservation[] @relation("UserReservations")
   driver         Driver?       @relation("UserDriver")
   managedCompany Company?      @relation("CompanyManager")
+  transactions   Transaction[] @relation("UserTransactions") // Ajout de la relation avec Transaction
 }
 
 model Vehicule {
@@ -81,12 +92,45 @@ model Trajet {
 }
 
 model Reservation {
-  id       Int      @id @default(autoincrement())
-  dateAt   DateTime
-  place    Place    @relation("PlaceReservations", fields: [placeId], references: [id])
-  placeId  Int
-  user     User     @relation("UserReservations", fields: [userId], references: [id])
-  userId   Int
-  trajet   Trajet   @relation("TrajetReservations", fields: [trajetId], references: [id])
-  trajetId Int
+  id          Int      @id @default(autoincrement())
+  dateAt      DateTime
+  place       Place    @relation("PlaceReservations", fields: [placeId], references: [id])
+  placeId     Int
+  user        User     @relation("UserReservations", fields: [userId], references: [id])
+  userId      Int
+  trajet      Trajet   @relation("TrajetReservations", fields: [trajetId], references: [id])
+  trajetId    Int
+  transaction Transaction? @relation("ReservationTransaction") // Ajout de la relation avec Transaction
+  qrcode      Qrcode?      @relation("ReservationQrcode")    // Relation unique avec Qrcode
+}
+//  a revoir 
+model Transaction {
+  id            String   @id @default(uuid())
+  amount        Float
+  currency      String
+  status        String   // pending, success, failed
+  provider      String   // cinetpay, stripe, wave, etc.
+  reference     String   // référence externe (PSP)
+  metadata      Json?
+  createdAt     DateTime @default(now())
+  updatedAt     DateTime @updatedAt
+
+  // Relation avec User
+  userId        Int    // Changé de String à Int
+  user          User     @relation("UserTransactions", fields: [userId], references: [id])
+
+  // Relation avec Reservation (une transaction est unique à une réservation)
+  reservationId Int      @unique
+  reservation   Reservation @relation("ReservationTransaction", fields: [reservationId], references: [id])
+}
+
+model Qrcode {
+  id            String   @id @default(uuid())
+  // Relation unique avec la Reservation
+  reservationId Int      @unique // Assure qu'un Qrcode est lié à une seule Reservation
+  reservation   Reservation @relation("ReservationQrcode", fields: [reservationId], references: [id])
+  value         String       // la valeur encodée (ex: JWT, UUID, URL...)
+  createdAt     DateTime     @default(now())
+  scannedAt     DateTime?    // Suivre les scans
 }
+// fin

+ 10 - 2
node_app/src/application/dto/responses/user-response.dto.ts

@@ -11,13 +11,21 @@ export class UserResponseDto {
   telephone: string;
   createdAt: Date;
   updatedAt: Date;
+  role: string;
 
   constructor(partial: Partial<UserResponseDto>) {
     Object.assign(this, partial);
   }
 
   static fromEntity(user: User): UserResponseDto {
-    const userData = user;
-    return new UserResponseDto(userData);
+    return new UserResponseDto({
+      id: user.id,
+      email: user.email,
+      name: user.name,
+      otherName: user.otherName,
+      address: user.address,
+      telephone: user.telephone,
+      role: user.role,
+    });
   }
 }

+ 14 - 0
node_app/src/domain/entities/user.entity.ts

@@ -2,6 +2,15 @@ import { Reservation } from './reservation.entity';
 import { Driver } from './driver.entity';
 import { Company } from './company.entity';
 
+// Définition de l'Enum pour les rôles utilisateurs
+export enum UserRole {
+  USER = 'USER',
+  ADMIN = 'ADMIN',
+  DRIVER = 'DRIVER',
+  MANAGER = 'MANAGER',
+  EMPLOYEE = 'EMPLOYEE',
+}
+
 export class User {
   readonly id: number;
   name: string;
@@ -10,11 +19,16 @@ export class User {
   telephone: string;
   email: string;
   password: string;
+  role: UserRole; // Ajout de la propriété role
   reservations?: Reservation[];
   driver?: Driver;
   managedCompany?: Company;
 
   constructor(partial: Partial<User>) {
     Object.assign(this, partial);
+    // Assigner une valeur par défaut si le rôle n'est pas fourni
+    if (!this.role) {
+      this.role = UserRole.USER;
+    }
   }
 }

+ 3 - 1
node_app/src/infrastructure/mappers/user.mapper.ts

@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-unsafe-assignment */
 import { IPrismaMapper } from './prisma-mapper.interface';
 import { User as PrismaUser } from '@prisma/client';
-import { User } from 'src/domain/entities/user.entity';
+import { User, UserRole } from 'src/domain/entities/user.entity';
 
 export class UserMapper implements IPrismaMapper<PrismaUser, User> {
   toDomain(prismaUser: PrismaUser): User {
@@ -13,6 +13,7 @@ export class UserMapper implements IPrismaMapper<PrismaUser, User> {
       password: prismaUser.password,
       address: prismaUser.address || undefined,
       telephone: prismaUser.telephone || undefined,
+      role: prismaUser.role ? (prismaUser.role as UserRole) : undefined,
     });
   }
 
@@ -25,6 +26,7 @@ export class UserMapper implements IPrismaMapper<PrismaUser, User> {
       password: user.password,
       address: user.address || '',
       telephone: user.telephone || '',
+      role: user.role || '',
     };
   }
 

+ 1 - 1
node_app/src/infrastructure/services/auth.service.ts

@@ -26,7 +26,7 @@ export class AuthService implements IAuthService {
 
   async generateToken(user: User): Promise<string> {
     return new Promise<string>((resolve) => {
-      const payload = { email: user.email, sub: user.id };
+      const payload = { email: user.email, sub: user.id, userRole: user.role };
       const token = this.jwtService.sign(payload);
       resolve(token);
     });

+ 2 - 1
node_app/src/infrastructure/services/mocks/auth.service.mock.ts

@@ -1,6 +1,6 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { IAuthService } from '../../../application/interfaces/i-auth.service';
-import { User } from 'src/domain/entities/user.entity';
+import { User, UserRole } from 'src/domain/entities/user.entity';
 import { JwtService } from '@nestjs/jwt';
 
 @Injectable()
@@ -16,6 +16,7 @@ export class MockAuthService implements IAuthService {
     telephone: '+33123456789',
     otherName: 'Test',
     password: 'password',
+    role: UserRole.USER,
   };
 
   // Token fictif pour les tests

+ 9 - 0
node_app/src/presentation/controllers/auth.controller.ts

@@ -38,6 +38,15 @@ export class LoginDto {
   @IsString({ message: 'Le mot de passe doit être une chaîne de caractères' })
   @IsNotEmpty({ message: 'Le mot de passe est requis' })
   password: string;
+
+  // @ApiProperty({
+  //   example: 'USER',
+  //   description:
+  //     "Rôle de l'utilisateur (USER, ADMIN, DRIVER, MANAGER, EMPLOYEE)",
+  // })
+  // @IsString({ message: 'Le rôle doit être une chaîne de caractères' })
+  // @IsNotEmpty({ message: 'Le rôle est requis' })
+  // userRole: string;
 }
 
 @ApiTags('Authentification')

+ 1 - 0
node_app/src/presentation/controllers/user.controller.spec.ts

@@ -48,6 +48,7 @@ describe('UserController', () => {
       credentials.email,
       credentials.password,
     );
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
     const token = await authService.generateToken(result);
 
     // Assert - Vérification des résultats

+ 6 - 0
node_app/src/presentation/controllers/user.controller.ts

@@ -25,6 +25,7 @@ import { UpdateUserDto } from '../../application/dto/update-user.dto';
 import { DeleteUserUseCase } from '../../application/use-cases/delete-user.use-case';
 import { FindAllUsersUseCase } from '../../application/use-cases/find-all-users.use-case';
 import { Public } from '../decorators/public.decorator';
+import { Roles } from '../decorators/roles.decorator';
 
 @Controller('users')
 @UseInterceptors(BodyParserInterceptor)
@@ -56,6 +57,7 @@ export class UserController {
 
   @Get(':id')
   // Route protégée - nécessite une authentification
+  @Roles('ADMIN', 'MANAGER')
   @ApiOperation({ description: 'Récupérer un utilisateur par son ID' })
   @ApiResponse({ status: 200, description: 'Utilisateur trouvé' })
   @ApiResponse({ status: 404, description: 'Utilisateur non trouvé' })
@@ -67,6 +69,7 @@ export class UserController {
 
   @Put(':id')
   // Route protégée - nécessite une authentification
+  @Roles('ADMIN', 'MANAGER')
   @ApiOperation({ description: 'Mettre à jour un utilisateur par son ID' })
   @ApiResponse({
     status: 200,
@@ -84,6 +87,7 @@ export class UserController {
 
   @Patch(':id')
   // Route protégée - nécessite une authentification
+  @Roles('ADMIN', 'MANAGER')
   @ApiOperation({
     description: 'Mettre à jour partiellement un utilisateur par son ID',
   })
@@ -103,6 +107,7 @@ export class UserController {
 
   @Delete(':id')
   // Route protégée - nécessite une authentification
+  @Roles('ADMIN')
   @ApiOperation({ description: 'Supprimer un utilisateur par son ID' })
   @ApiResponse({ status: 200, description: 'Utilisateur supprimé avec succès' })
   @ApiResponse({ status: 404, description: 'Utilisateur non trouvé' })
@@ -115,6 +120,7 @@ export class UserController {
 
   @Get()
   // Route protégée - nécessite une authentification
+  @Roles('ADMIN')
   @ApiOperation({ description: 'Récupérer tous les utilisateurs' })
   @ApiResponse({ status: 200, description: 'Liste des utilisateurs' })
   @ApiResponse({ status: 401, description: 'Non autorisé' })

+ 4 - 0
node_app/src/presentation/decorators/roles.decorator.ts

@@ -0,0 +1,4 @@
+import { SetMetadata } from '@nestjs/common';
+
+export const ROLES_KEY = 'roles';
+export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

+ 15 - 0
node_app/src/presentation/guards/auth.guard.ts

@@ -9,6 +9,7 @@ import {
 import { Reflector } from '@nestjs/core';
 import { IAuthService } from '../../application/interfaces/i-auth.service';
 import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
+import { ROLES_KEY } from '../decorators/roles.decorator';
 
 @Injectable()
 export class AuthGuard implements CanActivate {
@@ -46,6 +47,20 @@ export class AuthGuard implements CanActivate {
     try {
       const payload = await this.authService.verifyToken(token);
       request.user = payload;
+      request.role = payload.userRole || payload.role;
+
+      // Vérification des rôles si le décorateur @Roles est présent
+      const requiredRoles = this.reflector.getAllAndOverride<string[]>(
+        ROLES_KEY,
+        [context.getHandler(), context.getClass()],
+      );
+      if (requiredRoles && requiredRoles.length > 0) {
+        // Si l'utilisateur n'a pas de rôle ou que son rôle n'est pas dans la liste, accès refusé
+        if (!request.role || !requiredRoles.includes(request.role)) {
+          return false;
+        }
+      }
+
       return true;
     } catch (error) {
       return false;

+ 8 - 0
notification_service/Dockerfile

@@ -0,0 +1,8 @@
+FROM node:18
+WORKDIR /app
+RUN npm install fastify fastify-socket.io nodemailer twilio
+RUN npm install -D typescript ts-node @types/node @types/nodemailer @types/twilio
+COPY package*.json ./
+RUN npm install
+COPY . .
+CMD ["npm", "run", "dev"]

+ 22 - 0
notification_service/package.json

@@ -0,0 +1,22 @@
+{
+    "name": "notifications_service",
+    "version": "1.0.0",
+    "main": "src/index.ts",
+    "scripts": {
+      "dev": "ts-node src/index.ts"
+    },
+    "dependencies": {
+      "fastify": "^4.x",
+      "fastify-socket.io": "^5.x",
+      "nodemailer": "^6.x",
+      "twilio": "^4.x"
+    },
+    "devDependencies": {
+      "typescript": "^5.x",
+      "ts-node": "^10.x",
+      "@types/node": "^20.x",
+      "@types/nodemailer": "^6.x",
+      "@types/twilio": "^3.x"
+    }
+  }
+  

+ 35 - 0
notification_service/service/cinetpay.ts

@@ -0,0 +1,35 @@
+// src/services/cinetpay.ts
+import axios from 'axios';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const baseURL = process.env.CINETPAY_BASE_URL || 'https://api-checkout.cinetpay.com/v2';
+
+const api = axios.create({
+  baseURL,
+  headers: { 'Content-Type': 'application/json' },
+});
+
+export async function initiatePayment(data: {
+  amount: number;
+  currency: string;
+  transaction_id: string;
+  description: string;
+  customer_name: string;
+  customer_surname: string;
+  customer_email: string;
+  customer_phone_number: string;
+  notify_url: string;
+  return_url: string;
+}) {
+  const response = await api.post('/payment', {
+    apikey: process.env.CINETPAY_API_KEY,
+    site_id: process.env.CINETPAY_SITE_ID,
+    ...data,
+    channels: 'ALL',
+    lang: 'fr',
+  });
+
+  return response.data;
+}

+ 13 - 0
notification_service/tsconfig.json

@@ -0,0 +1,13 @@
+{
+    "compilerOptions": {
+      "target": "es2020",
+      "module": "commonjs",
+      "outDir": "./dist",
+      "rootDir": "./src",
+      "strict": true,
+      "esModuleInterop": true,
+      "skipLibCheck": true
+    },
+    "include": ["src"]
+  }
+  

+ 3 - 0
payement_service/.env.dev

@@ -0,0 +1,3 @@
+CINETPAY_API_KEY=your_cinetpay_api_key
+CINETPAY_SITE_ID=your_site_id
+CINETPAY_BASE_URL=https://api-checkout.cinetpay.com/v2

+ 36 - 0
payement_service/Dockerfile

@@ -0,0 +1,36 @@
+# Étape 1 : build
+FROM node:18 AS builder
+
+# Définir le répertoire de travail
+WORKDIR /app
+
+# Copier les fichiers de dépendances
+COPY package*.json ./
+
+# Installer les dépendances
+RUN npm install
+
+# Copier le reste du code
+COPY . .
+
+# Compiler TypeScript
+RUN npm run build
+
+# Étape 2 : production
+FROM node:18-slim
+
+# Répertoire de travail
+WORKDIR /app
+
+RUN npm install axios dotenv
+
+# Copier uniquement les fichiers nécessaires au runtime
+COPY --from=builder /app/node_modules ./node_modules
+COPY --from=builder /app/dist ./dist
+COPY --from=builder /app/package*.json ./
+
+# Exposer le port (ex. : 4002)
+EXPOSE 4002
+
+# Commande de démarrage
+CMD ["node", "dist/index.js"]

+ 5 - 0
payement_service/package.json

@@ -0,0 +1,5 @@
+"scripts": {
+  "build": "tsc",
+  "start": "node dist/index.js",
+  "dev": "ts-node-dev --respawn --transpile-only src/index.ts"
+}

+ 41 - 0
payement_service/src/index.ts

@@ -0,0 +1,41 @@
+import Fastify from 'fastify';
+
+const server = Fastify({ logger: true });
+
+// Exemple d'endpoint pour initier un paiement
+server.post('/api/payments/initiate', async (request, reply) => {
+  const { amount, currency, userId, method } = request.body as {
+    amount: number;
+    currency: string;
+    userId: string;
+    method: 'mobile_money' | 'credit_card' | 'bank_transfer';
+  };
+
+  // Logique de validation de base
+  if (!amount || !currency || !userId || !method) {
+    return reply.status(400).send({ error: 'Missing required fields' });
+  }
+
+  // ⚠️ Exemple fictif – ici tu feras appel à ton fournisseur de paiement (ex: CinetPay, Orange Money, etc.)
+  const paymentId = `pay_${Date.now()}`;
+
+  server.log.info(`Paiement initié pour l'utilisateur ${userId} - Montant: ${amount} ${currency}`);
+
+  return reply.send({
+    status: 'initiated',
+    paymentId,
+    redirectUrl: `https://fake-gateway.com/pay/${paymentId}`, // URL de redirection fictive
+  });
+});
+
+const start = async () => {
+  try {
+    await server.listen({ port: 4002, host: '0.0.0.0' });
+    console.log('Service de paiement démarré sur le port 4002');
+  } catch (err) {
+    server.log.error(err);
+    process.exit(1);
+  }
+};
+
+start();