🔐 Servicio de loginUser - API Autenticación de usuario por vertical
⚙️ Funcionamiento interno de la API
El API completo de Login es el siguiente:
async function verifyComponentData(verticalsUserPasswords, vertical) { if ( !Array.isArray(verticalsUserPasswords) || verticalsUserPasswords.length === 0 ) { throw new Error(ERROR_PASSWORD_NOT_FOUND); }
const existsOnVertical = verticalsUserPasswords.find( (pass) => pass.vertical === vertical ); if (!existsOnVertical) throw new Error(ERROR_VERTICAL_PASSWORD_NOT_FOUND);
return existsOnVertical;}
export default { async loginUser(ctx) { const body = ctx.request.body;
try { const { email, password, vertical } = body; validateKeys(body, VERIFICATION_KEYS, FIELD_ROOT);
const requiredFields = VERIFICATION_KEYS.filter( (key) => key !== USER_FIELD_PASSWORD ); validateRequiredFields(body, requiredFields);
const strapiUser = await findOne( USER, FIELDS_USER, { email }, COMPONENT_NAME_USER_PASSWORDS ); if (strapiUser === NOT_FOUND) throw new Error(ERROR_USER_NOT_FOUND);
if (strapiUser.origin === USER_ORIGIN_WORDPRESS) { const existsOnVertical = await verifyComponentData( strapiUser.verticalsUserPasswords, vertical );
if (!existsOnVertical.verticalPassword) { return ctx.send( { code: PASSWORD_RESET_REQUIRED_CODE, message: PASSWORD_RESET_REQUIRED_MESSAGE, }, 409 ); } }
validateRequiredFields(body, VERIFICATION_KEYS); const existsOnVertical = await verifyComponentData( strapiUser.verticalsUserPasswords, vertical ); const existingPassword = await bcrypt.compare( password, existsOnVertical.verticalPassword ); if (!existingPassword) throw new Error(ERROR_INVALID_PASSWORD);
const jwt = strapi.plugins[USER_STRIPE_USERS_PLUGIN].services.jwt.issue({ id: strapiUser.id, email: strapiUser.email, vertical, });
const sanitizedUser = { id: strapiUser.id, documentId: strapiUser.documentId, username: strapiUser.username, email: strapiUser.email, confirmed: strapiUser.confirmed, phone: strapiUser.phone, origin: strapiUser.origin, identificationNumber: strapiUser.identificationNumber, nameLastName: strapiUser.nameLastName, };
return ctx.send( { jwt, user: sanitizedUser, }, 200 ); } catch (error) { return ctx.badRequest(error); } },};Esta API está encargada de hacer el inicio de sesión de los usuarios registrados, ya sea de usuarios normales (creados directamente en el sistema) o usuarios migrados desde WordPress. Por esa razón, no todos los usuarios siguen el mismo flujo de validación: algunos requerirán contraseña, otros podrían ser redirigidos a un flujo de recuperación.
Autentica a un usuario en función de su correo, contraseña y vertical específico. Retorna un token JWT y los datos esenciales del usuario.
El proceso comienza recibiendo el body de la solicitud HTTP (ctx.request.body), el cual debe contener tres campos: email, password y vertical. Esto permite que un mismo usuario pueda tener distintas contraseñas y roles dependiendo del contexto.
📨 Recepción del body y validación de claves
const { email, password, vertical } = body;validateKeys(body, VERIFICATION_KEYS, FIELD_ROOT);Antes de realizar cualquier consulta a la base de datos, el sistema valida que el objeto recibido (body) no contenga claves inesperadas. Esto se logra con la función validateKeys, que compara las claves del objeto con un arreglo permitido (VERIFICATION_KEYS). Este paso es fundamental para evitar que se cuelen campos no deseados que puedan ser utilizados para inyecciones o errores lógicos.
✅ Validación inicial de campos requeridos (sin password todavía)
const requiredFields = VERIFICATION_KEYS.filter( (key) => key !== USER_FIELD_PASSWORD);validateRequiredFields(body, requiredFields);Luego, se verifica que el body contenga las claves obligatorias mínimas validateRequiredFields.
Inicialmente, no se incluye password como obligatoria ya que algunos usuarios migrados desde WordPress podrían no tenerla configurada aún. Por eso, en esta etapa se exige solamente email y vertical, permitiendo que la validación de password se haga más adelante si realmente se requiere.
🔍 Búsqueda del usuario en la base de datos
const strapiUser = await findOne( USER, FIELDS_USER, { email }, COMPONENT_NAME_USER_PASSWORDS);Después de las validaciones iniciales, se intenta buscar al usuario en la base de datos mediante la función findOne.
🧩 Validación especial para usuarios migrados desde WordPress
if (strapiUser.origin === USER_ORIGIN_WORDPRESS) { const existsOnVertical = await verifyComponentData(...); if (!existsOnVertical.verticalPassword) { return ctx.send({ code, message }, 409); }}Una vez obtenido el usuario, se evalúa si fue migrado desde WordPress. Esto se hace revisando el campo origin del usuario, el cual debe coincidir con la constante USER_ORIGIN_WORDPRESS. En caso afirmativo, se ejecuta una validación adicional: se verifica que el usuario tenga una contraseña registrada para el vertical que intenta acceder. Esta lógica se encapsula en la función auxiliar verifyComponentData, que explora el arreglo verticalsUserPasswords buscando una entrada que coincida con el vertical solicitado. Si se encuentra esa entrada Y si la entrada no tiene una contraseña asociada (verticalPassword), el sistema devuelve una respuesta 409 Conflict, indicando que es necesario restablecer la contraseña.
🔁 Segunda validación con password incluida
validateRequiredFields(body, VERIFICATION_KEYS);Una vez pasado ese filtro, se vuelve a validar que todos los campos estén presentes, incluyendo ahora sí el password.
Esto con el motivo de que si se descarta la posibilidad de que sea un usuario de wordpress entonces si debe ser obligatorio el que envie un password.
🧪 Comparación de contraseñas por vertical
const existsOnVertical = await verifyComponentData(...);const existingPassword = await bcrypt.compare(password, existsOnVertical.verticalPassword);if (!existingPassword) throw new Error(ERROR_INVALID_PASSWORD);Con todos los datos verificados, se utiliza nuevamente verifyComponentData para obtener la contraseña cifrada del vertical indicado. Después se usa bcrypt.compare para comparar el hash almacenado con la contraseña ingresada. Si no coinciden, el login se rechaza con el error ERROR_INVALID_PASSWORD.
🔐 Generación de JWT personalizado
const jwt = strapi.plugins[USER_STRIPE_USERS_PLUGIN].services.jwt.issue({ id: strapiUser.id, email: strapiUser.email, vertical,});En caso de éxito, se genera un token JWT usando los servicios internos del plugin JWT (strapi.plugins[USER_STRIPE_USERS_PLUGIN].services.jwt.issue). Este token se construye con el id y email del usuario, junto con el vertical, y servirá para autenticar futuras peticiones del cliente.
🧼 Sanitización del usuario para la respuesta
const sanitizedUser = { id: strapiUser.id, documentId: strapiUser.documentId, username: strapiUser.username, ... // demás campos seguros};Finalmente, se crea un objeto sanitizedUser que incluye únicamente los datos seguros y relevantes del usuario.
📤 Respuesta final al cliente
return ctx.send({ jwt, user: sanitizedUser }, 200);Se responde al cliente con un 200 OK, el JWT y el objeto user resultante.
📤 Ejemplo de solicitud
URL: /api/user-endpoints/login
Método: POST
Body ejemplo:
{ "email": "prueba2@prueba2.com", "password": "MiContraseñaSegura123", "vertical": "Pharma"}✅ Respuestas esperadas
✔️ Login exitoso
{ "jwt": " ... ", "user": { "id": 130, "documentId": "yr1xmn5o0d1bq7cdh06iwymf", "username": "prueba@prueba.com", "email": "prueba@prueba.com", "confirmed": false, "phone": "5551234567", "origin": null, "identificationNumber": "ABC123456", "nameLastName": "Fernando Rodríguez" }}❌ Campos inválidos o faltantes
{ "data": null, "error": { "status": 400, "name": "Error", "message": "Missing required parameters: password", "details": {} }}{ "data": null, "error": { "status": 400, "name": "Error", "message": "Not Allowed fields at root: passwordss", "details": {} }}✔️ Usuario de wordpress
{ "code": "PASSWORD_RESET_REQUIRED", "message": "Password update required. This account was migrated and needs a new password."}