Skip to content

Audit Architecture - Auth Service

Résumé Exécutif

CritèreStatusNote
Interfaces définies⚠️ PartielCustomerHttpService implémente interface, mais pas pour Auth0
Injection de dépendances✅ BonUtilisation correcte de DI NestJS
Séparation des responsabilités❌ Non conformeLogique métier dans controllers
Clean Architecture Layers❌ AbsentPas de séparation domain/application/infrastructure
Use Cases❌ AbsentPas de use-cases, logique dans controllers/services
Domain Exceptions❌ AbsentUtilise HttpException de NestJS uniquement
Compensation (Saga)❌ AbsentPas de rollback pour workflows multi-services
Naming conventions⚠️ PartielServices HTTP mal nommés (devrait être adapters)

Score global : 35% - Structure basique fonctionnelle, mais non conforme à Clean Architecture.


Structure Actuelle vs Cible

Actuelle

src/
├── auth/
│   ├── auth.controller.ts          # ❌ 712 lignes, logique métier dedans
│   ├── auth0.service.ts             # ⚠️ Service Auth0 (devrait être adapter)
│   ├── customer-http.service.ts     # ⚠️ Adapter HTTP (mal nommé)
│   ├── ledger-wallet-http.service.ts # ⚠️ Adapter HTTP (mal nommé)
│   ├── otp.service.ts               # ✅ Service métier
│   ├── phone-auth.service.ts        # ✅ Service métier
│   └── dto/                         # ⚠️ DTOs mélangés
├── users/
│   ├── users.controller.ts          # ✅ OK
│   ├── users.service.ts             # ⚠️ Logique métier basique
│   ├── users.repository.ts         # ✅ Repository pattern OK
│   └── entities/
│       └── user.entity.ts          # ✅ ORM Entity OK
├── common/
│   ├── http/
│   │   ├── base-http.service.ts     # ✅ Base adapter OK
│   │   └── exceptions/             # ✅ OK
│   └── filters/                    # ✅ OK
└── app.module.ts

Cible (selon CLAUDE.md)

src/
├── core/
│   ├── config/
│   └── constants/
│       └── injection-tokens.ts
├── common/                         # Cross-cutting concerns
│   ├── decorators/
│   ├── filters/
│   ├── guards/
│   └── interceptors/
├── domain/                         # COUCHE DOMAIN (ABSENTE)
│   ├── entities/                   # Entités métier pures
│   │   ├── user.entity.ts
│   │   └── otp-code.entity.ts
│   ├── value-objects/
│   │   ├── email.vo.ts
│   │   ├── phone-number.vo.ts
│   │   └── pin.vo.ts
│   ├── exceptions/                 # Domain exceptions
│   │   ├── base.exception.ts
│   │   ├── invalid-credentials.exception.ts
│   │   ├── invalid-pin.exception.ts
│   │   ├── otp-expired.exception.ts
│   │   ├── user-not-found.exception.ts
│   │   └── email-already-exists.exception.ts
│   └── interfaces/
│       └── services/
│           ├── auth0.service.interface.ts
│           ├── customer.service.interface.ts
│           └── ledger.service.interface.ts
├── application/                    # COUCHE APPLICATION (ABSENTE)
│   ├── use-cases/
│   │   ├── auth/
│   │   │   ├── login.use-case.ts
│   │   │   ├── register.use-case.ts
│   │   │   ├── login-with-phone-pin.use-case.ts
│   │   │   ├── verify-otp.use-case.ts
│   │   │   ├── send-otp.use-case.ts
│   │   │   ├── refresh-token.use-case.ts
│   │   │   ├── logout.use-case.ts
│   │   │   ├── check-pin.use-case.ts
│   │   │   ├── change-pin.use-case.ts
│   │   │   └── reset-pin.use-case.ts
│   │   └── users/
│   │       ├── create-user.use-case.ts
│   │       └── update-user.use-case.ts
│   └── services/                   # Application services
│       ├── token-validation.service.ts
│       └── user-resolution.service.ts
├── infrastructure/                 # COUCHE INFRASTRUCTURE
│   ├── database/
│   │   ├── entities/               # ORM entities
│   │   │   ├── user.orm-entity.ts
│   │   │   └── otp-code.orm-entity.ts
│   │   └── repositories/
│   │       └── typeorm-user.repository.ts
│   └── adapters/
│       ├── auth0/
│       │   └── auth0.adapter.ts     # Auth0 adapter
│       └── http/
│           ├── customer-http.adapter.ts
│           └── ledger-http.adapter.ts
├── presentation/                   # COUCHE PRESENTATION
│   ├── controllers/
│   │   ├── auth.controller.ts       # Légers, délèguent aux use-cases
│   │   └── users.controller.ts
│   └── dto/
│       ├── requests/
│       └── responses/
└── modules/                        # Composition DI
    ├── auth.module.ts
    └── users.module.ts

