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 :
// 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
-- 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
/**
* 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
-- 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
@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
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
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
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
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
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
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
@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
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: SupprimercustomerHttpService.getPersonByPhone()dansloginWithPhonePin() - ✅
auth.controller.ts: SupprimercustomerHttpService.getPersonByPhone()dansconfirmResetPin() - ✅
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
@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
- Phase 1 : Créer les migrations SQL et TypeORM
- Phase 2 : Mettre à jour Entity et Repository
- Phase 3 : Mettre à jour le workflow Register (pour nouveaux users)
- Phase 4 : Refactoriser Login Phone-PIN
- Phase 5 : Mettre à jour Reset PIN
- Phase 6 : Mettre à jour OTP Service (optionnel)
- 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.sqlaprè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
- ✅
UsersRepository.findByPhone()retourne le bon user - ✅
UsersRepository.existsByPhone()détecte les doublons - ✅
UsersService.create()valide l'unicité du phone - ✅
Auth0Service.loginWithPhonePin()fonctionne sans Customer
Tests d'Intégration
- ✅ Register avec phone → phone sauvegardé dans auth.users
- ✅ Login phone-PIN → fonctionne avec auth.users uniquement
- ✅ Reset PIN → fonctionne avec auth.users uniquement
- ✅ Doublon phone → erreur lors de la création
Tests de Migration
- ✅ Migration SQL s'exécute sans erreur
- ✅ Données existantes migrées correctement
- ✅ Index créés et fonctionnels
- ✅ 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)
- Migrer Register vers Orchestrator : Maintenant que Auth est autonome, on peut déplacer Register vers Orchestrator
- Synchronisation Phone : Si phone change dans Customer, synchroniser avec Auth (via événements)
- Support Multi-Phone : Si besoin, ajouter support pour plusieurs phones par user
Plan créé le 22/01/2026