🔑 Strapi Passwords
🧩 Componente verticalsUserPasswords
Section titled “🧩 Componente verticalsUserPasswords”Este componente personalizado en Strapi permite que cada usuario tenga una contraseña diferente por cada vertical o subsistema del producto.
🗂️ Definición del schema
Section titled “🗂️ Definición del schema”{ "collectionName": "components_verticals_passwords_verticals_passwords_s", "info": { "displayName": "verticalsUserPasswords", "description": "" }, "options": {}, "attributes": { "vertical": { "type": "enumeration", "required": true, "enum": [ "Cirugía", "Edificio Hospital", "Emergencias", "Inspiria", "Oncología", "Pharma", "Salud Mental", "Traumatología", "UNIMED" ] }, "verticalPassword": { "type": "password", "private": true, "required": false }, "originWps": { "type": "boolean", "private": true, "required": false, "default": false, "configurable": false, "visible": false } }}vertical: campo obligatorio que representa la vertical a la que se le asigna la contraseña.verticalPassword: contraseña cifrada asociada a esa vertical específica. Es opcional y marcada como privada.originWps: campo que indica si la cuenta de una vertical ha sido migrada de wordpress
Este diseño hace posible que un mismo usuario tenga varias credenciales almacenadas, cada una asociada a una vertical específica.
🔄 Lifecycle para cifrado automático de contraseñas
Section titled “🔄 Lifecycle para cifrado automático de contraseñas”El componente cuenta con un lifecycle definido en src/index.ts que intercepta las operaciones de create y update sobre el componente para validar y encriptar la contraseña antes de guardarla.
🔐 Función de cifrado
Section titled “🔐 Función de cifrado”async function hashPassword(password: string) { if (!password) throw new ApplicationError(ERROR_PASSWORD_REQUIRED); return await bcrypt.hash(password, 10);}Esta función es utilizada internamente por los eventos del lifecycle para asegurar que nunca se guarde una contraseña en texto plano.
strapi.db.lifecycles.subscribe({ models: [COMPONENT_USER_PASSWORD],
async beforeCreate(event) { const { data } = event.params; if (!data) { throwValidationError( USER_FIELD_PARAMS, ERROR_PASSWORD_COMPONENT_NOT_FOUND ); }
if (data.originWps) { if (data.verticalPassword) { throwValidationError( USER_FIELD_VERTICALPASSWORD, ERROR_ORIGIN_WORDPRESS_PASSWORD ); } return; }
if (!data.id) { if (!data.vertical) throwValidationError(USER_FIELD_VERTICAL, VERTICAL_REQUIRED);
if (!data.verticalPassword) { throwValidationError( USER_FIELD_VERTICALPASSWORD, ERROR_PASSWORD_REQUIRED ); }
validateFieldPatterns( { password: data.verticalPassword }, { password: FIELD_VALIDATIONS.password, } );
data.verticalPassword = await hashPassword(data.verticalPassword); } },
async beforeUpdate(event) { const { data } = event.params;
if (!data) { throwValidationError( USER_FIELD_PARAMS, ERROR_PASSWORD_COMPONENT_NOT_FOUND ); }
if (!data.vertical) { throwValidationError(USER_FIELD_VERTICAL, VERTICAL_REQUIRED); }
const hasPassword = !!data.verticalPassword;
if (data.originWps) { if (hasPassword) { throwValidationError( USER_FIELD_VERTICALPASSWORD, ERROR_ORIGIN_WORDPRESS_PASSWORD ); } return; }
if (hasPassword) { const isHashed = /^(\$2[aby]\$|\$argon2)/.test(data.verticalPassword);
if (!isHashed) { validateFieldPatterns( { password: data.verticalPassword }, { password: FIELD_VALIDATIONS.password, } );
data.verticalPassword = await hashPassword(data.verticalPassword); } } else { if (!data.id) { throwValidationError( USER_FIELD_VERTICALPASSWORD, ERROR_PASSWORD_REQUIRED ); } } },});Este lifecycle garantiza que:
- Solo se cree una contraseña cuando el
originWpsno seatruees decir sea de wordpress. - Se validen los campos obligatorios (
vertical,verticalPassword). - La contraseña sea cifrada con
bcryptantes de ser almacenada unicamente si no ha sido hasheada anteriormente, todo automatico.
⚠️ Se incluyeron las siguientes funciones para el manejo del componente:
🛠️ Función: syncPasswordWithVerticals
Section titled “🛠️ Función: syncPasswordWithVerticals”export async function syncPasswordWithVerticals( email: string, verticalPassword: string | undefined, vertical: Verticals, userData?: UserProfileUpdateData, originWps?: boolean) { try { if (!email) throw new Error(ERROR_EMAIL_REQUIRED); if (!vertical) throw new Error(ERROR_VERTICAL_REQUIRED);
const strapiUser = await findOne( USER, FIELDS_USER, { email: email }, COMPONENT_NAME_USER_PASSWORDS ); if (strapiUser === NOT_FOUND) throw new Error(ERROR_USER_NOT_FOUND);
if ( Array.isArray(strapiUser.verticalsUserPasswords) && strapiUser.verticalsUserPasswords.length > 0 ) { const alreadyExists = strapiUser.verticalsUserPasswords.some( (pass) => pass.vertical === vertical ); if (alreadyExists) throw new Error(ERROR_VERTICAL_PASSWORD_ALREADY_EXISTS);
const updatedPasswords = [ ...strapiUser.verticalsUserPasswords, { vertical, verticalPassword, originWps: originWps || false, }, ];
return await update( USER, { documentId: strapiUser.documentId }, { ...userData, verticalsUserPasswords: updatedPasswords, } ); } else { return await update( USER, { documentId: strapiUser.documentId }, { ...userData, verticalsUserPasswords: [ { vertical, verticalPassword, originWps: originWps || false }, ], } ); } } catch (error) { console.error(error); throw new Error(error); }}Esta función recibe como parámetros el email del usuario, la nueva verticalPassword, el nombre de la vertical y opcionalmente una copia actualizada de los datos del usuario (userData), además de opcionalmente originWps, en donde si es true significa que viene migrado de wordpress.
Su propósito es mantener un sistema donde un mismo usuario pueda tener diferentes contraseñas por cada vertical en la que se registra.
- La función comienza validando que tanto
emailcomoverticalhayan sido proporcionados. Si alguno falta, lanza un error específico. - A continuación, localiza al usuario en la base de datos. Si no se encuentra, lanza un error informando que el usuario no existe.
- Si el usuario tiene un arreglo
verticalsUserPasswordsya definido, se verifica si dentro de ese arreglo ya existe una entrada con la vertical solicitada. Si ya existe, lanza un error para evitar duplicidad. - Si no existe la contraseña para esa vertical, se agrega una nueva entrada al arreglo.
- Finalmente, se actualiza el documento del usuario utilizando
updateDocument, lo que guarda tanto la nueva contraseña por vertical como cualquier otro dato del usuario que se haya incluido en la petición original.
🛠️ Función: updateVerticalPassword
Section titled “🛠️ Función: updateVerticalPassword”Esta función permite actualizar una contraseña existente para una vertical específica. Es útil cuando un usuario necesita modificar su clave en un entorno particular, sin afectar otras verticales.
export async function updateVerticalPassword( email: string, vertical: Verticals, verticalPassword: string) { try { if (!email) throw new Error(ERROR_EMAIL_REQUIRED); if (!vertical) throw new Error(ERROR_VERTICAL_REQUIRED); if (!verticalPassword) throw new Error(ERROR_PASSWORD_REQUIRED);
const strapiUser = await findOne( USER, FIELDS_USER, { email: email }, COMPONENT_NAME_USER_PASSWORDS );
if (strapiUser === NOT_FOUND) return ERROR_USER_NOT_FOUND;
if ( !Array.isArray(strapiUser.verticalsUserPasswords) || strapiUser.verticalsUserPasswords.length === 0 ) { throw new Error(ERROR_PASSWORD_NOT_FOUND); }
const existingPassword = strapiUser.verticalsUserPasswords.find( (pass) => pass.vertical === vertical ); if (!existingPassword) throw new Error(ERROR_VERTICAL_PASSWORD_NOT_FOUND);
const updatedPasswords = strapiUser.verticalsUserPasswords.map((pass) => { if (pass.id === existingPassword.id) { return { vertical: pass.vertical, verticalPassword: verticalPassword, originWps: false, }; } return pass; });
return await update( USER, { documentId: strapiUser.documentId }, { verticalsUserPasswords: updatedPasswords, } ); } catch (error) { console.error(error); throw new Error(error); }}🔍 Lógica de funcionamiento
Section titled “🔍 Lógica de funcionamiento”- Se valida la existencia de los tres parámetros necesarios:
email,verticalyverticalPassword. Sin alguno de ellos, se lanza un error. - Se busca al usuario por
email, junto con el componenteverticalsUserPasswords. - Se verifica que exista al menos una contraseña registrada.
- Se localiza la contraseña correspondiente a la
verticalindicada. - Si existe, se procede a actualizarla con el nuevo valor.
Esta función es fundamental para permitir que los usuarios puedan cambiar sus contraseñas de forma controlada, sin crear entradas duplicadas.
⚠️ Cabe aclarar que para actualizar la contraseña, se debe utilizar esta API.