Skip to content

Plan de Migration : Ajout du champ Phone dans Auth Service

Contexte et Problématique

Situation Actuelle

Le service Auth dépend actuellement du service Customer pour le login phone-PIN :

typescript
// auth/auth0.service.ts - loginWithPhonePin()
// 1. Trouve le user via Customer Service
const personResult = await this.customerHttpService.getPersonByPhone(countryCode, phoneNumber);
// 2. Cherche dans Auth0 avec person_id
const searchResult = await this.managementClient.users.list({
  q: `user_metadata.person_id:"${person.id}"`,
});

Problèmes identifiés :

  • Dépendance forte : Auth ne peut pas fonctionner si Customer Service est down
  • Violation de l'architecture : Auth appelle directement Customer (devrait passer par Orchestrator)
  • Performance : 2 appels HTTP (Customer + Auth0) pour un simple login
  • Faille de sécurité : Si Customer est compromis, l'authentification est compromise

Objectif

Rendre le service Auth autonome pour le login phone-PIN en ajoutant les champs phone_number et country_code dans auth.users.


Analyse de l'Impact

Fichiers à Modifier

1. Base de Données

  • ✅ Migration SQL : tools/database/migrations/auth/010-add-phone-to-users.sql
  • ✅ Migration TypeORM : services/auth/src/migrations/[timestamp]-AddPhoneToUsers.js
  • ✅ Entity : services/auth/src/users/entities/user.entity.ts

2. Code Application

  • ✅ Repository : services/auth/src/users/users.repository.ts
  • ✅ Service : services/auth/src/users/users.service.ts
  • ✅ Auth0Service : services/auth/src/auth/auth0.service.ts (loginWithPhonePin)
  • ✅ DTOs : services/auth/src/auth/dto/extended-register.dto.ts
  • ✅ OTP Service : services/auth/src/auth/otp.service.ts (vérifications phone)

3. Workflows Impactés

  • Register : Doit sauvegarder phone dans auth.users
  • Login Phone-PIN : Doit utiliser auth.users au lieu de Customer
  • Reset PIN : Doit utiliser auth.users au lieu de Customer
  • Send OTP : Peut vérifier dans auth.users
  • Verify OTP : Peut vérifier dans auth.users

Plan de Migration Détaillé

Phase 1 : Préparation de la Base de Données

1.1 Créer la Migration SQL

Fichier : tools/database/migrations/auth/010-add-phone-to-users.sql

sql
-- Migration: Add Phone Fields to Auth Users
-- Description: Adds phone_number and country_code to auth.users for autonomous phone-PIN login
-- Date: 2026-01-22
-- Author: NXPay Team

-- =====================================================
-- Add phone fields to auth.users
-- =====================================================

ALTER TABLE auth.users
ADD COLUMN IF NOT EXISTS phone_number VARCHAR(20),
ADD COLUMN IF NOT EXISTS country_code VARCHAR(5);

-- Create unique index on phone (country_code + phone_number)
CREATE UNIQUE INDEX IF NOT EXISTS idx_auth_users_phone_unique 
ON auth.users(country_code, phone_number) 
WHERE phone_number IS NOT NULL AND country_code IS NOT NULL;

-- Create index for phone lookups
CREATE INDEX IF NOT EXISTS idx_auth_users_phone 
ON auth.users(country_code, phone_number) 
WHERE phone_number IS NOT NULL;

-- Add comments
COMMENT ON COLUMN auth.users.phone_number IS 'User phone number for phone-PIN authentication';
COMMENT ON COLUMN auth.users.country_code IS 'Country code (e.g., +242) for phone number';

-- Log confirmation
DO $$
BEGIN
    RAISE NOTICE 'Phone fields added to auth.users successfully';
END $$;

1.2 Créer la Migration TypeORM

Fichier : services/auth/src/migrations/[timestamp]-AddPhoneToUsers.js

javascript
/**
 * Migration: Add Phone Fields to Auth Users
 * Adds phone_number and country_code columns to auth.users table
 */

