Notificaciones con Socket.IO
Este código implementa un sistema de notificaciones en tiempo real utilizando Socket.IO. Permite a los usuarios autenticados marcar las notificaciones como leídas, gestionando las relaciones entre usuarios y notificaciones cuando se emite la acción SOCKET_NOTIFICATION_READ. El código asegura la autenticación mediante tokens JWT y maneja la creación o actualización de relaciones de las notificaciones leídas en la colección READNOTIFICATION.
Dependencias
Socket.IO: Biblioteca basada en WebSockets que facilita la comunicación bidireccional y en tiempo real entre clientes y servidores. Permite la emisión y escucha de eventos personalizados, la gestión de salas (rooms), así como el manejo eficiente de conexiones con baja latencia y tolerancia a desconexiones.
Archivo SocketIO.ts
Funciones definidas
Función para registrar a Strapi de un error en los logs.
function logCatchError(error, userId, notificationId, context) { const errMsg = error instanceof Error ? error.message : String(error);
const messages = `Error while ${context} with user ID: ${userId} and notification ID: ${notificationId}: ${errMsg}`; strapi.log.error(messages);}Ejemplo del mensaje por consola:
[2025-06-02 08:47:06.137] error: ejemplo del mensaje userId: 5Función para encontrar a la notificación.
async function findNotification(notificationId) { return await findOne( NOTIFICATION, null, { id: notificationId }, null, PUBLISHED );}Configuración cors
Configuración que permite conexiones desde cualquier dominio, esta configuración es útil en entornos de desarrollo, pero debe reemplazarse en producción por una lista específica de dominios autorizados para reforzar la seguridad
export const io = new SocketIOServer(strapi.server.httpServer, { cors: { origin: "*", methods: ["GET", "POST"], },});Configuración autenticación
Desde el frontend se envia el token
El código envía un objeto JSON en el cuerpo de la solicitud con los campos identifier (que puede ser el email o el nombre de usuario) y password. Si las credenciales son correctas, Strapi responde con un JWT y los datos del usuario autenticado (código de ejemplo).
const response = await fetch("http://localhost:1337/api/auth/local", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ identifier: email, password: password, }),});
const data = await response.json();if (data.jwt) { jwt = data.jwt;}Desde el backend se recibe el token
Este código implementa un middleware de autenticación para SocketIO, encargado de validar la identidad de los usuarios mediante la verificación del token JWT recibido desde el frontend, antes de permitir el establecimiento de la conexión con el servidor de sockets.
io.use(async (socket, next) => { const token = socket.handshake.auth.token;
if (!token) { return next(new Error(error.ERROR_SOCKETIO_TOKEN)); }
try { const decoded = verify( token, strapi.config.get(USER_JWT_USERS_PLUGIN) ) as JwtPayload; if (decoded) { return; } next(); } catch (error) { next(new Error(`${error.ERROR_ON_AUTHENTICATED}: ${error.message}`)); }});Configuración notificaciones leídas
configuración de socketIO.ts en strapi-backend
Se establece un listener en el servidor de socket. Las constantes de dichos eventos se encuentran en ../src/constants/structures/api/socketIO.ts
io.on(SOCKET_CONNECTION, socket);Obtiene el identificador (id) del usuario enviado desde el frontend y lo asigna a una sala específica en el servidor de sockets, garantizando que la notificación se dirija exclusivamente al usuario correspondiente al ID recibido.
const userId = socket.handshake.query.userId;socket.join(SOCKET_USER_ROOM + userId);Desde el frontend, se debe emitir un evento que será recibido y gestionado por Strapi
socket.on(SOCKET_NOTIFICATION_READ, async(notificationData));El evento incluye el ID de la notificación y, utilizando este identificador (id) de la notificación recibida, se realiza una consulta en la colección READNOTIFICATION para verificar si existe una relación entre ambos.
const notification = await findNotification(notificationData.notificationId) as Notification;
const existingRelation = await findMany( READNOTIFICATION, null, { notification: { documentId: notification.documentId }, users_permissions_users: { id: userId } }, READNOTIFICATION_COMPONENTS);Se comprueba si ya existe una relación en READNOTIFICATION. si existe, se detiene la ejecución con un return.
const isExistingRelation = existingRelation === NOTHING_FOUND ? [] : existingRelation as ReadNotification[];if (isExistingRelation && isExistingRelation.length > 0) { return;}En caso de que no exista una relación en READNOTIFICATION para el usuario, se buscará si existe la relación de la notificación con alguno otro usuario creado.
const allRelationsForNotification = await findMany( READNOTIFICATION, null, { notification: { documentId: notification.documentId } }, READNOTIFICATION_COMPONENTS) as ReadNotification[] | string;En caso de que exista se actualizará la relación con el nuevo usuario.
if (isAllRelationsForNotification && isAllRelationsForNotification.length > 0) { try { await updateDocument( READNOTIFICATION, isAllRelationsForNotification[0].documentId, { users_permissions_users: { connect: [{ id: userId }] } }, PUBLISHED ); } catch (error) { logCatchError( error, userId, isAllRelationsForNotification[0].documentId, error.ERROR_ADD_USER_TO_NOTIFICATION ); }
return;}Si no existe ninguna relación previa, se creará una nueva relación en READNOTIFICATION vinculando la notificación con el usuario correspondiente.
try { await create( READNOTIFICATION, { notification: { connect: [{ documentId: notification.documentId }] }, users_permissions_users: { connect: [{ id: userId }] }, }, PUBLISHED );} catch (error) { logCatchError( error, userId, notification.documentId, error.ERROR_CREATE_USER_TO_NOTIFICATION_RELATION );}En el lifecycle de Strapi, se importa para que se ejecute durante la fase de bootstrap al iniciar el servidor.
import socketIOService from './middlewares/socketIO';async bootstrap({strapi}) { socketIOService({strapi});},Archivo Lifecycles.ts
Lifecycle afterCreate
Estos son los datos que se envían al frontend.
const notificationData = { mensaje: result.message, id: result.id, url: result.url || null, document: result.document, title: result.date || null,};Este es el evento que envía a cada usuario seleccionado.
if (result.publishedAt) { const users = event.params.data.notification_users.set ? event.params.data.notification_users.set : event.params.data.notification_users;
if (users) { users.map((user) => { const userid = user.id ? user.id : user; io.to(SOCKET_USER_ROOM + userid).emit( SOCKET_NOTIFICATION, notificationData ); }); }}Lifecycle beforeUpdate
Obtener los datos de event y el ctx para obtener el id.
const { data } = event.params;const ctx = strapi.requestContext.get();const users = await findUsers();Buscar la notificación y añadir los usuarios no seleccionados, además de actualizar el botón a true o false.
const notification = await findOne( NOTIFICATION, null, { documentId: ctx.params.id }, NOTIFICATION_COMPONENTS);
const totalConnected = getConnectedUsers(ctx, notification);
data.allUsers = totalConnected === users.length;
if ( ctx.request.body.allUsers && totalConnected < users.length && ctx.request.body.notification_users.disconnect.length === 0) { selectUnselectedUsers(users, data, notification);}Lifecycle beforeCreate
Al crear la notificación si el botón está en true seleccionar a todos los usuarios.
const { data } = event.params;const ctx = strapi.requestContext.get();const users = await findUsers();
if (ctx.request.body.allUsers) { selectAllUsers(users, data);}(Ejemplo de uso frontend)
- Se hace la petición a la API de strapi incluyendo el JWT para acceder a la
información.
const response = await fetch("http://localhost:1337/api/users/me", { headers: { Authorization: `Bearer ${jwt}`, },});- Se verifica que la
autenticaciónhaya sido exitosa.
if (response.ok) { const data = await response.json(); socket = io("http://localhost:1337", { auth: { token: jwt }, query: { userId: data.id }, });}- Se inicializa socketIO utilizando el token
JWTpara la autenticación.
socket = io(strapiBaseUrl, { auth: { token: jwt }, query: { userId: id },});- El socket de Strapi envía en la información el
idde lanotificaciónque se utilizará en el frontend para identificar a qué notificación se ha hechoclick(SOCKET_NOTIFICATIONes el evento que se emite en el back).
socket.on(SOCKET_NOTIFICATION, (data) => { const notifDiv = document.createElement("div"); notifDiv.className = "notification"; notifDiv.setAttribute("data-id", data.id);});- Se emite un evento que se recibirá en Strapi, el evento enviado incluye una propiedad llamada
notificationIdque previamente se asigno a la notificación correspondiente.
notificationsContainer.addEventListener("click", (event) => { const notifElement = event.target.closest(".notification");
if (!notifElement) return; /* <--- Si no se hizo clic en una notificación, salimos */ const notificationId = notifElement.dataset.id;
socket.emit(SOCKET_NOTIFICATION_READ, { notificationId, }); /* <-- Emitir al servidor */});Estructuras
Estructura SocketIO
En ../src/constants/structures/api/socketIO.ts se encuentra el conjunto de constantes para la gestión de eventos del socket.
export const SOCKET_CONNECTION = "connection";export const SOCKET_NOTIFICATION = "notificacion";export const SOCKET_USER_ROOM = "user-";export const SOCKET_NOTIFICATION_READ = "notification:read";