Skip to content

Strapi Passwords

Este componente personalizado en Strapi permite que cada usuario tenga una contraseña diferente por cada vertical o subsistema del producto.

{
"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.

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 originWps no sea true es decir sea de wordpress.
  • Se validen los campos obligatorios (vertical, verticalPassword).
  • La contraseña sea cifrada con bcrypt antes 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.

  1. La función comienza validando que tanto email como vertical hayan sido proporcionados. Si alguno falta, lanza un error específico.
  2. A continuación, localiza al usuario en la base de datos. Si no se encuentra, lanza un error informando que el usuario no existe.
  3. Si el usuario tiene un arreglo verticalsUserPasswords ya 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.
  4. Si no existe la contraseña para esa vertical, se agrega una nueva entrada al arreglo.
  5. 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.

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);
}
}
  1. Se valida la existencia de los tres parámetros necesarios: email, vertical y verticalPassword. Sin alguno de ellos, se lanza un error.
  2. Se busca al usuario por email, junto con el componente verticalsUserPasswords.
  3. Se verifica que exista al menos una contraseña registrada.
  4. Se localiza la contraseña correspondiente a la vertical indicada.
  5. 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.