module.exports = class AddPhoneToUsers[timestamp] {
    async up(queryRunner) {
        // Add columns
        await queryRunner.query(`
            ALTER TABLE "auth"."users"
            ADD COLUMN IF NOT EXISTS "phone_number" VARCHAR(20),
            ADD COLUMN IF NOT EXISTS "country_code" VARCHAR(5)
        `);

        // Create unique index
        await queryRunner.query(`
            CREATE UNIQUE INDEX IF NOT EXISTS "IDX_auth_users_phone_unique"
            ON "auth"."users"("country_code", "phone_number")
            WHERE "phone_number" IS NOT NULL AND "country_code" IS NOT NULL
        `);

        // Create lookup index
        await queryRunner.query(`
            CREATE INDEX IF NOT EXISTS "IDX_auth_users_phone"
            ON "auth"."users"("country_code", "phone_number")
            WHERE "phone_number" IS NOT NULL
        `);
    }

    async down(queryRunner) {
        // Drop indexes
        await queryRunner.query(`DROP INDEX IF EXISTS "auth"."IDX_auth_users_phone"`);
        await queryRunner.query(`DROP INDEX IF EXISTS "auth"."IDX_auth_users_phone_unique"`);
        
        // Drop columns
        await queryRunner.query(`
            ALTER TABLE "auth"."users"
            DROP COLUMN IF EXISTS "phone_number",
            DROP COLUMN IF EXISTS "country_code"
        `);
    }
}

1.3 Script de Migration des Données Existantes

Fichier : tools/database/migrations/auth/011-migrate-phone-data-from-customer.sql

sql
-- Migration: Migrate Phone Data from Customer to Auth
-- Description: Populates auth.users.phone_number and country_code from customer.phones
-- Date: 2026-01-22
-- Author: NXPay Team

-- =====================================================
-- Migrate phone data from customer.phones to auth.users
-- =====================================================

-- Update auth.users with phone data from customer.phones
-- Only update users that have a person_id and don't already have a phone
UPDATE auth.users u
SET 
    phone_number = p.number,
    country_code = p.country_code
FROM customer.phones p
WHERE 
    u.person_id = p.person_id
    AND u.phone_number IS NULL
    AND p.is_primary = true  -- Only use primary phone
    AND EXISTS (
        SELECT 1 FROM customer.people pe 
        WHERE pe.id = p.person_id
    );

-- Log results
DO $$
DECLARE
    updated_count INTEGER;
BEGIN
    SELECT COUNT(*) INTO updated_count
    FROM auth.users
    WHERE phone_number IS NOT NULL AND country_code IS NOT NULL;
    
    RAISE NOTICE 'Migrated phone data for % users', updated_count;
END $$;

Note : Cette migration nécessite que Customer Service soit accessible. Si ce n'est pas le cas, on peut créer un script séparé qui sera exécuté manuellement.


Phase 2 : Mise à Jour de l'Entity et Repository

2.1 Mettre à jour l'Entity User

Fichier : services/auth/src/users/entities/user.entity.ts

typescript
@Entity('users', { schema: 'auth' })
@Index(['username'], { unique: true })
@Index(['email'], { unique: true })
@Index(['auth0Id'], { unique: true })
@Index(['countryCode', 'phoneNumber'], { unique: true, where: 'phone_number IS NOT NULL' }) // Nouveau
export class User {
  // ... champs existants ...

  @Column({ name: 'phone_number', length: 20, nullable: true })
  phoneNumber?: string;

  @Column({ name: 'country_code', length: 5, nullable: true })
  countryCode?: string;

  // ... reste de l'entity ...
}

2.2 Ajouter Méthodes au Repository

Fichier : services/auth/src/users/users.repository.ts

typescript
export class UsersRepository extends Repository<User> {
  // ... méthodes existantes ...

  /**
   * Trouve un utilisateur par téléphone
   */
  async findByPhone(countryCode: string, phoneNumber: string): Promise<User | null> {
    return this.findOne({
      where: { countryCode, phoneNumber },
    });
  }

  /**
   * Vérifie si un téléphone existe déjà
   */
  async existsByPhone(countryCode: string, phoneNumber: string): Promise<User | null> {
    return this.findOne({
      where: { countryCode, phoneNumber },
    });
  }
}

Phase 3 : Mise à Jour du Workflow Register

3.1 Modifier Auth0Service.executeRegistration()

Fichier : services/auth/src/auth/auth0.service.ts

