🔑 Strapi Passwords
🧩 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
{ "collectionName": "components_verticals_passwords_verticals_passwords_s", "info": { "displayName": "verticalsUserPasswords", "description": "" }, "options": {}, "attributes": { "vertical": { "type": "enumeration", "enum": [ "Cirugía", "Edificio Hospital", "Emergencias", "Inspiria", "Oncología", "Pharma", "Salud Mental", "Traumatología", "UNIMED" ], "required": true }, "verticalPassword": { "type": "password", "required": false, "private": true } }}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.
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
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
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;
const ctx = strapi.requestContext.get(); const { origin } = ctx.request.body; if (origin?.toLowerCase() === USER_ORIGIN_WORDPRESS) return;
if (!data) throwValidationError( USER_FIELD_PARAMS, ERROR_PASSWORD_COMPONENT_NOT_FOUND );
if (!data.id) { if (!data.vertical) throwValidationError(USER_FIELD_VERTICAL, VERTICAL_REQUIRED);
if (!data.verticalPassword) throwValidationError( USER_FIELD_VERTICALPASSWORD, ERROR_PASSWORD_REQUIRED );
const encryptedPassword = await hashPassword(data.verticalPassword); data.verticalPassword = encryptedPassword; } },
async beforeUpdate(event) { const { data } = event.params;
const ctx = strapi.requestContext.get(); const { origin } = ctx.request.body; if (origin?.toLowerCase() === USER_ORIGIN_WORDPRESS) return;
if (!data) throwValidationError( USER_FIELD_PARAMS, ERROR_PASSWORD_COMPONENT_NOT_FOUND );
if (!data.id) { if (!data.vertical) throwValidationError(USER_FIELD_VERTICAL, VERTICAL_REQUIRED);
if (!data.verticalPassword) throwValidationError( USER_FIELD_VERTICALPASSWORD, ERROR_PASSWORD_REQUIRED );
const encryptedPassword = await hashPassword(data.verticalPassword); data.verticalPassword = encryptedPassword; } },});Este lifecycle garantiza que:
- Solo se cree una contraseña cuando el
originno sea WordPress. - Se validen los campos obligatorios (
vertical,verticalPassword). - La contraseña sea cifrada con
bcryptantes de ser almacenada.
⚠️ NOTA IMPORTANTE ⚠️
- SOLO SE HASHEA LA CONTRASEÑA CUANDO ESTA CREANDOSE! SI SE ACTUALIZA SE DEBE QUITAR EL ID PARA QUE ACTUALICE EL ELEMENTO AL ELIMINAR EL ANTERIOR, SINO DEBE HASHEARSE MANUALMENTE! De esa manera lo hace updateVerticalPassword mas abajo.
⚠️ Se incluyeron las siguientes funciones para el manejo del componente:
🛠️ Función: syncPasswordWithVerticals
export async function syncPasswordWithVerticals( email: string, verticalPassword: string | undefined, vertical: Verticals, userData?: UserProfileUpdateData) { 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, }, ];
return await updateDocument(USER, strapiUser.documentId, { ...userData, verticalsUserPasswords: updatedPasswords, }); } else { return await updateDocument(USER, strapiUser.documentId, { ...userData, verticalsUserPasswords: [{ vertical, verticalPassword }], }); } } 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). 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
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, }; } return pass; });
return await updateDocument(USER, strapiUser.documentId, { verticalsUserPasswords: updatedPasswords, }); } catch (error) { console.error(error); throw new Error(error); }}🔍 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 acutalizar la contraseña, se debe utilizar esta API.