✨ Servicio de Actualización de Datos de Usuario por Medio de API
La API update-user fue diseñada para permitir a los usuarios autenticados modificar su información de perfil de forma segura. Está pensada para actualizar campos específicos como:
- Número de teléfono (
phone) - Número de identificación (
identificationNumber) - Nombre completo (
nameLastName) - Dirección de envío (
mailingAddress) - Dirección de facturación (
billingAddress)
Esta API está protegida por autenticación JWT, lo que garantiza que solo los usuarios autenticados puedan modificar su información.
🔐 Autenticación
El acceso a esta API requiere un token JWT válido. Este token debe enviarse en el encabezado de la solicitud como:
Authorization: Bearer TU_TOKEN_JWTSi el token es inválido o no se proporciona, la API rechazará la solicitud.
🧠 Controlador
Este archivo contiene la lógica principal para el manejo de la actualización del perfil del usuario.
Ruta del archivo
src/api/user-endpoints/controllers/update-user.ts
Código del controlador
import { ERROR_UNAUTHORIZED, ERROR_UPDATE } from "../../../constants/errors";import "module-alias/register";import { findOne, update, updateDocument } from "../../../utils/strapi-queries";import { UserProfileUpdateData, BillingAddress, MailingAddress,} from "../../../type/user";import * as User from "../../../constants/structures/collections/user";
const validUserKeys = [ "phone", "identificationNumber", "nameLastName", "mailingAddress", "billingAddress",];const validBillingAddressKeys = [ "name", "company", "identificationNumber", "phone", "street", "city", "country", "state",];const validMailingAddressKeys = [ "name", "country", "phone", "street", "city", "zipCode", "identificationNumber", "state",];
function validateKeys<T extends Record<string, unknown>>( obj: T, validKeys: string[], context: string): void { if (!obj || typeof obj !== "object") return;
const invalidKeys = Object.keys(obj).filter( (key) => !validKeys.includes(key) );
if (invalidKeys.length > 0) { throw new Error( `Not Allowed fields at '${context}': ${invalidKeys.join(", ")}` ); }}
function validateRequestBody(body: UserProfileUpdateData) { validateKeys(body, validUserKeys, "root");
if (body.billingAddress && typeof body.billingAddress === "object") { validateKeys<BillingAddress>( body.billingAddress, validBillingAddressKeys, "billingAddress" ); }
if (body.mailingAddress && typeof body.mailingAddress === "object") { validateKeys<MailingAddress>( body.mailingAddress, validMailingAddressKeys, "mailingAddress" ); }}
export default { async updateProfile(ctx) { const recivedUser = ctx.state.user; validateRequestBody(ctx.request.body); if (!recivedUser) { return ctx.unauthorized(ERROR_UNAUTHORIZED); }
const { email } = recivedUser; const { phone, identificationNumber, nameLastName, mailingAddress, billingAddress, } = ctx.request.body;
const dataToUpdate: UserProfileUpdateData = {};
if (phone !== undefined) dataToUpdate.phone = phone; if (identificationNumber !== undefined) dataToUpdate.identificationNumber = identificationNumber; if (nameLastName !== undefined) dataToUpdate.nameLastName = nameLastName;
try { let userStrapi = await findOne( User.USER, User.FIELDS_USER, { email }, User.COMPONENTS_USER );
await update(User.USER, { email: email }, dataToUpdate);
if (billingAddress && typeof billingAddress === "object") { if (userStrapi.billingAddress) { await update( User.COMPONENT_USER_ADDRESS_BILLING, { id: userStrapi.billingAddress.id }, billingAddress ); } else { await updateDocument(User.USER, userStrapi.documentId, { billingAddress, }); } }
if (mailingAddress && typeof mailingAddress === "object") { if (userStrapi.mailingAddress) { await update( User.COMPONENT_USER_ADDRESS_MAILING, { id: userStrapi.mailingAddress.id }, mailingAddress ); } else { await updateDocument(User.USER, userStrapi.documentId, { mailingAddress, }); } }
const updatedUser = await findOne( User.USER, User.FIELDS_USER, { email }, User.COMPONENTS_USER );
return ctx.send(updatedUser); } catch (error) { return ctx.badRequest(error, ERROR_UPDATE); } },};🗞 Explicación del código
- Autenticación del usuario
const recivedUser = ctx.state.user;if (!recivedUser) { return ctx.unauthorized(ERROR_UNAUTHORIZED);}Esta línea verifica que el usuario esté autenticado. ctx.state.user solo estará presente si el JWT fue enviado correctamente y es válido. Si el usuario no está autenticado, se devuelve un error 401.
- Validación de Campos
validateRequestBody(ctx.request.body);Esta línea valida que el usuario no esté enviando campos indeseados a la API, cualquier campo no permitido regresará un error. vease en ✅ Validación de Claves Permitidas
- Extracción de datos de la solicitud
const { phone, identificationNumber, nameLastName, mailingAddress, billingAddress,} = ctx.request.body;Aquí se obtienen los datos enviados por el usuario que desea actualizar. Todos estos campos son opcionales.
- Construcción del objeto a actualizar
const dataToUpdate: UserProfileUpdateData = {};Se declara un objeto vacío que solo se llenará con los campos que realmente fueron enviados. Esto permite que el usuario actualice solo lo que desee.
- Consulta del usuario en Strapi
let userStrapi = await findOne( User.USER, User.FIELDS_USER, { email }, User.COMPONENTS_USER);Esta función busca el usuario actual en la base de datos de Strapi usando su correo electrónico. FIELDS_USER define los campos a recuperar y COMPONENTS_USER incluye los componentes (direcciones).
- Actualización de datos simples
await update(User.USER, { email }, dataToUpdate);Se actualizan los campos principales (como phone, nameLastName, etc.) usando la función update.
- Actualización o creación de direcciones
Las direcciones (billingAddress, mailingAddress) se tratan como componentes separados:
- Si el componente ya existe, se actualiza.
- Si no existe, se crea dentro del documento del usuario.
Este comportamiento se maneja con update y updateDocument.
- Obtención del usuario actualizado
const updatedUser = await findOne(...);Una vez finalizada la actualización, se consulta nuevamente el usuario para enviarlo como respuesta al frontend.
✅ Validación de Claves Permitidas
Para mantener la integridad y seguridad de los datos, la API implementa una validación explícita de las claves enviadas en el cuerpo (body) de la solicitud. Esto evita que se intenten modificar campos no autorizados.
🔒 ¿Cómo funciona?
Antes de que se realice cualquier operación de actualización en la base de datos, el controlador ejecuta una función llamada validateRequestBody, la cual:
-
Valida que los campos en el objeto raíz (
body) pertenezcan al conjunto permitido. -
Valida de forma independiente las claves dentro de
billingAddressymailingAddress. -
Si alguna clave no está permitida, lanza un error con un mensaje claro indicando el contexto (por ejemplo, “billingAddress”) y las claves inválidas.
🔑 Claves permitidas
Nivel raíz (UserProfileUpdateData)
const validUserKeys = [ "phone", "identificationNumber", "nameLastName", "mailingAddress", "billingAddress",];billingAddress
const validBillingAddressKeys = [ "name", "company", "identificationNumber", "phone", "street", "city", "country", "state",];mailingAddress
const validMailingAddressKeys = [ "name", "country", "phone", "street", "city", "zipCode", "identificationNumber", "state",];🔎 Validaciones de campos
Antes de actualizar cualquier dato, se realiza una validación estricta para asegurar que únicamente se procesen campos permitidos. Esto se logra mediante funciones auxiliares que comparan las claves recibidas con las definidas como válidas:
-
Campos válidos del usuario:
phone,identificationNumber,nameLastName,mailingAddress,billingAddress -
Campos válidos en billingAddress:
name,company,identificationNumber,phone,street,city,country,state -
Campos válidos en mailingAddress:
name,country,phone,street,city,zipCode,identificationNumber,state
Si se encuentra una clave no permitida, se lanza un error indicando exactamente qué campo es inválido y en qué parte del objeto ocurrió (por ejemplo, en billingAddress).
🧠 Código de Validación
Estas son las funciones utilizadas para aplicar la validación en cada parte de la solicitud:
🧩 Explicación de las funciones de validación
- validateRequestBody
Esta función es la encargada de coordinar la validación completa del cuerpo (body) de la solicitud antes de procesarla.
function validateRequestBody(body: UserProfileUpdateData) { validateKeys(body, validUserKeys, "root");
if (body.billingAddress && typeof body.billingAddress === "object") { validateKeys<BillingAddress>( body.billingAddress, validBillingAddressKeys, "billingAddress" ); }
if (body.mailingAddress && typeof body.mailingAddress === "object") { validateKeys<MailingAddress>( body.mailingAddress, validMailingAddressKeys, "mailingAddress" ); }}-
Llama a validateKeys sobre el cuerpo principal
(root)de la solicitud, usando las claves permitidas(validUserKeys). -
Si existe un
billingAddressy es un objeto, valida sus claves convalidBillingAddressKeys. -
Si existe un
mailingAddressy es un objeto, valida sus claves convalidMailingAddressKeys.
📌 Esta función asegura que ningún campo inesperado sea procesado por la API, y es el primer filtro de seguridad antes de actualizar los datos del usuario.
- validateKeys
Esta función genérica (<T>) valida que un objeto no contenga propiedades no permitidas. Se utiliza tanto en el nivel raíz como dentro de billingAddress y mailingAddress.
function validateKeys<T extends Record<string, unknown>>( obj: T, validKeys: string[], context: string): void { if (!obj || typeof obj !== "object") return;
const invalidKeys = Object.keys(obj).filter( (key) => !validKeys.includes(key) );
if (invalidKeys.length > 0) { throw new Error( `Not Allowed fields at '${context}': ${invalidKeys.join(", ")}` ); }}- Parámetros:
obj: el objeto que se quiere validar (por ejemplo, ctx.request.body, billingAddress, etc.).
validKeys: un arreglo con las claves permitidas para ese contexto.
context: un string que representa el nombre del objeto (solo se usa para indicar en el mensaje de error dónde ocurrió el problema).
- Funcionamiento:
Verifica que obj sea un objeto válido.
Obtiene todas las claves (Object.keys(obj)) y filtra las que no están en validKeys.
Si hay claves no válidas, lanza un error indicando exactamente cuáles son y en qué contexto se encuentran.
⚠️ Esto ayuda a garantizar que la API no procese datos inesperados o no controlados, brindando mayor seguridad y trazabilidad.
🧹 Estructura de Datos
📌 Importante: La validación se realiza antes de ejecutar cualquier actualización. Si se detectan campos no válidos en el nivel raíz o dentro de billingAddress y mailingAddress, se lanza un error 500 Bad Request con el detalle de los campos no permitidos.
Nota: Solo se podrán modificar los datos que estén explícitamente declarados en esta sección. Cualquier campo que no esté definido en los TYPES será ignorado por la API, incluso si se incluye en la solicitud, es decir, no envia el error por que es permitido, pero si no se define en los tipos no lo actualiza, son 2 validaciones.
Estructura del cuerpo (body) de la solicitud
export type UserProfileUpdateData = { phone?: string; identificationNumber?: string; nameLastName?: string; mailingAddress?: MailingAddress; billingAddress?: BillingAddress;};mailingAddress
export type MailingAddress = { name?: string; country?: string; phone?: string; street?: string; city?: string; zipCode?: string; identificationNumber?: string; state?: string;};billingAddress
export type BillingAddress = { name?: string; company?: string; identificationNumber?: string; phone?: string; street?: string; city?: string; country?: string; state?: string;};⚙️ Configuración de la Ruta
Archivo: user-endpoints/routes/update-user.ts
export default { routes: [ { method: "PUT", path: "/user-endpoints/profile", handler: "update-user.updateProfile", }, ],};Explicación
- Método:
PUT, porque se están actualizando datos existentes. - Ruta:
/user-endpoints/profileindica que se trata de una actualización de perfil. - Handler:
'update-user.updateProfile'llama a la función definida en el controlador.
🧪 Pruebas y Uso de la API
1. Autenticación
Si no tienes un token JWT, realiza una solicitud POST al endpoint /auth/local con tus credenciales (para esto deberás haber creado un usuario autenticado previamente):
- URL:
/api/auth/local - Método:
POST - Headers:
Content-Type:application/json
Body:
{ "identifier": "tu_correo@example.com", "password": "tu_contraseña"}Recibirás un token JWT válido en la respuesta.
2. Solicitud de actualización
- URL:
/api/user-endpoints/profile - Método:
PUT - Headers:
Authorization:Bearer TU_TOKEN_JWTContent-Type:application/json
Ejemplo de body (JSON)
{ "phone": "123456789", "nameLastName": "Fernando Rodríguez", "mailingAddress": { "name": "Fernando", "street": "Calle del Lago 45", "city": "Morelia", "country": "México", "zipCode": "58000", "phone": "123456789" }, "billingAddress": { "name": "Fernando", "company": "DevTech S.A.", "identificationNumber": "RFC123456", "street": "Calle del Sol 99", "city": "Morelia", "country": "México", "phone": "123456789" }}📤 Respuestas de la API
✅ 1. Éxito (200 OK)
{ "id": 1, "email": "fernando@email.com", "nameLastName": "Fernando Rodríguez", "phone": "123456789", "billingAddress": { ... }, "mailingAddress": { ... }}La respuesta incluirá todos los datos actualizados del usuario.
❌ 2. Error de Autenticación (401 Unauthorized)
{ "data": null, "error": { "status": 401, "name": "UnauthorizedError", "message": "Not authorized to perform this action", "details": {} }}Este error se produce si no se proporciona un token JWT o si el token es inválido o ha expirado.
❌ 3. Error de Validación o Interno (400 Bad Request)
{ "data": null, "error": { "status": 400, "name": "BadRequestError", "message": "Error updating profile", "details": {} }}Este error se devuelve si ocurre algún problema durante la actualización de los datos, por ejemplo, si los datos enviados no cumplen con el formato esperado.
📛 4. Error de Datos Invalidos enviados
Si un usuario intenta enviar una clave no permitida, como username en el nivel raíz, o postalCode dentro de billingAddress, se retornará una respuesta de error como esta:
{ "data": null, "error": { "status": 500, "name": "InternalServerError", "message": "Internal Server Error" }}En la consola del Servidor se mostrará que campos son los no permitidos.