typescript
private async executeRegistration(data: BaseRegistrationData) {
  // ... étapes existantes ...

  // ÉTAPE 2: Créer l'utilisateur local
  let localUser: { id: string } | null = null;
  try {
    localUser = await this.usersService.create({
      username: data.username,
      email: data.email,
      password: data.password,
      pin: data.pin,
      auth0Id: userData.user_id,
      status: UserStatus.PENDING,
      // ✅ NOUVEAU : Ajouter phone si fourni
      phoneNumber: data.phoneInfo?.number,
      countryCode: data.phoneInfo?.countryCode,
    });
    // ... reste ...
  }
}

3.2 Mettre à jour UsersService.create()

Fichier : services/auth/src/users/users.service.ts

typescript
async create(createUserDto: CreateUserDto): Promise<UserResponseDto> {
  // Vérifier si l'utilisateur existe déjà (username, email, OU phone)
  const existingUser = await this.usersRepository.existsByUsernameOrEmail(
    createUserDto.username,
    createUserDto.email,
  );

  if (existingUser) {
    throw new ConflictException('Username or email already exists');
  }

  // ✅ NOUVEAU : Vérifier si le téléphone existe déjà
  if (createUserDto.phoneNumber && createUserDto.countryCode) {
    const existingPhone = await this.usersRepository.existsByPhone(
      createUserDto.countryCode,
      createUserDto.phoneNumber,
    );
    if (existingPhone) {
      throw new ConflictException('Phone number already exists');
    }
  }

  // ... reste de la création ...
}

3.3 Mettre à jour CreateUserDto

Fichier : services/auth/src/users/dto/create-user.dto.ts

typescript
export class CreateUserDto {
  // ... champs existants ...

  @IsOptional()
  @IsString()
  @Length(1, 20)
  phoneNumber?: string;

  @IsOptional()
  @IsString()
  @Length(1, 5)
  @Matches(/^\+\d{1,4}$/)
  countryCode?: string;
}

Phase 4 : Refactoriser Login Phone-PIN

4.1 Nouvelle Implémentation (Autonome)

Fichier : services/auth/src/auth/auth0.service.ts

typescript
async loginWithPhonePin(phonePinLoginDto: PhonePinLoginDto) {
  try {
    const { phoneNumber, countryCode, pin } = phonePinLoginDto;
    const fullPhoneNumber = `${countryCode}${phoneNumber}`;

    // ✅ NOUVEAU : Trouver directement dans auth.users (plus besoin de Customer)
    const localUser = await this.usersService.findByPhone(countryCode, phoneNumber);

    if (!localUser) {
      this.logger.error(`User not found for phone: ${fullPhoneNumber}`);
      return {
        success: false,
        error: 'Invalid phone number or PIN',
      };
    }

    if (!localUser.auth0Id) {
      this.logger.error(`No Auth0 ID for user: ${localUser.id}`);
      return {
        success: false,
        error: 'User account not properly configured',
      };
    }

    // 2. Get Auth0 user
    const auth0User = await this.managementClient.users.get({ id: localUser.auth0Id });

    // 3. Get hashed PIN from Auth0 user_metadata
    const userMetadata = auth0User.data?.user_metadata as any;
    const storedHashedPin = userMetadata?.mobile_pin;

    if (!storedHashedPin || typeof storedHashedPin !== 'string') {
      this.logger.error(`No PIN configured for Auth0 user: ${localUser.auth0Id}`);
      return {
        success: false,
        error: 'PIN not configured for this user',
      };
    }

    // 4. Verify PIN
    const isPinValid = await bcrypt.compare(pin, storedHashedPin);

    if (!isPinValid) {
      this.logger.error(`Invalid PIN for phone: ${fullPhoneNumber}`);
      return {
        success: false,
        error: 'Invalid phone number or PIN',
      };
    }

    // 5. Generate tokens
    const tokens = await this.generateAuthTokens(
      localUser.auth0Id,
      localUser.id,
      localUser.email,
      localUser.personId || '',
    );

    return {
      success: true,
      data: tokens,
    };
  } catch (error: any) {
    this.logger.error(`Phone-PIN authentication failed:`, error?.message || error);
    return {
      success: false,
      error: 'Authentication failed',
    };
  }
}

