✨ Servicio de Migración de Datos de Usuarios de Wordpress por Medio de API
La API migrate-user está diseñada para permitir la migración de usuarios desde sistemas externos como WordPress, registrando sus datos personales y asociándolos a una vertical del sistema, pero sin requerir una contraseña.
⚠️ Esta API funciona de la misma manera que la de registro de usuarios a diferencia que no exige contraseña de la vertical.
Esta API no requiere autenticación JWT, ya que está pensada como un endpoint público para el proceso inicial de migración.
⚙️ Funcionamiento interno de la API
El componente completo de migrado es el siguiente:
const WordpressUserKeys = User.VALID_USER_MIGRATE_KEYS.filter( (key) => ![User.USER_FIELD_PASSWORD].includes(key));
function validateRequestBody(body: UserProfileUpdateData) { validateKeys(body, WordpressUserKeys, User.FIELD_ROOT);
if (body.phone !== undefined) { if (typeof body.phone !== User.FIELD_OBJECT) throw new Error(ERROR_INVALID_FORMAT + User.COMPONENT_NAME_PHONE);
validateKeys(body.phone, User.VALID_PHONE_KEYS, User.COMPONENT_NAME_PHONE); }
const addressFields = [ { value: body.billingAddress, validKeys: User.VALID_BILLING_ADDRESS_KEYS, context: User.COMPONENT_NAME_BILLING, }, { value: body.mailingAddress, validKeys: User.VALID_MAILING_ADDRESS_KEYS, context: User.COMPONENT_NAME_MAILING, }, ];
for (const { value, validKeys, context } of addressFields) { if (value !== undefined) { if (typeof value !== User.FIELD_OBJECT) throw new Error(ERROR_INVALID_FORMAT + context);
validateKeys(value, validKeys, context);
if (value.phone !== undefined) { if (typeof value.phone !== User.FIELD_OBJECT) throw new Error(ERROR_INVALID_FORMAT + `${context}.phone`);
validateKeys(value.phone, User.VALID_PHONE_KEYS, `${context}.phone`); } } }}
const validatePhoneFields = (phone?: { codePhone?: string; number?: string;}) => { if (!phone) return; validateFieldPatterns( { codePhone: phone.codePhone }, { codePhone: FIELD_VALIDATIONS.codePhone } ); validateFieldPatterns( { number: phone.number }, { number: FIELD_VALIDATIONS.number } );};
export default { async migrateUser(ctx) { const body = ctx.request.body;
try { validateRequestBody(ctx.request.body); validateRequiredFields(body, User.MIGRATE_VERIFICATION_KEYS); const { email, vertical, origin, ...cleanBody } = body;
validatePhoneFields(cleanBody.phone); validatePhoneFields(cleanBody.billingAddress?.phone); validatePhoneFields(cleanBody.mailingAddress?.phone);
const strapiUser = await findOne( User.USER, User.FIELDS_USER, { email }, User.COMPONENTS_USER );
if (strapiUser === NOT_FOUND) { await create(User.USER, { email, origin, identificationNumber: cleanBody.identificationNumber, nameLastName: cleanBody.nameLastName, role: 2, username: email, verticalsUserPasswords: [ { vertical, verticalPassword: undefined, }, ], phone: cleanBody.phone, billingAddress: cleanBody.billingAddress, mailingAddress: cleanBody.mailingAddress, }); } else { const updatedData = { ...strapiUser, ...cleanBody, mailingAddress: { ...strapiUser.mailingAddress, ...cleanBody.mailingAddress, }, billingAddress: { ...strapiUser.billingAddress, ...cleanBody.billingAddress, }, };
await syncPasswordWithVerticals( email, undefined, vertical, updatedData );
await updateDocument(User.USER, strapiUser.documentId, { origin, }); }
const registeredUser = await findOne( User.USER, User.FIELDS_USER, { email }, User.COMPONENTS_USER );
return ctx.send(registeredUser, 200); } catch (error) { return ctx.badRequest(error); } },};Cuando se realiza una solicitud a esta API, el primer paso es la validación del cuerpo para garantizar que no se estén enviando campos inesperados o mal estructurados.
-
Se valida tanto que existan las claves obligatorias (
email,origin,vertical) mediante el util:validateRequiredFields(...); -
Así como los objetos anidados (
mailingAddress,billingAddress) mediante la función:validateRequestBody(...);:
La cual hace la llamada al util validateKeys por los 3 datos que espera recibir, datos de usuario y sus componentes de direcciones.
function validateRequestBody(body: UserProfileUpdateData) { validateKeys(body, WordpressUserKeys, User.FIELD_ROOT);
if (body.phone !== undefined) { if (typeof body.phone !== User.FIELD_OBJECT) throw new Error(ERROR_INVALID_FORMAT + User.COMPONENT_NAME_PHONE);
validateKeys(body.phone, User.VALID_PHONE_KEYS, User.COMPONENT_NAME_PHONE); }
const addressFields = [ { value: body.billingAddress, validKeys: User.VALID_BILLING_ADDRESS_KEYS, context: User.COMPONENT_NAME_BILLING, }, { value: body.mailingAddress, validKeys: User.VALID_MAILING_ADDRESS_KEYS, context: User.COMPONENT_NAME_MAILING, }, ];
for (const { value, validKeys, context } of addressFields) { if (value !== undefined) { if (typeof value !== User.FIELD_OBJECT) throw new Error(ERROR_INVALID_FORMAT + context);
validateKeys(value, validKeys, context);
if (value.phone !== undefined) { if (typeof value.phone !== User.FIELD_OBJECT) throw new Error(ERROR_INVALID_FORMAT + `${context}.phone`);
validateKeys(value.phone, User.VALID_PHONE_KEYS, `${context}.phone`); } } }}Asi como tambien valida los campos mediante sus regex correspondientes para cada uno de los campos de telefono. esta se encuentra en el mismo URL que el de arriba->validateFieldPatterns.
const validatePhoneFields = (phone?: { codePhone?: string; number?: string;}) => { if (!phone) return; validateFieldPatterns( { codePhone: phone.codePhone }, { codePhone: FIELD_VALIDATIONS.codePhone } ); validateFieldPatterns( { number: phone.number }, { number: FIELD_VALIDATIONS.number } );};
validatePhoneFields(cleanBody.phone);validatePhoneFields(cleanBody.billingAddress?.phone);validatePhoneFields(cleanBody.mailingAddress?.phone);asegurandose contengan únicamente las propiedades válidas.
🧾 Datos esperados en el cuerpo de la solicitud (body)
El cuerpo debe enviarse en formato JSON y puede incluir los siguientes campos:
email(obligatorio): correo electrónico único del usuario.vertical(obligatorio): nombre de la vertical para la cual se está registrando la contraseña.origin(obligatorio): campo específico que indica que el usuario proviene de WordPress.identificationNumber: número de identificación oficial o empresarial.nameLastName: nombre completo del usuario.phone: teléfono de contacto en un objeto con los camposcodePhoneynumber.mailingAddress: dirección de envío, con campos comoname,street,city,zipCode,country, etc.billingAddress: dirección de facturación, con campos comoname,company,identificationNumber,phone,street,city,country, etc.
Antes de procesar cualquier operación, el controlador valida que solo se estén enviando las claves permitidas, tanto a nivel raíz como en las direcciones anidadas.
Una vez validado el contenido, se separan las credenciales (email, origin, vertical) del resto de los datos (cleanBody). Esto facilita el manejo diferenciado de las contraseñas y los datos personales del usuario.
const { email, vertical, origin, ...cleanBody } = body;Luego, el sistema intenta localizar un usuario existente con el mismo correo electrónico. Aquí se distinguen dos posibles caminos:
- Si el usuario no existe, se crea uno nuevo en la base de datos con todos los datos proporcionados. También se inicializa el campo
verticalsUserPasswords, que es un arreglo de objetos con las clavesverticalyverticalPassword. Esto permite que un mismo usuario pueda autenticarse con distintas contraseñas dependiendo de la vertical en la que esté operando.
if (strapiUser === NOT_FOUND) { await create(User.USER, { email, origin, identificationNumber: cleanBody.identificationNumber, nameLastName: cleanBody.nameLastName, role: 2, username: email, verticalsUserPasswords: [ { vertical, verticalPassword: undefined, }, ], phone: cleanBody.phone, billingAddress: cleanBody.billingAddress, mailingAddress: cleanBody.mailingAddress, });}- Si el usuario ya existe, entonces se invoca una función auxiliar llamada
syncPasswordWithVerticals. Esta función verifica si el usuario ya tiene una contraseña asociada a la vertical solicitada. Si ya existe, lanza un error y detiene el proceso para evitar sobrescrituras no deseadas. Si no existe aún una contraseña para esa vertical, la agrega al arregloverticalsUserPasswords. Esta operación es atómica y segura, y además permite actualizar el resto de los datos del usuario si fueron incluidos en la solicitud.
Antes de eso se hace un llenado de la información del usuario por si al cambiar la contraseña es necesario actualizar alguno de los datos anteriormente mencionados del usuario sin sobrescribir los datos antiguos si no están siendo modificados 👍
Despues de la sincronización, se cambia el origin del usuario para saber que una de las verticales del mismo ha sido exportada
else { const updatedData = { ...strapiUser, ...cleanBody, mailingAddress: { ...strapiUser.mailingAddress, ...cleanBody.mailingAddress, }, billingAddress: { ...strapiUser.billingAddress, ...cleanBody.billingAddress, }, };
await syncPasswordWithVerticals( email, undefined, vertical, updatedData, );
await updateDocument(User.USER, strapiUser.documentId, { origin, }); }🔐 ¿Qué hace syncPasswordWithVerticals?
- Vease su funcionamiento Aquí.
📤 Ejemplo de solicitud
URL: /api/user-endpoints/migrate
Método: POST
Body ejemplo:
{ "email": "prueba2@prueba2.com", "vertical": "Pharma", "origin": "wordpress", "phone": { "codePhone": "+52", "number": "5551234567" }, "identificationNumber": "ABC123456", "nameLastName": "Fernando Rodríguez", "mailingAddress": { "name": "2donombre", "city": "CIUDADO", "phone": { "codePhone": "+52", "number": "5551234567" } }, "billingAddress": { "name": "123123123" }}✅ Respuestas esperadas
✔️ Registro exitoso
{ "id": 1, "email": "nuevo@email.com", "nameLastName": "Fernando Nuevo", ...}❌ Campos inválidos o faltantes
{ "data": null, "error": { "status": 400, "name": "Error", "message": "Missing required parameters: origin", "details": {} }}{ "data": null, "error": { "status": 400, "name": "Error", "message": "Not Allowed fields at root: password", "details": {} }}❌ Contraseña ya registrada para esa vertical
{ "data": null, "error": { "status": 400, "name": "Error", "message": "Error: A password record already exists for this vertical", "details": {} }}