Analyse Détaillée

1. Points Positifs ✅

1.1 Repository Pattern bien implémenté

typescript
// users/users.repository.ts
@Injectable()
export class UsersRepository extends Repository<User> {
  async findById(id: string): Promise<User | null> { ... }
  async findByAuth0Id(auth0Id: string): Promise<User | null> { ... }
}

Verdict : Bon pattern, séparation DB/Service.

1.2 Base HTTP Service pour adapters

typescript
// common/http/base-http.service.ts
export abstract class BaseHttpService {
  protected async post<T>(...): Promise<ServiceResponse<T>> { ... }
}

Verdict : Bonne base pour les adapters HTTP.

1.3 Guards et Decorators pour RBAC

  • RolesGuard, PermissionsGuard
  • @Roles(), @Permissions() decorators Verdict : Bonne implémentation de la sécurité.

2. Points Critiques ❌

2.1 Controller avec logique métier (712 lignes !)

Problème : auth.controller.ts contient toute la logique métier :

  • Décodage de tokens JWT
  • Résolution d'utilisateurs (multi-stratégies)
  • Vérification de PIN
  • Gestion OTP
  • Logique de reset PIN
typescript
// ❌ MAUVAIS : Logique métier dans controller
@Post('check-pin')
async checkPin(@Headers('authorization') authorization: string, @Body() checkPinDto: CheckPinDto) {
  // Décodage JWT
  const jwt = require('jsonwebtoken');
  const decoded = jwt.decode(token) as any;
  
  // Résolution utilisateur (4 stratégies différentes !)
  let localUser = await this.auth0Service['usersService'].findByAuth0Id(decoded.sub);
  if (!localUser && decoded.person_id) {
    localUser = await this.auth0Service['usersService']['userRepository'].findOne({...});
  }
  // ... 3 autres stratégies
  
  // Vérification PIN
  const isPinValid = await this.auth0Service['usersService'].verifyPin(localUser, checkPinDto.pin);
  // ...
}

Impact :

  • Controller difficile à tester
  • Logique métier non réutilisable
  • Violation du principe Single Responsibility
  • Impossible de réutiliser la logique ailleurs

Solution : Extraire dans des Use Cases

typescript
// ✅ BON : Use Case
@Injectable()
export class CheckPinUseCase {
  constructor(
    @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
    @Inject(AUTH0_SERVICE) private readonly auth0Service: IAuth0Service,
  ) {}

  async execute(input: CheckPinInput): Promise<CheckPinResult> {
    const user = await this.resolveUser(input.token);
    const isValid = await this.auth0Service.verifyPin(user, input.pin);
    if (!isValid) {
      throw new InvalidPinException();
    }
    return { verified: true };
  }

  private async resolveUser(token: string): Promise<User> {
    // Logique de résolution centralisée
  }
}

2.2 Pas de couche Domain

Problème : Aucune entité de domaine, value objects, ou exceptions métier.

Impact :

  • Validation métier dispersée
  • Pas de richesse comportementale
  • Exceptions génériques (HttpException) au lieu d'exceptions métier

Solution : Créer la couche domain

typescript
// domain/exceptions/invalid-pin.exception.ts
export class InvalidPinException extends DomainException {
  readonly code = 'INVALID_PIN';
  readonly statusCode = 401;
  constructor() {
    super('PIN invalide');
  }
}

// domain/value-objects/pin.vo.ts
export class Pin {
  private constructor(private readonly value: string) {
    if (!/^\d{4,6}$/.test(value)) {
      throw new InvalidPinFormatException();
    }
  }

  static create(value: string): Pin {
    return new Pin(value);
  }

  async verify(hashedPin: string): Promise<boolean> {
    return bcrypt.compare(this.value, hashedPin);
  }
}