4.2 Ajouter Méthode dans UsersService

Fichier : services/auth/src/users/users.service.ts

typescript
async findByPhone(countryCode: string, phoneNumber: string): Promise<User | null> {
  return await this.usersRepository.findByPhone(countryCode, phoneNumber);
}

Phase 5 : Mettre à Jour Reset PIN

5.1 Refactoriser confirmResetPin()

Fichier : services/auth/src/auth/auth.controller.ts

typescript
@Post('reset-pin/confirm')
async confirmResetPin(@Body() resetPinDto: ResetPinDto) {
  try {
    // 1. Vérifier OTP
    const otpResult = await this.otpService.verifyResetPinOtp(...);

    // ✅ NOUVEAU : Trouver directement dans auth.users (plus besoin de Customer)
    const user = await this.auth0Service['usersService'].findByPhone(
      resetPinDto.countryCode,
      resetPinDto.phoneNumber,
    );

    if (!user) {
      throw new HttpException('User not found', HttpStatus.NOT_FOUND);
    }

    if (!user.auth0Id) {
      throw new HttpException('User account not properly configured', HttpStatus.INTERNAL_SERVER_ERROR);
    }

    // ... reste du workflow (update PIN dans Auth0 et local DB) ...
  }
}

Phase 6 : Mettre à Jour OTP Service (Optionnel)

6.1 Vérification Phone dans SendOtp

Fichier : services/auth/src/auth/otp.service.ts

typescript
async sendOtp(phoneNumber: string, countryCode: string, type: 'SMS' | 'WHATSAPP', purpose: 'registration' | 'reset_pin' = 'registration'): Promise<OtpResult> {
  // ✅ NOUVEAU : Vérifier dans auth.users au lieu de Customer
  if (purpose === 'registration') {
    const existingUser = await this.usersService.findByPhone(countryCode, phoneNumber);
    if (existingUser) {
      return {
        success: false,
        error: 'Phone number already registered',
      };
    }
  } else if (purpose === 'reset_pin') {
    const existingUser = await this.usersService.findByPhone(countryCode, phoneNumber);
    if (!existingUser) {
      return {
        success: false,
        error: 'Phone number not found',
      };
    }
  }

  // ... reste de l'envoi OTP ...
}

Note : On peut garder la vérification Customer pour compatibilité, mais ce n'est plus nécessaire.


Phase 7 : Nettoyage et Suppression des Dépendances

7.1 Supprimer les Appels à Customer dans Auth

Fichiers à modifier :

  • auth0.service.ts : Supprimer customerHttpService.getPersonByPhone() dans loginWithPhonePin()
  • auth.controller.ts : Supprimer customerHttpService.getPersonByPhone() dans confirmResetPin()
  • otp.service.ts : Remplacer les vérifications Customer par auth.users

7.2 Supprimer CustomerHttpService du AuthModule (Optionnel)

Fichier : services/auth/src/auth/auth.module.ts

typescript
@Module({
  // ... autres imports ...
  providers: [
    Auth0Service,
    OtpService,
    PhoneAuthService,
    // ❌ SUPPRIMER : CustomerHttpService,
    // ❌ SUPPRIMER : LedgerWalletHttpService, (si plus utilisé)
  ],
  // ... reste ...
})

Note : On peut garder CustomerHttpService si d'autres parties du code l'utilisent encore (ex: users.controller.ts pour les merchants).


Ordre d'Exécution

Séquence de Migration

  1. Phase 1 : Créer les migrations SQL et TypeORM
  2. Phase 2 : Mettre à jour Entity et Repository
  3. Phase 3 : Mettre à jour le workflow Register (pour nouveaux users)
  4. Phase 4 : Refactoriser Login Phone-PIN
  5. Phase 5 : Mettre à jour Reset PIN
  6. Phase 6 : Mettre à jour OTP Service (optionnel)
  7. Phase 7 : Nettoyer les dépendances

Migration des Données Existantes

Option A : Migration Automatique (Recommandée)

  • Exécuter 011-migrate-phone-data-from-customer.sql après la migration de schéma
  • Nécessite que Customer Service soit accessible

