Move SMS feature to the main file

This commit is contained in:
Omar Oughriss 2025-07-18 15:41:58 +02:00
parent 862d82ac6b
commit f6f36e1453
2 changed files with 353 additions and 361 deletions

View File

@ -1,179 +0,0 @@
const express = require('express');
const cors = require('cors');
const fetch = require('node-fetch');
// Initialisation de l'application Express
const app = express();
const PORT = process.env.PORT || 8080;
// Configuration CORS
const corsOptions = {
origin: ['http://local.lecoffreio.4nkweb:3000', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
};
app.use(cors(corsOptions));
app.use(express.json());
function getOfficeStatus(statusName) {
switch (statusName) {
case 'Pourvu':
return 'ACTIVATED';
case 'Pourvu mais décédé':
return 'ACTIVATED';
case 'Sans titulaire':
return 'ACTIVATED';
case 'Vacance':
return 'ACTIVATED';
case 'En activité':
return 'ACTIVATED';
default:
return 'DESACTIVATED';
}
}
function getRole(roleName) {
switch (roleName) {
case 'Notaire titulaire':
return { name: 'admin', label: 'Administrateur' };
case 'Notaire associé':
return { name: 'admin', label: 'Administrateur' };
case 'Notaire salarié':
return { name: 'notary', label: 'Notaire' };
case 'Collaborateur':
return { name: 'notary', label: 'Notaire' };
case 'Suppléant':
return { name: 'notary', label: 'Notaire' };
case 'Administrateur':
return { name: 'admin', label: 'Administrateur' };
case 'Curateur':
return { name: 'notary', label: 'Notaire' };
default:
return { name: 'default', label: 'Défaut' };
}
}
function getCivility(civility) {
switch (civility) {
case 'Monsieur':
return 'MALE';
case 'Madame':
return 'FEMALE';
default:
return 'OTHERS';
}
}
app.get('/api/v1/health', (req, res) => {
res.json({ message: 'OK' });
});
app.post('/api/v1/idnot/user/:code', async (req, res) => {
const code = req.params.code;
try {
const params = {
client_id: 'B3CE56353EDB15A9',
client_secret: '3F733549E879878344B6C949B366BB5CDBB2DB5B7F7AB7EBBEBB0F0DD0776D1C',
redirect_uri: 'http://local.lecoffreio.4nkweb:3000/authorized-client',
grant_type: 'authorization_code',
code: code
};
const tokens = await (
await fetch('https://qual-connexion.idnot.fr/user/IdPOAuth2/token/idnot_idp_v1', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(params).toString()
})
).json();
const jwt = tokens.id_token;
if (!jwt) {
console.error('jwt not defined');
return null;
}
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64').toString('utf8'));
const searchParams = new URLSearchParams({
key: 'ba557f84-0bf6-4dbf-844f-df2767555e3e'
});
let userData;
try {
userData = await (
await fetch(`https://qual-api.notaires.fr/annuaire/api/pp/v2/rattachements/${payload.profile_idn}?` + searchParams, {
method: 'GET'
})
).json();
} catch (error) {
console.error('Error fetching ' + `https://qual-api.notaires.fr/annuaire/api/pp/v2/rattachements/${payload.profile_idn}`, error);
return null;
}
if (!userData || !userData.statutDuRattachement || userData.entite.typeEntite.name !== 'office') {
console.error('User not attached to an office (May be a partner)');
return null;
}
let officeLocationData;
try {
officeLocationData = (await (
await fetch(`https://qual-api.notaires.fr/annuaire${userData.entite.locationsUrl}?` + searchParams,
{
method: 'GET'
})
).json());
} catch (error) {
console.error('Error fetching' + `https://qual-api.notaires.fr/annuaire${userData.entite.locationsUrl}`, error);
return null;
}
if (!officeLocationData || !officeLocationData.result || officeLocationData.result.length === 0) {
console.error('Office location data not found');
return null;
}
const idnotUser = {
idNot: payload.sub,
office: {
idNot: payload.entity_idn,
name: userData.entite.denominationSociale ?? userData.entite.codeCrpcen,
crpcen: userData.entite.codeCrpcen,
office_status: getOfficeStatus(userData.entite.statutEntite.name),
address: {
address: officeLocationData.result[0].adrGeo4,
city: officeLocationData.result[0].adrGeoVille.split(' ')[0] ?? officeLocationData.result[0].adrGeoVille,
zip_code: Number(officeLocationData.result[0].adrGeoCodePostal)
},
status: 'ACTIVE'
},
role: getRole(userData.typeLien.name),
contact: {
first_name: userData.personne.prenom,
last_name: userData.personne.nomUsuel,
email: userData.mailRattachement,
phone_number: userData.numeroTelephone,
cell_phone_number: userData.numeroMobile ?? userData.numeroTelephone,
civility: getCivility(userData.personne.civilite)
}
};
if (!idnotUser.contact.email) {
console.error("User pro email empty");
return null;
}
res.json(idnotUser);
} catch (error) {
res.status(500).json({
error: 'Internal Server Error',
message: error.message
});
}
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

View File

@ -1,224 +1,395 @@
const express = require('express'); const express = require('express');
const cors = require('cors');
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const ovh = require('ovh'); const ovh = require('ovh');
require('dotenv').config(); require('dotenv').config();
// Initialisation de l'application Express
const app = express(); const app = express();
app.use(express.json()); const PORT = process.env.PORT || 8080;
const config = { // Configuration CORS
// Configuration OVH const corsOptions = {
OVH_APP_KEY: process.env.OVH_APP_KEY, origin: ['http://local.lecoffreio.4nkweb:3000', 'http://localhost:3000'],
OVH_APP_SECRET: process.env.OVH_APP_SECRET, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
OVH_CONSUMER_KEY: process.env.OVH_CONSUMER_KEY, allowedHeaders: ['Content-Type', 'Authorization']
OVH_SMS_SERVICE_NAME: process.env.OVH_SMS_SERVICE_NAME,
// Configuration SMS Factor
SMS_FACTOR_TOKEN: process.env.SMS_FACTOR_TOKEN,
PORT: process.env.PORT || 8080
}; };
// Stockage temporaire des codes app.use(cors(corsOptions));
app.use(express.json());
function getOfficeStatus(statusName) {
switch (statusName) {
case 'Pourvu':
return 'ACTIVATED';
case 'Pourvu mais décédé':
return 'ACTIVATED';
case 'Sans titulaire':
return 'ACTIVATED';
case 'Vacance':
return 'ACTIVATED';
case 'En activité':
return 'ACTIVATED';
default:
return 'DESACTIVATED';
}
}
function getRole(roleName) {
switch (roleName) {
case 'Notaire titulaire':
return { name: 'admin', label: 'Administrateur' };
case 'Notaire associé':
return { name: 'admin', label: 'Administrateur' };
case 'Notaire salarié':
return { name: 'notary', label: 'Notaire' };
case 'Collaborateur':
return { name: 'notary', label: 'Notaire' };
case 'Suppléant':
return { name: 'notary', label: 'Notaire' };
case 'Administrateur':
return { name: 'admin', label: 'Administrateur' };
case 'Curateur':
return { name: 'notary', label: 'Notaire' };
default:
return { name: 'default', label: 'Défaut' };
}
}
function getCivility(civility) {
switch (civility) {
case 'Monsieur':
return 'MALE';
case 'Madame':
return 'FEMALE';
default:
return 'OTHERS';
}
}
app.get('/api/v1/health', (req, res) => {
res.json({ message: 'OK' });
});
app.post('/api/v1/idnot/user/:code', async (req, res) => {
const code = req.params.code;
try {
const params = {
client_id: 'B3CE56353EDB15A9',
client_secret: '3F733549E879878344B6C949B366BB5CDBB2DB5B7F7AB7EBBEBB0F0DD0776D1C',
redirect_uri: 'http://local.lecoffreio.4nkweb:3000/authorized-client',
grant_type: 'authorization_code',
code: code
};
const tokens = await (
await fetch('https://qual-connexion.idnot.fr/user/IdPOAuth2/token/idnot_idp_v1', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(params).toString()
})
).json();
const jwt = tokens.id_token;
if (!jwt) {
console.error('jwt not defined');
return null;
}
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64').toString('utf8'));
const searchParams = new URLSearchParams({
key: 'ba557f84-0bf6-4dbf-844f-df2767555e3e'
});
let userData;
try {
userData = await (
await fetch(`https://qual-api.notaires.fr/annuaire/api/pp/v2/rattachements/${payload.profile_idn}?` + searchParams, {
method: 'GET'
})
).json();
} catch (error) {
console.error('Error fetching ' + `https://qual-api.notaires.fr/annuaire/api/pp/v2/rattachements/${payload.profile_idn}`, error);
return null;
}
if (!userData || !userData.statutDuRattachement || userData.entite.typeEntite.name !== 'office') {
console.error('User not attached to an office (May be a partner)');
return null;
}
let officeLocationData;
try {
officeLocationData = (await (
await fetch(`https://qual-api.notaires.fr/annuaire${userData.entite.locationsUrl}?` + searchParams,
{
method: 'GET'
})
).json());
} catch (error) {
console.error('Error fetching' + `https://qual-api.notaires.fr/annuaire${userData.entite.locationsUrl}`, error);
return null;
}
if (!officeLocationData || !officeLocationData.result || officeLocationData.result.length === 0) {
console.error('Office location data not found');
return null;
}
const idnotUser = {
idNot: payload.sub,
office: {
idNot: payload.entity_idn,
name: userData.entite.denominationSociale ?? userData.entite.codeCrpcen,
crpcen: userData.entite.codeCrpcen,
office_status: getOfficeStatus(userData.entite.statutEntite.name),
address: {
address: officeLocationData.result[0].adrGeo4,
city: officeLocationData.result[0].adrGeoVille.split(' ')[0] ?? officeLocationData.result[0].adrGeoVille,
zip_code: Number(officeLocationData.result[0].adrGeoCodePostal)
},
status: 'ACTIVE'
},
role: getRole(userData.typeLien.name),
contact: {
first_name: userData.personne.prenom,
last_name: userData.personne.nomUsuel,
email: userData.mailRattachement,
phone_number: userData.numeroTelephone,
cell_phone_number: userData.numeroMobile ?? userData.numeroTelephone,
civility: getCivility(userData.personne.civilite)
}
};
if (!idnotUser.contact.email) {
console.error("User pro email empty");
return null;
}
res.json(idnotUser);
} catch (error) {
res.status(500).json({
error: 'Internal Server Error',
message: error.message
});
}
});
//------------------------------------ SMS Section ------------------------------------
const config = {
// OVH config
OVH_APP_KEY: process.env.OVH_APP_KEY,
OVH_APP_SECRET: process.env.OVH_APP_SECRET,
OVH_CONSUMER_KEY: process.env.OVH_CONSUMER_KEY,
OVH_SMS_SERVICE_NAME: process.env.OVH_SMS_SERVICE_NAME,
// SMS Factor config
SMS_FACTOR_TOKEN: process.env.SMS_FACTOR_TOKEN,
PORT: process.env.PORT || 8080
};
// Codes storage
const verificationCodes = new Map(); const verificationCodes = new Map();
// Service SMS // Service SMS
class SmsService { class SmsService {
static generateCode() { static generateCode() {
return Math.floor(100000 + Math.random() * 900000); return Math.floor(100000 + Math.random() * 900000);
} }
// Service OVH // OVH Service
static sendSmsWithOvh(phoneNumber, message) { static sendSmsWithOvh(phoneNumber, message) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const ovhClient = ovh({ const ovhClient = ovh({
appKey: config.OVH_APP_KEY, appKey: config.OVH_APP_KEY,
appSecret: config.OVH_APP_SECRET, appSecret: config.OVH_APP_SECRET,
consumerKey: config.OVH_CONSUMER_KEY consumerKey: config.OVH_CONSUMER_KEY
}); });
ovhClient.request('POST', `/sms/${config.OVH_SMS_SERVICE_NAME}/jobs`, { ovhClient.request('POST', `/sms/${config.OVH_SMS_SERVICE_NAME}/jobs`, {
message: message, message: message,
receivers: [phoneNumber], receivers: [phoneNumber],
senderForResponse: false, senderForResponse: false,
sender: "not.IT Fact", sender: "not.IT Fact",
noStopClause: true noStopClause: true
}, (error, result) => { }, (error, result) => {
if (error) { if (error) {
console.error('Erreur OVH SMS:', error); console.error('Erreur OVH SMS:', error);
resolve({ success: false, error: 'Échec de l\'envoi du SMS via OVH' }); resolve({ success: false, error: 'Échec de l\'envoi du SMS via OVH' });
} else { } else {
resolve({ success: true }); resolve({ success: true });
} }
}); });
}); });
} }
// Service SMS Factor // SMS Factor Service
static async sendSmsWithSmsFactor(phoneNumber, message) { static async sendSmsWithSmsFactor(phoneNumber, message) {
try { try {
const url = new URL('https://api.smsfactor.com/send/simulate'); const url = new URL('https://api.smsfactor.com/send/simulate');
url.searchParams.append('to', phoneNumber); url.searchParams.append('to', phoneNumber);
url.searchParams.append('text', message); url.searchParams.append('text', message);
url.searchParams.append('sender', 'LeCoffre'); url.searchParams.append('sender', 'LeCoffre');
url.searchParams.append('token', config.SMS_FACTOR_TOKEN); url.searchParams.append('token', config.SMS_FACTOR_TOKEN);
const response = await fetch(url.toString()); const response = await fetch(url.toString());
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Erreur SMS Factor:', error); console.error('Erreur SMS Factor:', error);
return { success: false, error: 'Échec de l\'envoi du SMS via SMS Factor' }; return { success: false, error: 'Échec de l\'envoi du SMS via SMS Factor' };
} }
} }
// Méthode principale d'envoi // Main method
static async sendSms(phoneNumber, message) { static async sendSms(phoneNumber, message) {
// Essai d'abord avec OVH // Try first with OVH
const ovhResult = await this.sendSmsWithOvh(phoneNumber, message); const ovhResult = await this.sendSmsWithOvh(phoneNumber, message);
if (ovhResult.success) { if (ovhResult.success) {
return ovhResult; return ovhResult;
} }
// Si OVH échoue, essai avec SMS Factor // If OVH fails, try with SMS Factor
console.log('OVH SMS failed, trying SMS Factor...'); console.log('OVH SMS failed, trying SMS Factor...');
return await this.sendSmsWithSmsFactor(phoneNumber, message); return await this.sendSmsWithSmsFactor(phoneNumber, message);
} }
} }
// Middleware de validation // Phone number validation middleware
const validatePhoneNumber = (req, res, next) => { const validatePhoneNumber = (req, res, next) => {
const { phoneNumber } = req.body; const { phoneNumber } = req.body;
if (!phoneNumber) { if (!phoneNumber) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Le numéro de téléphone est requis' message: 'Le numéro de téléphone est requis'
}); });
} }
// Validation basique du format // Validation basique du format
const phoneRegex = /^\+?[1-9]\d{1,14}$/; const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(phoneNumber)) { if (!phoneRegex.test(phoneNumber)) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Format de numéro de téléphone invalide' message: 'Format de numéro de téléphone invalide'
}); });
} }
next(); next();
}; };
// Routes // Routes
app.post('/api/send-verification', validatePhoneNumber, async (req, res) => { app.post('/api/send-code', validatePhoneNumber, async (req, res) => {
const { phoneNumber } = req.body; const { phoneNumber } = req.body;
try { try {
// Vérifier si un code existe déjà et n'est pas expiré // Check if a code already exists and is not expired
const existingVerification = verificationCodes.get(phoneNumber); const existingVerification = verificationCodes.get(phoneNumber);
if (existingVerification) { if (existingVerification) {
const timeSinceLastSend = Date.now() - existingVerification.timestamp; const timeSinceLastSend = Date.now() - existingVerification.timestamp;
if (timeSinceLastSend < 30000) { // 30 secondes if (timeSinceLastSend < 30000) { // 30 secondes
return res.status(429).json({ return res.status(429).json({
success: false, success: false,
message: 'Veuillez attendre 30 secondes avant de demander un nouveau code' message: 'Veuillez attendre 30 secondes avant de demander un nouveau code'
}); });
} }
} }
// Générer un nouveau code // Generate a new code
const code = SmsService.generateCode(); const code = SmsService.generateCode();
// Stocker le code // Store the code
verificationCodes.set(phoneNumber, { verificationCodes.set(phoneNumber, {
code, code,
timestamp: Date.now(), timestamp: Date.now(),
attempts: 0 attempts: 0
}); });
// Envoyer le SMS // Send the SMS
const message = `Votre code de vérification LeCoffre est : ${code}`; const message = `Votre code de vérification LeCoffre est : ${code}`;
const result = await SmsService.sendSms(phoneNumber, message); const result = await SmsService.sendSms(phoneNumber, message);
if (result.success) { if (result.success) {
res.json({ res.json({
success: true, success: true,
message: 'Code envoyé avec succès', message: 'Code envoyé avec succès',
}); });
} else { } else {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Échec de l\'envoi du SMS via les deux fournisseurs' message: 'Échec de l\'envoi du SMS via les deux fournisseurs'
}); });
} }
} catch (error) { } catch (error) {
console.error('Erreur:', error); console.error('Erreur:', error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Erreur serveur lors de l\'envoi du code' message: 'Erreur serveur lors de l\'envoi du code'
}); });
} }
}); });
app.post('/api/verify-code', validatePhoneNumber, (req, res) => { app.post('/api/verify-code', validatePhoneNumber, (req, res) => {
const { phoneNumber, code } = req.body; const { phoneNumber, code } = req.body;
if (!code) { if (!code) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Le code est requis' message: 'Le code est requis'
}); });
} }
const verification = verificationCodes.get(phoneNumber); const verification = verificationCodes.get(phoneNumber);
if (!verification) { if (!verification) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Aucun code n\'a été envoyé à ce numéro' message: 'Aucun code n\'a été envoyé à ce numéro'
}); });
} }
// Vérifier si le code n'a pas expiré (5 minutes) // Check if the code has not expired (5 minutes)
if (Date.now() - verification.timestamp > 5 * 60 * 1000) { if (Date.now() - verification.timestamp > 5 * 60 * 1000) {
verificationCodes.delete(phoneNumber); verificationCodes.delete(phoneNumber);
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Le code a expiré' message: 'Le code a expiré'
}); });
} }
// Vérifier le code // Check if the code is correct
if (verification.code.toString() === code.toString()) { if (verification.code.toString() === code.toString()) {
verificationCodes.delete(phoneNumber); verificationCodes.delete(phoneNumber);
res.json({ res.json({
success: true, success: true,
message: 'Code vérifié avec succès' message: 'Code vérifié avec succès'
}); });
} else { } else {
verification.attempts += 1; verification.attempts += 1;
if (verification.attempts >= 3) { if (verification.attempts >= 3) {
verificationCodes.delete(phoneNumber); verificationCodes.delete(phoneNumber);
res.status(400).json({ res.status(400).json({
success: false, success: false,
message: 'Trop de tentatives. Veuillez demander un nouveau code' message: 'Trop de tentatives. Veuillez demander un nouveau code'
}); });
} else { } else {
res.status(400).json({ res.status(400).json({
success: false, success: false,
message: 'Code incorrect' message: 'Code incorrect'
}); });
} }
} }
}); });
// Démarrage du serveur app.listen(PORT, () => {
app.listen(config.PORT, () => { console.log(`Server is running on port ${PORT}`);
console.log(`Serveur démarré sur le port ${config.PORT}`);
}); });