2.3 Services HTTP mal nommés et mal positionnés

Problème :

  • customer-http.service.ts dans auth/ au lieu de infrastructure/adapters/http/
  • Devrait s'appeler customer-http.adapter.ts
  • auth0.service.ts devrait être auth0.adapter.ts dans infrastructure/adapters/auth0/

Solution :

auth/customer-http.service.ts
→ infrastructure/adapters/http/customer-http.adapter.ts

auth/auth0.service.ts
→ infrastructure/adapters/auth0/auth0.adapter.ts

2.4 Pas de Use Cases

Problème : Toute la logique est dans les controllers et services.

Workflows à extraire en Use Cases :

  1. LoginUseCase - Login avec username/password
  2. LoginWithPhonePinUseCase - Login avec téléphone + PIN
  3. RegisterUseCase - Inscription complète (Auth0 + Customer + Ledger)
  4. SendOtpUseCase - Envoi OTP
  5. VerifyOtpUseCase - Vérification OTP
  6. CheckPinUseCase - Vérification PIN
  7. ChangePinUseCase - Changement PIN
  8. ResetPinUseCase - Reset PIN (workflow multi-étapes)
  9. RefreshTokenUseCase - Refresh token
  10. LogoutUseCase - Logout

2.5 Pas de compensation pour workflows multi-services

Problème : Le workflow register crée :

  1. User dans Auth0
  2. User local
  3. Person dans Customer Service
  4. Wallet dans Ledger Service

Si une étape échoue, pas de rollback.

Exemple actuel :

typescript
// auth0.service.ts - registerUser()
async registerUser(registerDto: ExtendedRegisterDto) {
  // 1. Créer Auth0 user
  const auth0User = await this.createAuth0User(...);
  
  // 2. Créer local user
  const localUser = await this.usersService.create(...);
  
  // 3. Créer person dans Customer Service
  const person = await this.customerHttpService.registerUser(...);
  
  // 4. Créer wallet dans Ledger Service
  const wallet = await this.ledgerWalletHttpService.initiateWallet(...);
  
  // ❌ Si étape 4 échoue, pas de rollback des étapes 1-3
}

Solution : Implémenter Saga Pattern

typescript
@Injectable()
export class RegisterUseCase {
  async execute(input: RegisterInput): Promise<RegisterResult> {
    let auth0User = null;
    let localUser = null;
    let person = null;
    let wallet = null;

    try {
      auth0User = await this.auth0Service.createUser(...);
      localUser = await this.userRepo.save(...);
      person = await this.customerService.registerUser(...);
      wallet = await this.ledgerService.initiateWallet(...);
      
      return { success: true, data: { ... } };
    } catch (error) {
      // Compensation
      if (wallet) await this.ledgerService.deleteWallet(wallet.id);
      if (person) await this.customerService.deletePerson(person.id);
      if (localUser) await this.userRepo.delete(localUser.id);
      if (auth0User) await this.auth0Service.deleteUser(auth0User.id);
      
      throw error;
    }
  }
}

2.6 Accès direct aux propriétés privées

Problème : Le controller accède aux propriétés privées des services :

typescript
// auth.controller.ts
const localUser = await this.auth0Service['usersService'].findByAuth0Id(decoded.sub);
const user = await this.auth0Service['usersService']['userRepository'].findOne({...});

Impact : Violation de l'encapsulation, code fragile.

Solution : Exposer des méthodes publiques ou utiliser des Use Cases.

2.7 DTOs mal organisés

Problème : DTOs mélangés dans auth/dto/ sans séparation request/response.

Solution :

presentation/dto/requests/
  ├── login.dto.ts
  ├── register.dto.ts
  └── check-pin.dto.ts

presentation/dto/responses/
  ├── login-response.dto.ts
  ├── register-response.dto.ts
  └── check-pin-response.dto.ts

application/dto/internal/
  └── register-internal.dto.ts

3. Points à Améliorer ⚠️

3.1 Services métier mélangés avec adapters

Problème : OtpService et PhoneAuthService sont des services métier, mais dans le même dossier que les adapters.

Solution : Déplacer vers application/services/ ou domain/services/.

3.2 Entity ORM utilisée comme entité domaine

Problème : User entity est une ORM entity, pas une entité domaine.