Option B : Migration Manuelle

  • Script séparé qui sera exécuté manuellement
  • Utile si Customer Service n'est pas accessible au moment de la migration

Option C : Migration Progressive

  • Les nouveaux users auront le phone dans auth.users
  • Les anciens users seront migrés progressivement lors de leur prochaine connexion
  • Script de backfill pour les users inactifs

Tests à Effectuer

Tests Unitaires

  1. UsersRepository.findByPhone() retourne le bon user
  2. UsersRepository.existsByPhone() détecte les doublons
  3. UsersService.create() valide l'unicité du phone
  4. Auth0Service.loginWithPhonePin() fonctionne sans Customer

Tests d'Intégration

  1. ✅ Register avec phone → phone sauvegardé dans auth.users
  2. ✅ Login phone-PIN → fonctionne avec auth.users uniquement
  3. ✅ Reset PIN → fonctionne avec auth.users uniquement
  4. ✅ Doublon phone → erreur lors de la création

Tests de Migration

  1. ✅ Migration SQL s'exécute sans erreur
  2. ✅ Données existantes migrées correctement
  3. ✅ Index créés et fonctionnels
  4. ✅ Rollback fonctionne

Risques et Mitigation

Risque 1 : Données Incomplètes

Problème : Certains users n'ont pas de phone dans Customer.

Mitigation :

  • Migration ne met à jour que les users avec phone dans Customer
  • Les users sans phone ne pourront pas utiliser phone-PIN login (comportement attendu)

Risque 2 : Doublons de Phone

Problème : Plusieurs users avec le même phone dans Customer.

Mitigation :

  • Index unique sur (country_code, phone_number)
  • Vérification lors de la migration
  • Script de détection des doublons avant migration

Risque 3 : Incohérence entre Auth et Customer

Problème : Phone dans auth.users différent de Customer.phones.

Mitigation :

  • Auth.users devient la source de vérité pour l'authentification
  • Customer.phones reste la source de vérité pour les données métier
  • Synchronisation optionnelle via événements (future amélioration)

Risque 4 : Performance

Problème : Index supplémentaire sur phone.

Mitigation :

  • Index partiel (WHERE phone_number IS NOT NULL) pour optimiser
  • Index unique pour garantir l'unicité
  • Monitoring des performances après migration

Bénéfices Attendus

✅ Autonomie du Service Auth

  • Auth ne dépend plus de Customer Service pour le login
  • Disponibilité améliorée (pas de single point of failure)

✅ Performance

  • 1 requête DB au lieu de 2 appels HTTP (Customer + Auth0)
  • Réduction de la latence du login phone-PIN

✅ Sécurité

  • Moins de surface d'attaque (pas d'appel externe)
  • Données d'authentification centralisées dans Auth

✅ Conformité Architecture

  • Auth se concentre uniquement sur l'authentification
  • Préparation pour migration vers Orchestrator (Register reste multi-services)

Checklist de Migration

Pré-Migration

  • [ ] Backup de la base de données
  • [ ] Vérifier que Customer Service est accessible (pour migration données)
  • [ ] Identifier les users sans phone dans Customer
  • [ ] Détecter les doublons de phone dans Customer

Migration

  • [ ] Exécuter migration SQL 010-add-phone-to-users.sql
  • [ ] Exécuter migration TypeORM
  • [ ] Mettre à jour Entity et Repository
  • [ ] Mettre à jour code application (Register, Login, Reset PIN)
  • [ ] Exécuter migration données 011-migrate-phone-data-from-customer.sql
  • [ ] Vérifier les données migrées

Post-Migration

  • [ ] Tester Register avec phone
  • [ ] Tester Login phone-PIN
  • [ ] Tester Reset PIN
  • [ ] Vérifier les performances
  • [ ] Supprimer dépendances Customer (si possible)
  • [ ] Mettre à jour la documentation

Prochaines Étapes (Post-Migration)

  1. Migrer Register vers Orchestrator : Maintenant que Auth est autonome, on peut déplacer Register vers Orchestrator
  2. Synchronisation Phone : Si phone change dans Customer, synchroniser avec Auth (via événements)
  3. Support Multi-Phone : Si besoin, ajouter support pour plusieurs phones par user

Plan créé le 22/01/2026

NxPay — Plateforme fintech CEMAC