Audit Architecture - Auth Service
Résumé Exécutif
| Critère | Status | Note |
|---|---|---|
| Interfaces définies | ⚠️ Partiel | CustomerHttpService implémente interface, mais pas pour Auth0 |
| Injection de dépendances | ✅ Bon | Utilisation correcte de DI NestJS |
| Séparation des responsabilités | ❌ Non conforme | Logique métier dans controllers |
| Clean Architecture Layers | ❌ Absent | Pas de séparation domain/application/infrastructure |
| Use Cases | ❌ Absent | Pas de use-cases, logique dans controllers/services |
| Domain Exceptions | ❌ Absent | Utilise HttpException de NestJS uniquement |
| Compensation (Saga) | ❌ Absent | Pas de rollback pour workflows multi-services |
| Naming conventions | ⚠️ Partiel | Services 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.tsCible (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.tsAnalyse Détaillée
1. Points Positifs ✅
1.1 Repository Pattern bien implémenté
// 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
// 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
// ❌ 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
// ✅ 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
// 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.tsdansauth/au lieu deinfrastructure/adapters/http/- Devrait s'appeler
customer-http.adapter.ts auth0.service.tsdevrait êtreauth0.adapter.tsdansinfrastructure/adapters/auth0/
Solution :
auth/customer-http.service.ts
→ infrastructure/adapters/http/customer-http.adapter.ts
auth/auth0.service.ts
→ infrastructure/adapters/auth0/auth0.adapter.ts2.4 Pas de Use Cases
Problème : Toute la logique est dans les controllers et services.
Workflows à extraire en Use Cases :
LoginUseCase- Login avec username/passwordLoginWithPhonePinUseCase- Login avec téléphone + PINRegisterUseCase- Inscription complète (Auth0 + Customer + Ledger)SendOtpUseCase- Envoi OTPVerifyOtpUseCase- Vérification OTPCheckPinUseCase- Vérification PINChangePinUseCase- Changement PINResetPinUseCase- Reset PIN (workflow multi-étapes)RefreshTokenUseCase- Refresh tokenLogoutUseCase- Logout
2.5 Pas de compensation pour workflows multi-services
Problème : Le workflow register crée :
- User dans Auth0
- User local
- Person dans Customer Service
- Wallet dans Ledger Service
Si une étape échoue, pas de rollback.
Exemple actuel :
// 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
@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 :
// 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.ts3. 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 pureinfrastructure/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)
- Créer
domain/exceptions/avec toutes les exceptions métier - Créer
domain/value-objects/(Email, PhoneNumber, Pin) - Créer
domain/interfaces/services/pour les interfaces - Créer
domain/entities/avec entités métier pures
Phase 2 : Extraire les Use Cases (P0)
- Créer
application/use-cases/auth/ - Extraire chaque endpoint du controller vers un use-case
- Implémenter compensation pour
RegisterUseCase
Phase 3 : Restructurer Infrastructure (P2)
- Déplacer
auth0.service.ts→infrastructure/adapters/auth0/auth0.adapter.ts - Déplacer
customer-http.service.ts→infrastructure/adapters/http/customer-http.adapter.ts - Renommer en
.adapter.ts - Créer interfaces dans
domain/interfaces/services/
Phase 4 : Réorganiser DTOs (P2)
- Créer
presentation/dto/requests/etresponses/ - Déplacer et séparer les DTOs
- Créer DTOs internes dans
application/dto/
Phase 5 : Refactoriser Controllers (P1)
- Simplifier les controllers (max 50 lignes)
- Déléguer aux use-cases
- Utiliser les domain exceptions
Checklist de Conformité
| Règle | Actuel | Action |
|---|---|---|
Use Cases dans application/use-cases/ | ❌ | Créer 10+ use-cases |
| Domain Exceptions | ❌ | Cré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 Objects | ❌ | Créer (Email, Pin, PhoneNumber) |
| Compensation (Saga) | ❌ | Implémenter pour Register |
| Injection tokens centralisés | ❌ | Créer core/constants/injection-tokens.ts |
| Entités domaine séparées des ORM | ❌ | Créer entités domaine + mapper |
| Pas d'accès aux propriétés privées | ❌ | Corriger |
Priorités Recommandées
- P0 - Critique : Extraire Use Cases du controller (surtout Register avec compensation)
- P1 - Important : Créer domain exceptions + les utiliser
- P1 - Important : Implémenter compensation pour RegisterUseCase
- P2 - Moyen : Restructurer les dossiers selon Clean Architecture
- P2 - Moyen : Créer value objects (Pin, Email, PhoneNumber)
- P3 - Low : Renommer les fichiers selon conventions
Exemple de Refactoring : CheckPin
Avant (Controller - 80 lignes)
@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)
// 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