Solution : Séparer

  • domain/entities/user.entity.ts - Entité métier pure
  • infrastructure/database/entities/user.orm-entity.ts - ORM entity
  • Mapper entre les deux

3.3 Pas d'injection tokens centralisés

Problème : Pas de fichier core/constants/injection-tokens.ts.

Solution : Créer et utiliser des tokens pour tous les services externes.


Plan de Refactoring

Phase 1 : Créer la couche Domain (P1)

  1. Créer domain/exceptions/ avec toutes les exceptions métier
  2. Créer domain/value-objects/ (Email, PhoneNumber, Pin)
  3. Créer domain/interfaces/services/ pour les interfaces
  4. Créer domain/entities/ avec entités métier pures

Phase 2 : Extraire les Use Cases (P0)

  1. Créer application/use-cases/auth/
  2. Extraire chaque endpoint du controller vers un use-case
  3. Implémenter compensation pour RegisterUseCase

Phase 3 : Restructurer Infrastructure (P2)

  1. Déplacer auth0.service.tsinfrastructure/adapters/auth0/auth0.adapter.ts
  2. Déplacer customer-http.service.tsinfrastructure/adapters/http/customer-http.adapter.ts
  3. Renommer en .adapter.ts
  4. Créer interfaces dans domain/interfaces/services/

Phase 4 : Réorganiser DTOs (P2)

  1. Créer presentation/dto/requests/ et responses/
  2. Déplacer et séparer les DTOs
  3. Créer DTOs internes dans application/dto/

Phase 5 : Refactoriser Controllers (P1)

  1. Simplifier les controllers (max 50 lignes)
  2. Déléguer aux use-cases
  3. Utiliser les domain exceptions

Checklist de Conformité

RègleActuelAction
Use Cases dans application/use-cases/Créer 10+ use-cases
Domain ExceptionsCréer 8+ exceptions
Adapters dans infrastructure/adapters/Déplacer + renommer
Controllers légers (< 50 lignes)Refactoriser
DTOs séparés (requests/responses)Réorganiser
Value ObjectsCréer (Email, Pin, PhoneNumber)
Compensation (Saga)Implémenter pour Register
Injection tokens centralisésCréer core/constants/injection-tokens.ts
Entités domaine séparées des ORMCréer entités domaine + mapper
Pas d'accès aux propriétés privéesCorriger

Priorités Recommandées

  1. P0 - Critique : Extraire Use Cases du controller (surtout Register avec compensation)
  2. P1 - Important : Créer domain exceptions + les utiliser
  3. P1 - Important : Implémenter compensation pour RegisterUseCase
  4. P2 - Moyen : Restructurer les dossiers selon Clean Architecture
  5. P2 - Moyen : Créer value objects (Pin, Email, PhoneNumber)
  6. P3 - Low : Renommer les fichiers selon conventions

Exemple de Refactoring : CheckPin

Avant (Controller - 80 lignes)

typescript
@Post('check-pin')
async checkPin(@Headers('authorization') authorization: string, @Body() checkPinDto: CheckPinDto) {
  // 80 lignes de logique métier...
}

Après (Use Case + Controller léger)

typescript
// application/use-cases/auth/check-pin.use-case.ts
@Injectable()
export class CheckPinUseCase {
  constructor(
    @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
    @Inject(AUTH0_SERVICE) private readonly auth0Service: IAuth0Service,
    private readonly tokenService: TokenValidationService,
    private readonly userResolutionService: UserResolutionService,
  ) {}

  async execute(input: CheckPinInput): Promise<CheckPinResult> {
    const user = await this.userResolutionService.resolveFromToken(input.token);
    const pin = Pin.create(input.pin);
    
    const isValid = await this.auth0Service.verifyPin(user, pin);
    if (!isValid) {
      throw new InvalidPinException();
    }

    return { verified: true };
  }
}

// presentation/controllers/auth.controller.ts
@Post('check-pin')
async checkPin(
  @Headers('authorization') authorization: string,
  @Body() dto: CheckPinDto,
) {
  const result = await this.checkPinUseCase.execute({
    token: authorization.replace('Bearer ', ''),
    pin: dto.pin,
  });
  return result;
}

Audit réalisé le 22/01/2026

NxPay — Plateforme fintech CEMAC