diff --git a/src/Files/chat-background-oscuro.png b/src/Files/chat-background-oscuro.png new file mode 100644 index 0000000..3973384 Binary files /dev/null and b/src/Files/chat-background-oscuro.png differ diff --git a/src/Files/chat-background.png b/src/Files/chat-background.png new file mode 100644 index 0000000..12be842 Binary files /dev/null and b/src/Files/chat-background.png differ diff --git a/src/Files/grupo.png b/src/Files/grupo.png new file mode 100644 index 0000000..da8e73f Binary files /dev/null and b/src/Files/grupo.png differ diff --git a/src/Files/logo-completo.png b/src/Files/logo-completo.png new file mode 100644 index 0000000..ef086ef Binary files /dev/null and b/src/Files/logo-completo.png differ diff --git a/src/Files/sin-foto.png b/src/Files/sin-foto.png new file mode 100644 index 0000000..f2c2694 Binary files /dev/null and b/src/Files/sin-foto.png differ diff --git a/src/Images/1685569022245-972273.png b/src/Images/1685569022245-972273.png new file mode 100644 index 0000000..7cd3529 Binary files /dev/null and b/src/Images/1685569022245-972273.png differ diff --git a/src/Images/1685569067299-100807.png b/src/Images/1685569067299-100807.png new file mode 100644 index 0000000..ca6318b Binary files /dev/null and b/src/Images/1685569067299-100807.png differ diff --git a/src/Images/1685582039100-341259.png b/src/Images/1685582039100-341259.png new file mode 100644 index 0000000..e6235db Binary files /dev/null and b/src/Images/1685582039100-341259.png differ diff --git a/src/db.js b/src/db.js new file mode 100644 index 0000000..95541a6 --- /dev/null +++ b/src/db.js @@ -0,0 +1,316 @@ +const mongoose = require("mongoose"); +const ObjectId = mongoose.Types.ObjectId; +const uri = "mongodb+srv://Mencoreh:RRyKUSz33lqXgkGm@cluster0.emhsybu.mongodb.net/ChatApp?retryWrites=true&w=majority"; +//CONEXIÓN A MONGODB +mongoose + .connect(uri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }) + .then(() => { + console.log("La conexión a MongoDB fue exitosa."); + }) + .catch((e) => { + console.log("La conexión a MongoDB falló.", e); + }); + +///////////////USER SCHEMA Y MODEL/////////////// +const userSchema = mongoose.Schema( + { + email: { + type: String, + }, + nickname: { + type: String, + }, + passwordHash: { + type: String, + }, + photo: { + type: String, + }, + }, + { versionKey: false } +); + +const UserModel = mongoose.model("users", userSchema, "Users"); + +///////////////CHANNEL SCHEMA Y MODEL/////////////// +const channelSchema = mongoose.Schema( + { + name: { + type: String, + required: true, + }, + description: { + type: String, + default: "Grupo de ChatApp", + }, + photo: { + type: String, + }, + participants: { + type: Array, + }, + }, + { versionKey: false } +); + +const ChannelModel = mongoose.model("channel", channelSchema, "Channels"); + +///////////////MESSAGE SCHEMA Y MODEL/////////////// +const messageSchema = mongoose.Schema( + { + channel: { + type: String, + required: true, + }, + author: { + type: String, + required: true, + }, + content: { + type: String, + required: true, + }, + date: { + type: Date, + required: true, + }, + }, + { versionKey: false } +); + +const MessageModel = mongoose.model("messages", messageSchema, "Messages"); + +////////////FUNCIONES//////////// + +/** + * Crea un nuevo usuario. + * @param {string} nickname - Nombre de usuario. + * @param {string} email - Correo electrónico del usuario. + * @param {string} passwordHash - Hash de la contraseña del usuario. + * @param {string} profilePicture - URL de la imagen de perfil del usuario. + * @returns {Promise} - Promesa que resuelve en un objeto con los datos del usuario creado. + */ +async function createUser(nickname, email, passwordHash, profilePicture) { + const usuario = new UserModel({ + nickname: nickname, + email: email, + passwordHash: passwordHash, + profilePicture: profilePicture, + }); + const resultado = await usuario.save(); + return resultado; +} + +/** + * Encuentra un usuario por un dato específico. + * @param {string} key - Clave para buscar el usuario (por ejemplo, "_id", "nickname", "email"). + * @param {string} value - Valor para buscar el usuario. + * @returns {Promise} - Promesa que resuelve en un objeto con los datos del usuario encontrado, o `null` si no se encuentra ningún usuario. + */ +async function findUser(key, value) { + const resultado = await UserModel.findOne({ [key]: value }); + return resultado; +} + +/** + * Encuentra todos los usuarios. + * @returns {Promise} - Promesa que resuelve en un arreglo de objetos con los datos de todos los usuarios. + */ +async function findAllUsers() { + const resultado = await UserModel.find(); + return resultado; +} + +/** + * Actualiza los datos de un usuario en la base de datos. + * + * @param {string} id - El id del usuario a actualizar. + * @param {object} data - Un objeto con las keys y valores a actualizar en el modelo UserModel. + * @returns {Promise} - Una promesa que se resuelve con el resultado de la operación de actualización. + */ +async function updateUser(id, updates) { + const idObject = new ObjectId(id); + const resultado = await UserModel.updateOne( + { _id: idObject }, + { + $set: updates, + } + ); + return resultado; +} + +/** + * Elimina un usuario por un dato específico. + * @param {string} key - Clave para buscar el usuario a eliminar (por ejemplo, "_id", "nickname", "email"). + * @param {string} value - Valor para buscar el usuario a eliminar. + * @returns {Promise} - Promesa que resuelve en un objeto con los detalles de la eliminación. + */ +async function deleteUser(key, value) { + const resultado = await UserModel.deleteOne({ [key]: value }); + return resultado; +} + +/** + * Crea un nuevo canal. + * @param {string} name - Nombre del canal. + * @param {string} description - Descripción del canal. + * @param {string} photo - URL de la foto del canal. + * @param {Array} participants - Participantes del canal. + * @param {Array} messages - Mensajes del canal. + * @returns {Promise} - Promesa que resuelve en un objeto con los datos del canal creado. + */ +async function createChannel(name, description, photo, participants, messages) { + const channel = new ChannelModel({ + name: name, + description: description, + photo: photo, + participants: participants, + messages: messages, + }); + const resultado = await channel.save(); + return resultado; +} + +/** + * Encuentra un canal por un dato específico. + * @param {string} key - Clave para buscar el canal (por ejemplo, "_id", "name", "description"). + * @param {string} value - Valor para buscar el canal. + * @returns {Promise} - Promesa que resuelve en un objeto con los datos del canal encontrado, o `null` si no se encuentra ningún canal. + */ +async function findChannel(key, value) { + const resultado = await ChannelModel.findOne({ [key]: value }); + return resultado; +} + +/** + * Encuentra un canal por un dato específico, sin incluir los mensajes. + * @param {string} key - Clave para buscar el canal (por ejemplo, "_id", "name", "description"). + * @param {string} value - Valor para buscar el canal. + * @returns {Promise} - Promesa que resuelve en un objeto con los datos del canal encontrado (sin incluir los mensajes), o `null` si no se encuentra ningún canal. + */ +async function findChannelWM(key, value) { + const resultado = await ChannelModel.findOne({ [key]: value }, { messages: 0 }); + return resultado; +} + +/** + * Encuentra todos los canales en los que el usuario con una ID es participante. + * @param {string} userId - ID del usuario. + * @returns {Promise} - Promesa que resuelve en un arreglo de objetos con los datos de todos los canales en los que el usuario es participante. + */ +async function findChannels(userId) { + const idObject = new ObjectId(userId); + const resultado = await ChannelModel.find({ participants: idObject }); + return resultado; +} + +/** + * Encuentra todos los canales. + * @returns {Promise} - Promesa que resuelve en un arreglo de objetos con los datos de todos los canales. + */ +async function findAllChannels() { + const resultado = await ChannelModel.find(); + return resultado; +} + +/** + * Actualiza los datos de un canal por su ID. + * @param {string} id - ID del canal a actualizar. + * @param {string} key - Clave del dato a actualizar (por ejemplo, "name", "description", "photo"). + * @param {any} value - Nuevo valor del dato a actualizar. + * @returns {Promise} - Promesa que resuelve en un objeto con los detalles de la actualización. + */ +async function updateChannel(id, key, value) { + const idObject = new ObjectId(id); + const resultado = await ChannelModel.updateOne( + { _id: idObject }, + { + $set: { + [key]: value, + }, + } + ); + return resultado; +} + +/** + * Elimina un canal por un dato específico. + * @param {string} key - Clave para buscar el canal a eliminar (por ejemplo, "_id", "name", "description"). + * @param {string} value - Valor para buscar el canal a eliminar. + * @returns {Promise} - Promesa que resuelve en un objeto con los detalles de la eliminación. + */ +async function deleteChannel(key, value) { + const resultado = await ChannelModel.deleteOne({ [key]: value }); + return resultado; +} + +/** + * Crea un nuevo mensaje en un canal especificado. + * + * @param {string} channelID - El ID del canal donde se quiere crear el mensaje. + * @param {string} authorID - El ID del autor del mensaje. + * @param {string} content - El contenido del mensaje. + * @returns {Promise} - Una promesa que devuelve el documento del mensaje creado. + */ +async function createMessage(channelID, authorID, content) { + const message = new MessageModel({ + channel: channelID, + author: authorID, + content: content, + date: new Date(), + }); + const resultado = await message.save(); + return resultado; +} + +/** + * Obtiene los mensajes de un canal específico. + * + * @param {string} channelId - El ID del canal del cual se obtendrán los mensajes. + * @param {number} [limit=20] - El número máximo de mensajes a devolver. + * @param {number} [offset=0] - El número de mensajes a omitir. + * @returns {Promise<{ channelId: string, messages: { author: string, content: string, date: Date }[] }>} - Un objeto que contiene los últimos mensajes para el canal especificado. + */ +async function getMessages(channelId, limit = 20, offset = 0) { + try { + const messages = await MessageModel.find({ channel: channelId }) + .sort({ date: -1 }) // Ordena los mensajes por fecha en orden descendente (los más recientes primero) + .skip(offset) + .limit(limit) + .exec(); + + const result = { + channelId: channelId, + messages: messages.map((message) => ({ + id: message._id, + author: message.author, + content: message.content, + date: message.date + })), + }; + + return result; + } catch (error) { + console.error(`Error al obtener los mensajes en el canal con la id ${channelId}`) + } +} + +module.exports = { + createUser, + findUser, + findAllUsers, + updateUser, + deleteUser, + createChannel, + findChannel, + findChannelWM, + findChannels, + findAllChannels, + updateChannel, + deleteChannel, + createMessage, + getMessages +}; \ No newline at end of file diff --git a/src/errorHandler.js b/src/errorHandler.js new file mode 100644 index 0000000..130aaee --- /dev/null +++ b/src/errorHandler.js @@ -0,0 +1,11 @@ +process.on("unhandledRejection", (reason) => { + console.error(`${reason.stack}`); +}); +process.on("uncaughtException", (error) => { + console.error(`${error.stack}`); +}); +process.on("uncaughtExceptionMonitor", (error) => { + console.error(`${error.stack}`); +}); + +console.log("Error handler funcionando.") \ No newline at end of file diff --git a/src/functions.js b/src/functions.js new file mode 100644 index 0000000..be6f2c9 --- /dev/null +++ b/src/functions.js @@ -0,0 +1,78 @@ +const db = require("./db.js"); +const fs = require("fs"); + +/** + * Middleware para redirigir a /chat en el caso de estar logeado + * @param {object} req - Objeto de solicitud HTTP. + * @param {object} res - Objeto de respuesta HTTP. + * @param {function} next - Siguiente función de middleware. + */ +function requireLogin(req, res, next) { + if (req.session && req.session.user) { + res.redirect("/chat"); + } else { + next(); + } +} + +/** + * Middleware para redirigir a /login en el caso de no estar logeado + * @param {object} req - Objeto de solicitud HTTP. + * @param {object} res - Objeto de respuesta HTTP. + * @param {function} next - Siguiente función de middleware. + */ +function requireLogout(req, res, next) { + if (req.session && req.session.user) { + next(); + } else { + res.redirect("/login"); + } +} + +/** + * Transforma los datos de los canales para darselos al cliente. + * @param {Array} channels - El array de objetos de canales a transformar. + * @returns {Promise>} El array de canales objetos de los canales modificados con los apodos de los participantes actualizados y la foto del canal en base64. + */ +async function parseChannelsData(channels) { + for (let channel of channels) { + for (let i = 0; i < channel.participants.length; i++) { + let user = await db.findUser("_id", channel.participants[i]); + channel.participants[i] = user.nickname; + } + const filePath = `${__dirname}/${channel.photo}`; + if (channel.photo && fs.existsSync(filePath)) { + let img = fs.readFileSync(filePath); + const base64Img = img.toString("base64"); + channel.photo = `data:image/png;base64,${base64Img}`; + } else channel.photo = "../Files/grupo.png" + } + return channels; +} + +/** + * Verifica si un socket está en un canal específico. + * @param {string} channel - ID del canal. + * @param {object} socket - Objeto de socket. + * @returns {Promise} - Promesa que resuelve en `true` si el socket está en el canal, o `false` en caso contrario. + */ +async function isSocketInChannel(channel, socket) { + try { + let requester = socket.request.session.user._id; + let query = await db.findChannelWM("_id", channel); + if (query.participants.includes(requester)) { + return true; + } else { + return false; + } + } catch (error) { + return false; + } +} + +module.exports = { + requireLogin, + requireLogout, + parseChannelsData, + isSocketInChannel, +}; \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..931e9db --- /dev/null +++ b/src/index.js @@ -0,0 +1,113 @@ +// Inicializar el servidor express y requerir dependencias +const config = require("../config.json") +const express = require("express"); +const app = express(); +const path = require("path"); +const fs = require("fs"); +const db = require("./db.js"); +const bcrypt = require("bcrypt"); +const session = require("express-session"); +const { requireLogin, requireLogout, parseChannelsData, isSocketInChannel } = require("./functions.js"); +const SocketIO = require("socket.io"); + +// Usar express-sesion para manejar las sesiones +const sessionMiddleware = session({ + secret: config.sessionSecret, + resave: false, + saveUninitialized: true, +}); + +app.use(sessionMiddleware); + +// Establecer las rutas base para los archivos estáticos +app.use("/Files", express.static(path.join(__dirname, "Files"))); +app.use(express.static(path.join(__dirname, 'public'))); + +// Parsear los datos de las solicitudes POST. +app.use(express.urlencoded({ extended: true })); +app.use(express.json({limit: "50mb"})); + +// Configuración de EJS como motor de plantillas +app.set("view engine", "ejs"); +app.set("views", path.join(__dirname, "views")); + +// Inicializar el servidor y establecer Socket.IO +const server = app.listen(config.port, "0.0.0.0", () => { + console.log(`Servidor Express funcionando en http://0.0.0.0:${config.port}`); +}); +const io = SocketIO(server); + +// Rutas +require('./routes/chat')(app, path, requireLogout); +require('./routes/createChannel')(app, db, parseChannelsData, fs, path, io); +require('./routes/editUser')(app, db, bcrypt, path, fs) +require('./routes/getUser')(app, db, fs, path); +require('./routes/getMessages')(app, db); +require('./routes/login')(app, db, bcrypt, requireLogin, path); +require('./routes/logout')(app); +require('./routes/register')(app, db, bcrypt, requireLogin, path); + +// Definir la ruta principal +app.get("/", (req, res) => { + res.redirect("/login") +}); + +//Redefinir las funciones console.log y console.error +const logPath = path.join(__dirname, "logs", "console.log"); +const logStream = fs.createWriteStream(logPath, { flags: "a" }); + +const logMessage = (message, messageType) => { + const now = new Date(); + const horaActual = `${now.getDate()}/${now.getMonth() + 1}/${now.getFullYear()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; + logStream.write(`[${horaActual}] ${message}\n`); + process[messageType].write(`[${horaActual}] ${message}\n`); +}; + +console.log = (message) => logMessage(message, "stdout"); +console.error = (message) => logMessage(message, "stderr"); + +// Manejar errores +require("./errorHandler.js"); + + +io.use((socket, next) => { + sessionMiddleware(socket.request, socket.request.res, next); +}); + +io.on("connection", async (socket) => { + const user = socket.request.session.user; + if (!user) return; + socket.join(user._id); + + let userChannels = await db.findChannels(user._id.toString()); + let parsedChannels = await parseChannelsData(userChannels, socket); + const dataObject = { + type: "channel", + channels: parsedChannels + } + socket.emit("renderData", dataObject); + + socket.on("joinChannel", async (channel) => { + if (!isSocketInChannel(channel, socket)) return; + socket.join(channel); + }); + + socket.on("chat:message", async ({ message, channel }) => { + if (!isSocketInChannel(channel, socket)) return; + db.createMessage(channel, user._id, message).then(async (message) => { + const dataObject = { + type: "message", + channelId: channel, + position: "beforeend", + messages: [{ + id: message._id, + author: user._id, + content: message.content, + date: Date.now(), + }] + } + io.in(channel).emit("renderData", dataObject); + console.log(`${user.nickname} > ${message.content}`); + }).catch((e) => console.error(e)); + }); +}); \ No newline at end of file diff --git a/src/logs/console.log b/src/logs/console.log new file mode 100644 index 0000000..13ddba1 --- /dev/null +++ b/src/logs/console.log @@ -0,0 +1,527 @@ +[31/5/2023 16:17:27] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:17:29] La conexión a MongoDB fue exitosa. +[31/5/2023 16:17:39] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:18:50] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:18:51] La conexión a MongoDB fue exitosa. +[31/5/2023 16:18:54] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:31:18] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:31:20] La conexión a MongoDB fue exitosa. +[31/5/2023 16:31:22] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:33:17] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:33:19] La conexión a MongoDB fue exitosa. +[31/5/2023 16:33:21] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:33:21] ReferenceError: photo is not defined + at C:\Users\Javier\Documents\Proyectos\ChatApp\src\routes\getUser.js:23:21 + at process.processTicksAndRejections (node:internal/process/task_queues:95:5) +[31/5/2023 16:33:32] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:33:33] La conexión a MongoDB fue exitosa. +[31/5/2023 16:33:36] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:33:36] ../Files/sin-foto.png +[31/5/2023 16:34:40] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:34:42] La conexión a MongoDB fue exitosa. +[31/5/2023 16:34:44] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:34:44] Images/1684906419380-529432.png +[31/5/2023 16:34:44] ../Files/sin-foto.png +[31/5/2023 16:35:13] Images/1684914045552-377850.webp +[31/5/2023 16:35:13] ../Files/sin-foto.png +[31/5/2023 16:35:15] Nuevo canal creado : 2 +[31/5/2023 16:36:36] Images/1684906419380-529432.png +[31/5/2023 16:36:36] ../Files/sin-foto.png +[31/5/2023 16:36:56] Images/1684914045552-377850.webp +[31/5/2023 16:36:56] ../Files/sin-foto.png +[31/5/2023 16:37:2] Nuevo canal creado : Canal de pruebas +[31/5/2023 16:37:47] Javier1 +[31/5/2023 16:37:47] PASSWORD: +[31/5/2023 16:37:47] PASSWORDCONFIMATION: +[31/5/2023 16:37:47] Se ha actualizado el usuario con el email javier@gmail.com +[31/5/2023 16:37:51] Images/1685569067299-100807.png +[31/5/2023 16:37:51] ../Files/sin-foto.png +[31/5/2023 16:40:37] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:40:38] La conexión a MongoDB fue exitosa. +[31/5/2023 16:40:45] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:40:46] La conexión a MongoDB fue exitosa. +[31/5/2023 16:40:53] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:40:55] La conexión a MongoDB fue exitosa. +[31/5/2023 16:40:57] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:40:58] Images/1685569067299-100807.png +[31/5/2023 16:40:58] ../Files/sin-foto.png +[31/5/2023 16:41:2] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:41:3] La conexión a MongoDB fue exitosa. +[31/5/2023 16:41:4] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:41:4] Images/1685569067299-100807.png +[31/5/2023 16:41:4] ../Files/sin-foto.png +[31/5/2023 16:41:34] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:41:36] La conexión a MongoDB fue exitosa. +[31/5/2023 16:41:38] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:41:38] C:\Users\Javier\Documents\Proyectos\ChatApp\src\routes\Images\1685569067299-100807.png +[31/5/2023 16:43:10] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:43:12] La conexión a MongoDB fue exitosa. +[31/5/2023 16:43:13] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:43:13] C:\Users\Javier\Documents\Proyectos\ChatApp\src\Images\Images\1685569067299-100807.png +[31/5/2023 16:43:28] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:43:29] La conexión a MongoDB fue exitosa. +[31/5/2023 16:43:35] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:43:36] C:\Users\Javier\Documents\Proyectos\ChatApp\src\Images\Images\1685569067299-100807.png +[31/5/2023 16:44:9] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:44:10] La conexión a MongoDB fue exitosa. +[31/5/2023 16:44:11] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:44:11] C:\Users\Javier\Documents\Proyectos\ChatApp\src\routes\Images\1685569067299-100807.png +[31/5/2023 16:44:22] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:44:23] La conexión a MongoDB fue exitosa. +[31/5/2023 16:44:25] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:44:25] C:\Users\Javier\Documents\Proyectos\ChatApp\src\routes\Images\1685569067299-100807.png +[31/5/2023 16:44:43] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:44:44] La conexión a MongoDB fue exitosa. +[31/5/2023 16:44:45] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:44:45] C:\Users\Javier\Documents\Proyectos\ChatApp\src\routes\Images\1685569067299-100807.png +[31/5/2023 16:44:54] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:44:55] La conexión a MongoDB fue exitosa. +[31/5/2023 16:44:56] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:44:57] C:\Users\Javier\Documents\Proyectos\ChatApp\src\Images\1685569067299-100807.png +[31/5/2023 16:44:57] Error: ENOENT: no such file or directory, open 'Images/1685569067299-100807.png' + at Object.openSync (node:fs:601:3) + at Object.readFileSync (node:fs:469:35) + at C:\Users\Javier\Documents\Proyectos\ChatApp\src\routes\getUser.js:19:26 + at process.processTicksAndRejections (node:internal/process/task_queues:95:5) +[31/5/2023 16:46:27] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:46:28] La conexión a MongoDB fue exitosa. +[31/5/2023 16:46:31] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:46:31] C:\Users\Javier\Documents\Proyectos\ChatApp\src\Images\1685569067299-100807.png +[31/5/2023 16:46:31] Error: ENOENT: no such file or directory, open 'Images/1685569067299-100807.png' + at Object.openSync (node:fs:601:3) + at Object.readFileSync (node:fs:469:35) + at C:\Users\Javier\Documents\Proyectos\ChatApp\src\routes\getUser.js:19:26 + at process.processTicksAndRejections (node:internal/process/task_queues:95:5) +[31/5/2023 16:47:52] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:47:53] La conexión a MongoDB fue exitosa. +[31/5/2023 16:48:2] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:48:3] La conexión a MongoDB fue exitosa. +[31/5/2023 16:48:5] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:48:5] ../Images/1685569067299-100807.png +[31/5/2023 16:48:36] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:48:37] La conexión a MongoDB fue exitosa. +[31/5/2023 16:48:40] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:48:40] C:\Users\Javier\Documents\Proyectos\ChatApp\src\Images\1685569067299-100807.png +[31/5/2023 16:48:40] Error: ENOENT: no such file or directory, open 'Images/1685569067299-100807.png' + at Object.openSync (node:fs:601:3) + at Object.readFileSync (node:fs:469:35) + at C:\Users\Javier\Documents\Proyectos\ChatApp\src\routes\getUser.js:19:26 + at process.processTicksAndRejections (node:internal/process/task_queues:95:5) +[31/5/2023 16:49:21] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:49:23] La conexión a MongoDB fue exitosa. +[31/5/2023 16:49:28] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:49:28] C:\Users\Javier\Documents\Proyectos\ChatApp\src\Images\1685569067299-100807.png +[31/5/2023 16:49:28] Error: ENOENT: no such file or directory, open 'Images/1685569067299-100807.png' + at Object.openSync (node:fs:601:3) + at Object.readFileSync (node:fs:469:35) + at C:\Users\Javier\Documents\Proyectos\ChatApp\src\routes\getUser.js:19:26 + at process.processTicksAndRejections (node:internal/process/task_queues:95:5) +[31/5/2023 16:52:22] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:52:24] La conexión a MongoDB fue exitosa. +[31/5/2023 16:52:28] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:52:28] C:\Users\Javier\Documents\Proyectos\ChatApp\src\Images\1685569067299-100807.png +[31/5/2023 16:52:28] Error: ENOENT: no such file or directory, open 'Images/1685569067299-100807.png' + at Object.openSync (node:fs:601:3) + at Object.readFileSync (node:fs:469:35) + at C:\Users\Javier\Documents\Proyectos\ChatApp\src\routes\getUser.js:19:26 + at process.processTicksAndRejections (node:internal/process/task_queues:95:5) +[31/5/2023 16:53:6] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:53:7] La conexión a MongoDB fue exitosa. +[31/5/2023 16:53:8] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:53:8] C:\Users\Javier\Documents\Proyectos\ChatApp\src\Images\1685569067299-100807.png +[31/5/2023 16:53:33] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:53:35] La conexión a MongoDB fue exitosa. +[31/5/2023 16:55:6] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:55:8] La conexión a MongoDB fue exitosa. +[31/5/2023 16:55:12] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:55:13] ReferenceError: path is not defined + at parseChannelsData (C:\Users\Javier\Documents\Proyectos\ChatApp\src\functions.js:43:21) + at process.processTicksAndRejections (node:internal/process/task_queues:95:5) + at async Namespace. (C:\Users\Javier\Documents\Proyectos\ChatApp\src\index.js:69:26) +[31/5/2023 16:57:7] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:57:9] La conexión a MongoDB fue exitosa. +[31/5/2023 16:57:13] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:57:13] C:\Users\Javier\Documents\Proyectos\ChatApp\src/../Images/1685569022245-972273.png +[31/5/2023 16:57:28] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:57:29] La conexión a MongoDB fue exitosa. +[31/5/2023 16:57:31] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:57:31] C:\Users\Javier\Documents\Proyectos\ChatApp\srcImages/1685569022245-972273.png +[31/5/2023 16:57:37] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:57:38] La conexión a MongoDB fue exitosa. +[31/5/2023 16:57:39] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:57:40] C:\Users\Javier\Documents\Proyectos\ChatApp\src/Images/1685569022245-972273.png +[31/5/2023 16:58:36] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:58:38] La conexión a MongoDB fue exitosa. +[31/5/2023 16:58:40] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 16:58:40] C:\Users\Javier\Documents\Proyectos\ChatApp\src/Images/1685569022245-972273.png +[31/5/2023 16:58:44] Javier1 > 1 +[31/5/2023 16:58:45] Javier1 > 2 +[31/5/2023 16:58:46] Javier1 > 3 +[31/5/2023 16:58:47] Javier1 > 4 +[31/5/2023 16:58:48] Javier1 > 5 +[31/5/2023 16:58:50] Javier1 > 6 +[31/5/2023 16:58:51] Javier1 > 7 +[31/5/2023 16:58:52] Javier1 > 8 +[31/5/2023 16:58:52] Javier1 > 9 +[31/5/2023 16:58:56] C:\Users\Javier\Documents\Proyectos\ChatApp\src/Images/1685569022245-972273.png +[31/5/2023 16:59:15] Javier +[31/5/2023 16:59:15] PASSWORD: +[31/5/2023 16:59:15] PASSWORDCONFIMATION: +[31/5/2023 16:59:15] Se ha actualizado el usuario con el email javier@gmail.com +[31/5/2023 16:59:19] C:\Users\Javier\Documents\Proyectos\ChatApp\src/Images/1685569022245-972273.png +[31/5/2023 16:59:46] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 16:59:47] La conexión a MongoDB fue exitosa. +[31/5/2023 17:0:21] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 17:0:24] Javier > 1 +[31/5/2023 17:0:25] Javier > 2 +[31/5/2023 17:0:26] Javier > 3 +[31/5/2023 17:0:27] Javier > 4 +[31/5/2023 17:0:27] Javier > 5 +[31/5/2023 17:0:28] Javier > 6 +[31/5/2023 17:0:29] Javier > 7 +[31/5/2023 17:0:29] Javier > 8 +[31/5/2023 17:0:33] Javier > 9 +[31/5/2023 17:0:35] Javier > 10 +[31/5/2023 17:0:41] Javier > 11 +[31/5/2023 17:0:42] Javier > 12 +[31/5/2023 17:0:42] Javier > 13 +[31/5/2023 17:0:43] Javier > 14 +[31/5/2023 17:0:43] Javier > 15 +[31/5/2023 17:0:44] Javier > 16 +[31/5/2023 17:0:46] Javier > 17 +[31/5/2023 17:0:47] Javier > 18 +[31/5/2023 17:0:47] Javier > 19 +[31/5/2023 17:0:48] Javier > 20 +[31/5/2023 17:0:49] Javier > 21 +[31/5/2023 17:0:50] Javier > 22 +[31/5/2023 17:0:51] Javier > 23 +[31/5/2023 17:0:52] Javier > 24 +[31/5/2023 17:0:52] Javier > 25 +[31/5/2023 17:0:53] Javier > 26 +[31/5/2023 17:0:54] Javier > 27 +[31/5/2023 17:0:55] Javier > 28 +[31/5/2023 17:0:56] Javier > 29 +[31/5/2023 17:0:56] Javier > 30 +[31/5/2023 17:1:27] Javier > aa +[31/5/2023 17:21:46] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:21:48] La conexión a MongoDB fue exitosa. +[31/5/2023 17:23:45] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:23:46] La conexión a MongoDB fue exitosa. +[31/5/2023 17:32:47] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:32:48] La conexión a MongoDB fue exitosa. +[31/5/2023 17:32:52] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 17:32:58] Javier +[31/5/2023 17:32:58] PASSWORD: +[31/5/2023 17:32:58] PASSWORDCONFIMATION: +[31/5/2023 17:33:14] Javier +[31/5/2023 17:33:14] PASSWORD: +[31/5/2023 17:33:14] PASSWORDCONFIMATION: +[31/5/2023 17:35:1] Javier +[31/5/2023 17:35:1] PASSWORD: +[31/5/2023 17:35:1] PASSWORDCONFIMATION: +[31/5/2023 17:35:10] Javier23 +[31/5/2023 17:35:10] PASSWORD: +[31/5/2023 17:35:10] PASSWORDCONFIMATION: +[31/5/2023 17:36:9] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:36:10] La conexión a MongoDB fue exitosa. +[31/5/2023 17:36:13] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 17:36:19] Javier +[31/5/2023 17:36:19] PASSWORD: +[31/5/2023 17:36:19] PASSWORDCONFIMATION: +[31/5/2023 17:36:42] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:36:43] La conexión a MongoDB fue exitosa. +[31/5/2023 17:36:46] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 17:36:50] Javier +[31/5/2023 17:36:50] PASSWORD: +[31/5/2023 17:36:50] PASSWORDCONFIMATION: +[31/5/2023 17:37:2] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:37:3] La conexión a MongoDB fue exitosa. +[31/5/2023 17:37:5] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 17:37:11] Javier +[31/5/2023 17:37:11] PASSWORD: +[31/5/2023 17:37:11] PASSWORDCONFIMATION: +[31/5/2023 17:37:20] Javier +[31/5/2023 17:37:20] PASSWORD: +[31/5/2023 17:37:20] PASSWORDCONFIMATION: +[31/5/2023 17:37:36] Javier +[31/5/2023 17:37:36] PASSWORD: +[31/5/2023 17:37:36] PASSWORDCONFIMATION: +[31/5/2023 17:37:58] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:37:59] La conexión a MongoDB fue exitosa. +[31/5/2023 17:38:4] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 17:38:11] Javier +[31/5/2023 17:38:11] PASSWORD: +[31/5/2023 17:38:11] PASSWORDCONFIMATION: +[31/5/2023 17:38:26] Javier +[31/5/2023 17:38:26] PASSWORD: +[31/5/2023 17:38:26] PASSWORDCONFIMATION: +[31/5/2023 17:38:34] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:38:37] La conexión a MongoDB fue exitosa. +[31/5/2023 17:39:17] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:39:18] La conexión a MongoDB fue exitosa. +[31/5/2023 17:39:18] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 17:39:22] [object Object] +[31/5/2023 17:39:35] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:39:36] La conexión a MongoDB fue exitosa. +[31/5/2023 17:39:39] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:39:40] La conexión a MongoDB fue exitosa. +[31/5/2023 17:39:42] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 17:39:46] { + "nickname": "Javier", + "email": "javier@gmail.com", + "password": "1", + "passwordConfirmation": "2" +} +[31/5/2023 17:40:53] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:40:54] La conexión a MongoDB fue exitosa. +[31/5/2023 17:41:3] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:41:4] La conexión a MongoDB fue exitosa. +[31/5/2023 17:41:5] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 17:41:10] { + "nickname": "Javier", + "email": "javier@gmail.com", + "password": "1", + "passwordConfirmation": "2" +} +[31/5/2023 17:41:10] PASSWORD: +[31/5/2023 17:41:10] PASSWORDCONFIMATION: +[31/5/2023 17:46:8] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:46:10] La conexión a MongoDB fue exitosa. +[31/5/2023 17:46:12] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 17:46:17] { + "nickname": "Javier", + "email": "javier@gmail.com", + "password": "1", + "passwordConfirmation": "2" +} +[31/5/2023 17:46:17] PASSWORD: +[31/5/2023 17:46:17] PASSWORDCONFIMATION: +[31/5/2023 17:46:17] NICKNAME: +[31/5/2023 17:46:26] { + "nickname": "Javier", + "email": "javier@gmail.com" +} +[31/5/2023 17:46:26] PASSWORD: +[31/5/2023 17:46:26] PASSWORDCONFIMATION: +[31/5/2023 17:46:26] NICKNAME: +[31/5/2023 17:46:26] Se ha actualizado el usuario con el email javier@gmail.com +[31/5/2023 17:46:53] { + "nickname": "Javier", + "email": "javier@gmail.com" +} +[31/5/2023 17:46:53] PASSWORD: +[31/5/2023 17:46:53] PASSWORDCONFIMATION: +[31/5/2023 17:46:53] NICKNAME: +[31/5/2023 17:46:53] Se ha actualizado el usuario con el email javier@gmail.com +[31/5/2023 17:48:45] { + "nickname": "Javier1", + "email": "javier@gmail.com" +} +[31/5/2023 17:48:45] PASSWORD: +[31/5/2023 17:48:45] PASSWORDCONFIMATION: +[31/5/2023 17:48:45] NICKNAME: +[31/5/2023 17:48:45] Se ha actualizado el usuario con el email javier@gmail.com +[31/5/2023 17:49:4] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 17:49:6] La conexión a MongoDB fue exitosa. +[31/5/2023 17:49:8] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 17:49:12] { + "nickname": "Javier1", + "email": "javier@gmail.com", + "password": "1" +} +[31/5/2023 17:49:21] { + "nickname": "Javier1", + "email": "javier@gmail.com", + "password": "123456789", + "passwordConfirmation": "123456789" +} +[31/5/2023 17:49:22] Se ha actualizado el usuario con el email javier@gmail.com +[31/5/2023 17:49:26] El usuario javier@gmail.com ha cerrado sesión +[31/5/2023 17:49:27] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 18:29:53] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 18:29:54] La conexión a MongoDB fue exitosa. +[31/5/2023 18:31:27] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 18:31:29] La conexión a MongoDB fue exitosa. +[31/5/2023 18:31:37] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 18:31:43] { + "nickname": "Javier1", + "email": "javier@gmail.com", + "password": "1" +} +[31/5/2023 18:31:49] { + "nickname": "Javier1", + "email": "javier@gmail.com", + "password": "12345678", + "passwordConfirmation": "12345678" +} +[31/5/2023 18:31:49] ReferenceError: hashedPassword is not defined +[31/5/2023 18:34:23] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 18:34:24] La conexión a MongoDB fue exitosa. +[31/5/2023 18:34:29] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 18:34:34] { + "nickname": "Javier1", + "email": "javier@gmail.com", + "password": "1" +} +[31/5/2023 18:34:34] { + "nickname": "Javier1", + "email": "javier@gmail.com", + "password": "1" +} +[31/5/2023 18:34:40] { + "nickname": "Javier1", + "email": "javier@gmail.com", + "password": "12345678", + "passwordConfirmation": "12345678" +} +[31/5/2023 18:34:40] Se ha actualizado el usuario con el email javier@gmail.com +[31/5/2023 18:34:55] { + "nickname": "Javier1", + "email": "javier@gmail.com", + "password": "123456789", + "passwordConfirmation": "123456789" +} +[31/5/2023 18:34:55] Se ha actualizado el usuario con el email javier@gmail.com +[31/5/2023 18:34:57] El usuario javier@gmail.com ha cerrado sesión +[31/5/2023 18:34:58] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 18:35:44] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 18:35:45] La conexión a MongoDB fue exitosa. +[31/5/2023 18:35:47] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 18:36:2] { + "nickname": "Javier1", + "email": "javier@gmail.com", + "password": "123456789", + "passwordConfirmation": "123456789" +} +[31/5/2023 18:36:2] $2b$10$O1OcpLTFqnW8yUZeseBQbeGgRfFt0sUK23pQpJLcjSyGTmfTtluEi +[31/5/2023 18:36:2] { + "nickname": "Javier1", + "email": "javier@gmail.com", + "password": "$2b$10$O1OcpLTFqnW8yUZeseBQbeGgRfFt0sUK23pQpJLcjSyGTmfTtluEi" +} +[31/5/2023 18:36:2] Se ha actualizado el usuario con el email javier@gmail.com +[31/5/2023 19:17:6] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 19:17:7] La conexión a MongoDB fue exitosa. +[31/5/2023 19:17:43] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 19:17:54] { + "nickname": "Javier1", + "email": "javier@gmail.com", + "password": "123456789", + "passwordConfirmation": "123456789" +} +[31/5/2023 19:17:54] Se ha actualizado el usuario con el email javier@gmail.com +[31/5/2023 19:17:56] El usuario javier@gmail.com ha cerrado sesión +[31/5/2023 19:17:58] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 19:18:41] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 19:18:42] La conexión a MongoDB fue exitosa. +[31/5/2023 19:18:45] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 19:18:49] { + "nickname": "Javier1", + "email": "javier@gmail.com", + "password": "123456789", + "passwordConfirmation": "123456789" +} +[31/5/2023 19:18:49] Se ha actualizado el usuario con el email javier@gmail.com +[31/5/2023 19:18:52] El usuario javier@gmail.com ha cerrado sesión +[31/5/2023 19:19:2] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 19:20:38] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 19:20:39] La conexión a MongoDB fue exitosa. +[31/5/2023 19:22:54] Error handler funcionando. +[31/5/2023 19:22:54] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 19:22:56] La conexión a MongoDB fue exitosa. +[31/5/2023 19:23:36] Error handler funcionando. +[31/5/2023 19:23:36] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 19:23:37] La conexión a MongoDB fue exitosa. +[31/5/2023 19:29:33] Error handler funcionando. +[31/5/2023 19:29:33] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 19:29:34] La conexión a MongoDB fue exitosa. +[31/5/2023 19:32:39] Error handler funcionando. +[31/5/2023 19:32:39] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 19:32:40] La conexión a MongoDB fue exitosa. +[31/5/2023 20:6:14] Error handler funcionando. +[31/5/2023 20:6:14] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 20:6:16] La conexión a MongoDB fue exitosa. +[31/5/2023 20:6:19] Error handler funcionando. +[31/5/2023 20:6:19] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 20:6:20] La conexión a MongoDB fue exitosa. +[31/5/2023 20:6:26] Error handler funcionando. +[31/5/2023 20:6:26] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 20:6:27] La conexión a MongoDB fue exitosa. +[31/5/2023 20:6:28] Error handler funcionando. +[31/5/2023 20:6:28] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 20:6:29] La conexión a MongoDB fue exitosa. +[31/5/2023 20:6:32] Error handler funcionando. +[31/5/2023 20:6:32] Servidor Express funcionando en http://0.0.0.0:30001 +[31/5/2023 20:6:33] La conexión a MongoDB fue exitosa. +[31/5/2023 20:6:35] Error handler funcionando. +[31/5/2023 20:6:35] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 20:6:36] La conexión a MongoDB fue exitosa. +[31/5/2023 20:6:58] Error handler funcionando. +[31/5/2023 20:6:58] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 20:6:59] La conexión a MongoDB fue exitosa. +[31/5/2023 20:8:38] Error handler funcionando. +[31/5/2023 20:8:38] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 20:8:40] La conexión a MongoDB fue exitosa. +[31/5/2023 20:8:48] Error handler funcionando. +[31/5/2023 20:8:48] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 20:8:49] La conexión a MongoDB fue exitosa. +[31/5/2023 20:8:58] Error handler funcionando. +[31/5/2023 20:8:58] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 20:8:59] La conexión a MongoDB fue exitosa. +[31/5/2023 20:9:12] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 20:10:42] Error handler funcionando. +[31/5/2023 20:10:42] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 20:10:44] La conexión a MongoDB fue exitosa. +[31/5/2023 20:11:17] Error handler funcionando. +[31/5/2023 20:11:17] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 20:11:18] La conexión a MongoDB fue exitosa. +[31/5/2023 20:11:37] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 20:11:37] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 20:11:52] Se ha actualizado el usuario con el email javier@gmail.com +[31/5/2023 20:11:57] El usuario javier@gmail.com ha cerrado sesión +[31/5/2023 20:11:59] El usuario javier@gmail.com ha iniciado sesión. +[31/5/2023 20:12:19] El usuario alex@gmail.com ha iniciado sesión. +[31/5/2023 20:12:22] Alex > 31 +[31/5/2023 20:12:23] Alex > 32 +[31/5/2023 20:12:24] Alex > 33 +[31/5/2023 20:12:25] Alex > 34 +[31/5/2023 20:12:26] Alex > 35 +[31/5/2023 20:12:26] Alex > 36 +[31/5/2023 20:12:28] Alex > 37 +[31/5/2023 20:12:33] Alex > 38 +[31/5/2023 20:12:34] Alex > 39 +[31/5/2023 20:12:35] Alex > 40 +[31/5/2023 20:12:35] Alex > 41 +[31/5/2023 20:12:36] Alex > 42 +[31/5/2023 20:12:37] Alex > 43 +[31/5/2023 20:12:37] Alex > 44 +[31/5/2023 20:12:38] Alex > 45 +[31/5/2023 20:12:44] Alex > 46 +[31/5/2023 20:12:57] Alex > 47 +[31/5/2023 20:12:59] Alex > 48 +[31/5/2023 20:13:0] Alex > 49 +[31/5/2023 20:13:1] Alex > 50 +[31/5/2023 20:13:1] Alex > 51 +[31/5/2023 20:13:4] Javier > 52 +[31/5/2023 20:13:4] Javier > 53 +[31/5/2023 20:13:6] Javier > 54 +[31/5/2023 20:13:7] Javier > 55 +[31/5/2023 20:13:8] Alex > 56 +[31/5/2023 20:13:10] Javier > 57 +[31/5/2023 20:13:12] Javier > 58 +[31/5/2023 20:13:13] Javier > 59 +[31/5/2023 20:13:13] Javier > 60 +[31/5/2023 20:13:15] Alex > 61 +[31/5/2023 20:13:15] Alex > 62 +[31/5/2023 20:13:16] Alex > 63 +[31/5/2023 20:13:18] Javier > 64 +[31/5/2023 20:13:20] Alex > 65 +[31/5/2023 20:13:21] Alex > 66 +[31/5/2023 20:13:22] Alex > 67 +[31/5/2023 20:13:24] Javier > 68 +[31/5/2023 20:13:26] Alex > 69 +[31/5/2023 20:13:30] Javier > 70 +[31/5/2023 20:13:59] Se ha actualizado el usuario con el email alex@gmail.com +[31/5/2023 20:22:24] Error handler funcionando. +[31/5/2023 20:22:24] Servidor Express funcionando en http://0.0.0.0:3000 +[31/5/2023 20:22:26] La conexión a MongoDB fue exitosa. diff --git a/src/public/chat/script.js b/src/public/chat/script.js new file mode 100644 index 0000000..2314360 --- /dev/null +++ b/src/public/chat/script.js @@ -0,0 +1,504 @@ +//UserModal y ChannelModal +const dialogs = document.querySelectorAll("dialog"); +const userModal = document.querySelector(".userModal"); +const channelModal = document.querySelector(".channelModal"); +const seePassword = document.querySelector(".seePassword"); +const userModalPassword = document.querySelector(".userModal [name='password']"); +const userModalPasswordConfirmation = document.querySelector(".userModal [name='passwordConfirmation']"); +const participantInput = document.querySelector(".participantInput"); +const photoInputs = document.querySelectorAll(".photoInput"); +const channelPhotoInput = document.querySelector(".channelPhotoInput"); +const userModalForm = document.querySelector(".userModal form"); +const channelModalForm = document.querySelector(".channelModal form"); +//--------------------------------------------------------------------------------------------- +const menu = document.querySelector(".menu"); +const menuOptions = document.querySelector(".menu-options"); +const darkModeToggle = document.querySelector(".menu-option input[type=checkbox]"); +const messageInput = document.querySelector(".messageInput"); +const send = document.querySelector(".send"); +const participantList = document.querySelector(".participantList"); +const photoDisplay = document.querySelector(".photoDisplay"); +const messageInputBox = document.querySelector(".messageInputBox"); +const nickname = document.querySelector(".nickname"); +const url = new URL(window.location); +const params = url.searchParams; +const socket = io(); + +function createNotification(type, notificationText) { + let background; + let color; + + if (type == "success") { + background = "#d4edda"; + color = "#155724"; + } else if ((type = "error")) { + background = "#f8d7da"; + color = "#721c24"; + } + return Toastify({ + text: notificationText, + duration: 3000, + style: { background, color }, + }).showToast(); +} + +socket.on("renderData", data => { + renderData(data); +}); + +const userCache = {}; + +function getUserInfo(author) { + if (userCache[author]) { + return Promise.resolve(userCache[author]); + } + return fetch(`/user?_id=${author}`) + .then(response => response.json()) + .then(userData => { + userCache[author] = userData; + return userData; + }); +} + +function renderData(...dataObjects) { + for (const dataObject of dataObjects) { + const { type } = dataObject; + + if (type === "eval") { + return eval(data); + } + + if (type === "channel") { + const channelList = document.querySelector(".channels"); + for (const channel of dataObject.channels) { + const { _id: id, name, description, photo, participants } = channel; + if (document.querySelector(`.channel[data-channel-id="${id}"]`)) continue; + socket.emit("joinChannel", id); + const channelHTML = ` +
+ Foto del canal +

${sanitizeHtml(name)}

+
+ `; + const channelHeaderHTML = ` +
+ arrow_back + Foto del canal +
+

${sanitizeHtml(name)}

+

${participants.join(", ")}

+
+ more_vert +
+ `; + const messageBoxHTML = ` +
+
+ `; + document.querySelector(".right").insertBefore(document.createRange().createContextualFragment(channelHeaderHTML), messageInputBox); + document.querySelector(".right").insertBefore(document.createRange().createContextualFragment(messageBoxHTML), messageInputBox); + channelList.insertAdjacentHTML("beforeend", channelHTML); + } + return; + } + + if (type === "message") { + const { messages, channelId, position } = dataObject; + const sendTo = document.querySelector(`.messageBox[data-channel-id="${channelId}"]`); + const containerHeight = sendTo.scrollHeight; + const scrollTopOffset = sendTo.scrollTop; + let messagesHTML = ""; + + messages + .reduce((previousPromise, message) => { + return previousPromise.then(() => { + const { id, author, content, date } = message; + return getUserInfo(author).then(userData => { + const messageHTML = ` +
+
+ Foto de perfil +

${userData.nickname}

+
+
${sanitizeHtml(content)}
+
${formatDate(date)}
+
+ `; + messagesHTML += messageHTML; + }); + }); + }, Promise.resolve()) + .then(() => { + sendTo.insertAdjacentHTML(position === "beforeend" ? "beforeend" : "afterbegin", messagesHTML); + const newContainerHeight = sendTo.scrollHeight; + sendTo.scrollTop = newContainerHeight - containerHeight + scrollTopOffset; + }) + .catch(error => { + console.error(error); + }); + } + } +} + +async function getMessages(channel, limit, offset) { + const response = await fetch(`/messages?channel=${channel}&limit=${limit}&offset=${offset}`); + const data = await response.json(); + data.type = "message"; + data.messages.reverse(); + return data; +} + +function sanitizeHtml(html) { + const element = document.createElement("div"); + element.textContent = html; + return element.innerHTML; +} + +function formatDate(timestamp) { + if (!timestamp) throw new Error("El parámetro 'timestamp' debe ser una fecha válida"); + + const date = new Date(timestamp); + const today = new Date(); + const oneDay = 24 * 60 * 60 * 1000; + const diffDays = Math.floor((today - date) / oneDay); + + let hour = date.getHours(); + let period = hour >= 12 ? "p. m." : "a. m."; + hour = hour % 12 || 12; + + const formattedHour = hour.toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + + let formattedDate; + + if (diffDays === 0) { + formattedDate = `${formattedHour}:${minutes} ${period}`; + } else if (diffDays === 1) { + formattedDate = `ayer a las ${formattedHour}:${minutes} ${period}`; + } else if (diffDays < 7 && diffDays > 1) { + const weekday = ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"][date.getDay()]; + formattedDate = `${weekday} a las ${formattedHour}:${minutes} ${period}`; + } else { + const month = ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"][date.getMonth()]; + const day = date.getDate(); + const year = date.getFullYear(); + formattedDate = `${day} de ${month} de ${year} a las ${formattedHour}:${minutes} ${period}`; + } + + return formattedDate; +} + +const channelsCache = {}; +async function selectChannel(selectedChannel) { + const channelId = selectedChannel.dataset.channelId; + messageInputBox.style.display = "flex"; + + const channels = document.querySelectorAll(".channel"); + selectedChannel.classList.add("selected"); + channels.forEach(channel => { + if (channel !== selectedChannel) { + channel.classList.remove("selected"); + } + }); + + // Modificar la URL agregando la id del canal como parámetro + url.searchParams.set("channel", channelId); + window.history.pushState({}, "", url); + + // Ocultar todos los headers de canales excepto el correspondiente al canal seleccionado + const channelHeaders = document.querySelectorAll(".channelHeader"); + channelHeaders.forEach(header => { + if (header.dataset.channelId === channelId) { + header.style.display = "flex"; + } else { + header.style.display = "none"; + } + }); + + // Ocultar todos los contenedores de mensajes excepto el correspondiente al canal seleccionado. + //También añade un listener para detectar cuando el scroll de SOLO el canal seleccionado llega a su tope. + const messageBoxes = document.querySelectorAll(".messageBox"); + messageBoxes.forEach(box => { + if (box.dataset.channelId === channelId) { + box.style.display = "block"; + box.addEventListener("scroll", handleScroll); + } else { + box.style.display = "none"; + box.removeEventListener("scroll", handleScroll); + } + }); + + if (channelsCache[channelId] && channelsCache[channelId].rendered) return; + let messages = await getMessages(channelId, 20, 0); + renderData(messages); + if (!channelsCache[channelId]) channelsCache[channelId] = {}; + channelsCache[channelId].rendered = true; +} + +function handleScroll(event) { + const container = event.target; + const channelId = container.dataset.channelId; + + if (!channelsCache[channelId] || (channelsCache[channelId] && !channelsCache[channelId].loadedMessages)) { + if (container.scrollTop === 0) { + const existingMessages = container.querySelectorAll(".message"); + const offset = existingMessages.length; + getMessages(channelId, 20, offset).then(response => { + if (response.messages.length < 20) channelsCache[channelId].loadedMessages = true; + renderData(response); + }).catch(error => console.error(error)); + } + } +} + +// Cerrar el menú desplegable si el usuario hace click fuera de él. Si el usuario hace click en el menú, no se cierra. +window.addEventListener("click", event => { + if (!event.target.matches(".menu")) { + menuOptions.classList.remove("show"); + } +}); + +// Agregar o remover la clase "show" del menú abierto, permitiendo mostrar u ocultar las opciones del menú al hacer click en el botón de menú. +function toggleMenu() { + menuOptions.classList.toggle("show"); +} + +//Evitar que se cierre el menú antes de que el usuario haya seleccionado una opción. +menuOptions.addEventListener("click", event => { + event.stopPropagation(); +}); + +darkModeToggle.addEventListener("click", e => { + e.stopPropagation(); + toggleDarkMode(); +}); + +function toggleDarkMode() { + const darkModeEnabled = darkModeToggle.checked; + document.body.classList.toggle("dark-mode", darkModeEnabled); + + // Establecer la cookie darkMode en true o false + document.cookie = `darkMode=${darkModeToggle.checked}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`; +} + +/*Modo oscuro */ +document.addEventListener("DOMContentLoaded", () => { + fetch(`/user`) + .then(response => response.json()) + .then(data => { + nickname.textContent = data.nickname; + nickname.dataset.nickname = data.nickname; + document.querySelector(".my .name").textContent = data.email; + if (data.photo) { + document.querySelector(".profilePhoto").src = data.photo; + document.querySelector(".userModal .photoDisplay").src = data.photo; + } + document.querySelector(".userModal input[name='nickname']").value = data.nickname; + document.querySelector(".userModal input[name='email']").value = data.email; + }) + .catch(error => { + console.error(error); + }); + params.delete("channel"); + url.search = params.toString(); + window.history.pushState({}, "", url); + // Modo oscuro + const darkModeCookie = document.cookie.split(";").find(cookie => cookie.trim().startsWith("darkMode=")); + const isDarkModeEnabled = darkModeCookie && darkModeCookie.split("=")[1] === "true"; + darkModeToggle.checked = isDarkModeEnabled; + toggleDarkMode(); + darkModeToggle.addEventListener("change", toggleDarkMode); +}); + +function sendMessage() { + socket.emit("chat:message", { message: messageInput.value, channel: params.get("channel") }); + messageInput.value = ""; + updateSendButton(); +} + +function updateSendButton() { + const hasValue = messageInput.value; + + send.style.opacity = hasValue ? 1 : 0.1; + send.style.pointerEvents = hasValue ? "auto" : "none"; + send.tabIndex = hasValue ? 0 : -1; +} + +//UserModal y ChannelModal +//Por cada elemento dialog, escuchar si se hace click fuera de él y cerrarlo. +dialogs.forEach(dialog => { + dialog.addEventListener("click", e => { + const dialogDimensions = dialog.getBoundingClientRect(); + if (e.clientX < dialogDimensions.left || e.clientX > dialogDimensions.right || e.clientY < dialogDimensions.top || e.clientY > dialogDimensions.bottom) { + dialog.close(); + } + }); +}); + +//Por cada elemento con la clase "photoInput" escuchar si se añade un archivo. +photoInputs.forEach(photoInput => { + photoInput.addEventListener("change", event => { + const file = event.target.files[0]; + if (!file.type.startsWith("image/")) { + photoInput.value = ""; + return createNotification("error", "El archivo seleccionado no es una imagen."); + } + const imageUrl = URL.createObjectURL(file); + const photoDisplay = photoInput.nextElementSibling; + photoDisplay.src = imageUrl; + }); +}); + +function togglePasswordVisibility() { + const isPasswordType = userModalPassword.type === "password"; + userModalPassword.type = isPasswordType ? "text" : "password"; + userModalPasswordConfirmation.type = isPasswordType ? "text" : "password"; + seePassword.style.opacity = isPasswordType ? 0.8 : 0.2; +} + +function showMenu() { + menu.show(); +} + +function showUserModal() { + userModal.showModal(); +} + +function closeUserModal() { + userModal.close(); +} + +function saveUserModal() { + const userData = {}; + const promises = []; + + for (const element of userModalForm.elements) { + if (element.type === "file" && element.files[0]) { + const file = element.files[0]; + const reader = new FileReader(); + const promise = new Promise(resolve => { + reader.onload = () => { + const img = new Image(); + img.onload = () => { + const canvas = Object.assign(document.createElement("canvas"), { + width: 500, + height: 500, + }); + canvas.getContext("2d").drawImage(img, 0, 0, img.width, img.height, 0, 0, 500, 500); + userData[element.name] = canvas.toDataURL(file.type); + resolve(); + }; + img.src = reader.result; + }; + }); + reader.readAsDataURL(file); + promises.push(promise); + } else if (element.value) { + userData[element.name] = element.value; + } + } + + Promise.all(promises).then(() => { + fetch("/editUser", { + method: "PUT", + body: JSON.stringify(userData), + headers: {"Content-Type": "application/json"} + }).then(response => response.json()).then(data => { + if (!data.success) return createNotification("error", data.message); + createNotification("success", data.message); + closeUserModal(); + }).catch(error => console.error(error)); + }); +} + +function showChannelModal() { + channelModal.showModal(); +} + +function closeChannelModal() { + channelModal.close(); +} + +function addParticipantToList() { + const participantNames = document.querySelectorAll(".participant .name"); + const user = participantInput.value; + let isUserInList = false; + + participantNames.forEach(participantName => { + if (participantName.textContent == user) { + isUserInList = true; + return createNotification("error", `El participante ${user} ya está en la lista.`); + } + }); + + if (!isUserInList) { + fetch(`/user?email=${encodeURIComponent(user)}`) + .then(response => response.json()) + .then(data => { + if (data.error) { + return createNotification("error", data.error); + } else { + const participantHTML = ` +
+ Foto de perfil +

${data.email}

+ +
+ `; + participantList.insertAdjacentHTML("beforeend", participantHTML); + participantInput.value = ""; + } + }) + .catch(error => { + console.error(error); + }); + } +} + +function saveChannelModal() { + const channelData = { + participants: [...participantList.querySelectorAll(".name")].map(name => name.textContent), + }; + + const promises = []; + + for (const element of channelModalForm.elements) { + if (element.type === "file" && element.files[0]) { + const file = element.files[0]; + const reader = new FileReader(); + const promise = new Promise(resolve => { + reader.onload = () => { + const img = new Image(); + img.onload = () => { + const canvas = Object.assign(document.createElement("canvas"), { + width: 500, + height: 500, + }); + canvas.getContext("2d").drawImage(img, 0, 0, img.width, img.height, 0, 0, 500, 500); + channelData[element.name] = canvas.toDataURL(file.type); + resolve(); + }; + img.src = reader.result; + }; + }); + reader.readAsDataURL(file); + promises.push(promise); + } else if (element.value) { + channelData[element.name] = element.value; + } + } + + Promise.all(promises).then(() => { + fetch("/createChannel", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(channelData), + }).then(response => response.json()).then(data => { + if(!data.success) return createNotification("error", data.message); + createNotification("success", data.message); + closeChannelModal(); + }).catch(error => { + console.error(error); + }); + }); +} diff --git a/src/public/chat/style.css b/src/public/chat/style.css new file mode 100644 index 0000000..a207e99 --- /dev/null +++ b/src/public/chat/style.css @@ -0,0 +1,705 @@ + +* { + box-sizing: border-box; + font-family: "Josefin sans", Arial, Helvetica, sans-serif; +} + +body { + padding: 0; + margin: 0; + background-color: var(--chat-body); + height: 100vh; + width: 100vw; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-size: 16px; + color: var(--color); +} + +.dark-mode .logo { + filter: invert(100%); +} + +.container { + width: 85%; + height: 95%; + display: flex; +} + +.container .left { + width: 30%; + display: flex; + flex-direction: column; + background-color: var(--chat-background-primary); + border-right: 1px solid var(--color-soft); +} + +.container .right { + width: 70%; + display: flex; + flex-direction: column; +} + +.user { + position: relative; + height: 60px; + background-color: var(--color-background-secondary); + display: flex; + align-items: center; + padding: 10px 15px; +} + +.user .profilePhoto { + width: 45px; + border-radius: 50%; +} + +.user .nickname { + margin-left: 20px; + width: 60%; + word-wrap: break-word; +} + +.user .menu { + height: 25px; + position: absolute; + right: 15px; +} + +img { + user-select: none; + -webkit-user-drag: none; +} + +.material-icons { + cursor: pointer; + user-select: none; + color: inherit; + outline: none; +} + +.material-icons:hover, .material-icons:focus { + background-color: rgba(136, 136, 136, 0.3); + box-shadow: 0 0 0 7px rgba(136, 136, 136, 0.3); + border-radius: 50%; + color: var(--color-primary); +} + +.left .channels { + display: flex; + flex-grow: 1; + flex-direction: column; + align-items: center; + overflow: auto; + gap: 10px; +} + +.left .createChannel { + width: 100%; + height: 70px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 10px; +} + +.left .createChannel .button { + width: 80%; +} + +.channel { + width: 90%; + min-height: 70px; + border: 1px solid var(--color-soft); + display: flex; + border-radius: 10px; + cursor: pointer; + outline: none; + align-items: center; + gap: 10px; + padding: 0 10px; +} + +.channel:focus-visible { + border-color: var(--color-primary); +} + +.channel:hover { + background-color: var(--color-background-secondary); +} + +.channel.selected { + background-color: var(--color-selected); +} + +.channel .channelPhoto { + height: 45px; + border-radius: 50%; +} + +/*Menú de opciones*/ +.menu-options { + display: none; + position: absolute; + top: 45px; + right: 25px; + background-color: var(--chat-background-primary); + border: 1px solid var(--color-soft); + width: 220px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); + border-radius: 4px; + z-index: 1; + padding: 10px 0; + opacity: 0; +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.menu-options.show { + opacity: 1; + display: block; + animation: fadeIn 0.2s ease-in-out; +} + +.menu-option { + margin: 0; + cursor: pointer; + text-decoration: none; + width: 100%; + height: 40px; + display: block; + padding: 12px 30px; + color: inherit; + position: relative; + user-select: none; +} + +.menu-option:hover { + background-color: var(--color-background-secondary); +} + +.right .channelHeader { + display: none; + width: 100%; + min-height: 60px; + height: 60px; + background-color: var(--color-background-secondary); + cursor: pointer; + align-items: center; + gap: 20px; + padding: 20px; +} + +.channelHeader .back { + display: none; +} + +.channelHeader .channelPhoto { + height: 45px; + border-radius: 50%; +} + +.channelHeader .channelInfo { + flex-grow: 1; + height: 60px; + display: flex; + flex-direction: column; + justify-content: center; + gap: 5px; +} + +.channelHeader .channelName { + padding: 0; + margin: 0; + height: 16px; + font-size: 18px; +} + +.channelHeader .participants { + font-size: 0.8em; + color: #888; + padding: 0; + margin: 0; + height: 16px; +} + +.messageBox { + width: 100%; + flex-grow: 1; + background-color: var(--color-messageBox); + background-repeat: repeat; + overflow: auto; + padding: 20px; + position: relative; +} + +.welcome { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: var(--color-welcomeBox); +} + +.welcome img { + width: 80%; +} + +.welcome p { + margin-top: 60px; + color: #888; + text-align: center; + pointer-events: none; +} + +.channelBox { + display: none; + background-image: var(--chatBackground) +} + +/*Mensaje*/ +.message { + display: flex; + flex-direction: column; + background-color: var(--color-background-primary); + border-radius: 15px; + min-width: 100px; + max-width: 90%; + margin-bottom: 10px; + clear: both; + padding: 10px; + float: left; + animation: fadeIn 0.5s ease-in-out; +} + +.message .author { + display: flex; + gap: 10px; + width: 100%; + height: 32px; + align-items: center; +} + +.message .authorPhoto { + width: 32px; + height: 32px; + border-radius: 50%; +} + +.message .authorName { + font-weight: bold; +} + +.message .content { + width: 100%; + flex-grow: 1; + display: flex; + min-height: 40px; + align-items: center; + word-wrap: break-word; +} + +.message .date { + width: 100%; + display: flex; + align-items: flex-end; + justify-content: right; + font-size: 0.8em; + color: #888; +} + +.message.own { + background-color: var(--color-primary); + color: #fff; + float: right; +} + +.message.own .author { + display: none; +} + +.message.own .content { + min-height: 0; +} + +.message.own .date { + color: #ebeef0; +} + +.messageInputBox { + position: relative; + width: 100%; + min-height: 70px; + display: none; + align-items: center; + background-color: var(--color-background-secondary); + gap: 20px; + padding: 0 20px; +} + +.messageInputBox input { + height: 40px; + width: 70%; + border-radius: 5px; + flex-grow: 1; + border: none; + padding: 0 10px; + font-size: 18px; + color: inherit; + outline: none; + background-color: var(--chat-background-primary); +} + +.messageInputBox .send { + margin-right: 10px; + pointer-events: none; + opacity: 0.1; + transition: opacity 0.2s; +} + +/*UserModal y ChannelModal*/ +.modal { + border: none; + border-radius: 15px; + width: 100%; + height: 100%; + padding: 0; + color: inherit; + background-color: var(--chat-background-primary); +} + +.userModal { + max-width: 600px; + max-height: 800px; +} + +.channelModal { + max-height: 700px; + max-width: 900px; +} + +.modal .content { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +.title { + font-size: 24px; + text-align: center; +} + +.channelModal .title { + height: 45px; + margin-bottom: 20px; +} + +.channelModal .title2 { + margin-top: 40px; +} + +.channelModal .mainDiv { + width: 100%; + flex-grow: 1; + display: flex; +} + +.userModal .mainDiv { + width: 100%; + flex-grow: 1; + display: flex; + flex-direction: column; +} + +.modal .buttonsDiv { + min-height: 120px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 30px; +} + +.modal .left { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + height: 100%; + width: 100%; +} + +.modal .right { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 55%; +} + +.userModal .right { + width: 100%; + gap: 30px; +} + +.channelModal .left { + width: 45%; +} + +.modal .photoLabel { + outline: 2px dashed var(--color-primary); + overflow: hidden; + cursor: pointer; + display: inline-block; + width: 256px; + height: 256px; + border-radius: 50%; + margin-bottom: 30px; +} + +.modal .photoDisplay { + width: 100%; + height: 100%; +} + +.modal .inputBox { + position: relative; +} + +.userModal .inputBox { + width: 80%; +} + +.channelModal .inputBox { + width: 90%; +} + +.modal .inputBox input { + font-size: 16px; + width: 100%; + height: 45px; + padding: 0 10px; + border: none; + outline: none; + border-bottom: 1px solid var(--color-primary); + background-color: transparent; + color: inherit; +} + +.modal .inputBox span { + position: absolute; + left: 7px; + top: 10px; + pointer-events: none; + color: #888; + transition: transform 0.2s, font-size 0.2s; + background-color: transparent; +} + +.userModal .seePassword { + position: absolute; + height: 60%; + top: 50%; + right: 8px; + transform: translateY(-50%); + opacity: 0.2; +} + +.userModal .seePassword:hover { + opacity: 0.8; +} + +.button { + border-radius: 5px; + cursor: pointer; + transition: filter 0.3s; + height: 45px; + width: 40%; + font-size: 18px; + color: inherit; + border: 1px solid var(--color-primary); + background-color: transparent; +} + +button:hover { + filter: brightness(92%); +} + +.saveButton { + background-color: var(--color-primary); + color: #fff; +} + +.modal .inputBox input:focus ~ span, +.modal .inputBox input:valid ~ span { + color: var(--color-primary); + border-bottom: var(--color-primary); + transform: translate(10px, -24px); + font-size: 0.75em; +} + +.channelModal .left .inputBox { + margin-bottom: 30px; +} + +.channelModal .administrate { + width: 90%; + height: 45px; + display: flex; + border: 1px solid #999; +} + +.administrate input { + height: 100%; + flex-grow: 1; + border: none; + padding: 0 20px; + font-size: 16px; + color: inherit; + outline: none; + background-color: transparent; +} + +.administrate:focus-within { + border: 1px solid var(--color-primary); +} + +.administrate .addParticipantButton, +.participant .removeParticipantButton { + width: 75px; + font-size: 16px; + border: none; + border-radius: 0; + height: 100%; +} + +.participant .removeParticipantButton { + background-color: #f00; +} + +.channelModal .participantList { + display: flex; + flex-direction: column; + align-items: center; + overflow: auto; + width: 100%; + flex-grow: 1; + padding: 0; + gap: 10px; + margin-top: 40px; +} + +.participant { + height: 50px; + width: 90%; + display: flex; + border: 1px solid #888; + align-items: center; +} + +.participant .profilePhoto { + width: 32px; + height: 32px; + border-radius: 50%; + margin: 0 10px; +} + +.participant .name { + flex-grow: 1; +} + +.channelModal .participantList .my { + opacity: 0.5; + pointer-events: none; +} + +.channelModal .participant .profilePhoto { + width: 32px; + height: 32px; + border-radius: 50%; +} + +.menu .option { + margin: 0; + cursor: pointer; + text-decoration: none; + width: 100%; + height: 40px; + display: block; + padding: 12px 30px; + color: initial; + position: relative; + user-select: none; +} + +.menu .option:hover { + background-color: var(--color-background-secondary); +} +@media (max-width: 1100px) { + .container { + width: 100%; + height: 100%; + } +} +@media (max-width: 800px) { + .container .left { + display: none; + } + .container .right { + width: 100%; + } + ::-webkit-scrollbar { + display: none; + } + .channelModal { + max-height: 1200px; + } + + .channelModal .title2 { + margin-top: 60px; + } + + .channelModal .mainDiv { + flex-direction: column; + } + + .channelModal .mainDiv .left, + .channelModal .mainDiv .right { + width: 100%; + border-bottom: 1px solid var(--color-soft); + height: 600px; + } + .channelHeader .back { + display: block; + } +} + +/* Estilos para el scrollbar */ +::-webkit-scrollbar { + width: 7px; +} + +::-webkit-scrollbar-thumb { + background-color: var(--color-primary); +} + +input:-webkit-autofill, +input:-webkit-autofill:focus, +input:-webkit-autofill:hover, +input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 1000px var(--chat-background-primary) inset; + -webkit-text-fill-color: var(--color); +} \ No newline at end of file diff --git a/src/public/login/script.js b/src/public/login/script.js new file mode 100644 index 0000000..be8714b --- /dev/null +++ b/src/public/login/script.js @@ -0,0 +1,72 @@ +// Definir los elementos del DOM +const body = document.body; +const darkModeToggle = document.querySelector(".dark-mode-toggle"); +const form = document.querySelector("form"); +const email = document.querySelector(".email"); +const password = document.querySelector(".password"); +const eye = document.querySelector(".seePassword"); + +document.addEventListener("DOMContentLoaded", () => { + // Verificar si la cookie está habilitada para establecer el modo oscuro + const darkModeCookie = document.cookie.split(";").find((cookie) => cookie.trim().startsWith("darkMode=")); + const isDarkModeEnabled = darkModeCookie && darkModeCookie.split("=")[1] === "true"; + darkModeToggle.checked = isDarkModeEnabled; + toggleDarkMode(); + + // Evento de cambio para el interruptor de modo oscuro + darkModeToggle.addEventListener("change", toggleDarkMode); +}); + +// Función para cambiar el modo oscuro +function toggleDarkMode() { + const darkModeEnabled = darkModeToggle.checked; + body.classList.toggle("dark-mode", darkModeEnabled); + + // Establecer la cookie darkMode en true o false + document.cookie = `darkMode=${darkModeEnabled}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`; +} + +function togglePasswordVisibility() { + if (password.type == "password") { + password.type = "text"; + eye.style.opacity = 0.8; + } else { + password.type = "password"; + eye.style.opacity = 0.2; + } +} + +function createNotification(type, notificationText) { + let background; + let color; + + if (type == "success") { + background = "#d4edda"; + color = "#155724"; + } else if ((type = "error")) { + background = "#f8d7da"; + color = "#721c24"; + } + return Toastify({ + text: notificationText, + duration: 3000, + style: { background, color }, + }).showToast(); +} + +function login() { + const data = { + email: email.value, + password: password.value, + }; + fetch("/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }).then((response) => response.json()).then((data) => { + if(!data.success) return createNotification("error", data.message) + window.location.href = "/chat"; + }).catch((error) => { + console.error(error); + }); +} \ No newline at end of file diff --git a/src/public/login/style.css b/src/public/login/style.css new file mode 100644 index 0000000..afb53a1 --- /dev/null +++ b/src/public/login/style.css @@ -0,0 +1,277 @@ +body { + padding: 0; + margin: 0; + font-family: "Josefin Sans", sans-serif; + background-color: var(--color-body); + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + overflow: hidden; +} + +form { + width: 90%; + max-width: 450px; + height: 450px; + margin: 50px auto; + background-color: var(--color-background-primary); + border-radius: 15px; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + box-shadow: 0px 0px 10px var(--color-terciary); + color: var(--color); +} + +form .logo { + width: 90%; + margin: 30px auto 30px; + max-width: 350px; + user-select: none; +} + +.dark-mode .logo { + filter: invert(100%); +} + +form .inputBox { + position: relative; + width: 60%; + display: flex; + justify-content: center; +} + +form .inputBox input { + font-family: "Josefin Sans", sans-serif; + font-size: 16px; + width: 100%; + height: 45px; + padding: 0 10px; + border-radius: 5px; + border: 1px solid var(--color-terciary); + background-color: transparent; + outline: none; + color: inherit; +} + +form .inputBox span { + position: absolute; + left: 7px; + top: 16px; + pointer-events: none; + font-size: 1em; + color: #888; + transition: transform 0.2s; + padding: 0 10px; +} + +.seePassword { + position: absolute; + top: 50%; + right: 8px; + transform: translateY(-50%); + cursor: pointer; + opacity: 0.2; +} + +.seePassword:hover { + opacity: 0.8; +} + +form .inputBox input:focus~span, +form .inputBox input:valid~span { + color: var(--color-primary); + transform: translate(10px, -23px); + font-size: 0.85em; + background-color: var(--color-background-primary); +} + +form .inputBox input:focus { + border: 1px solid var(--color-primary) +} + +form .submit-button { + font-family: "Josefin Sans", sans-serif; + width: 65%; + height: 45px; + background-color: var(--color-primary); + color: #fff; + font-size: 20px; + font-weight: 500; + border: none; + padding: 0 10px; + border-radius: 5px; +} + +form .submit-button:hover { + opacity: 0.9; + cursor: pointer; +} + +form .subtitle { + font-size: 18px; +} + +form a, form a:visited { + font-size: 18px; + text-decoration: none; + color: var(--color-primary); +} + +form .inputBox input.invalidInput { + border: 1px solid red !important; +} + +form .inputBox input.invalidInput~span { + color: red !important; +} + +/* Checkbox del modo oscuro */ +.checkbox-wrapper-54 input[type="checkbox"] { + visibility: hidden; + display: none; +} + +.checkbox-wrapper-54 *, +.checkbox-wrapper-54 ::after, +.checkbox-wrapper-54 ::before { + box-sizing: border-box; + position: absolute; + top: 40px; +} + +.checkbox-wrapper-54 .switch { + --width-of-switch: 3.5em; + --height-of-switch: 2em; + --size-of-icon: 1.4em; + --slider-offset: 0.3em; + position: relative; + width: var(--width-of-switch); + height: var(--height-of-switch); + display: inline-block; +} + +.checkbox-wrapper-54 .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #f4f4f5; + transition: .4s; + border-radius: 30px; + border: 1px solid #888; +} + +.checkbox-wrapper-54 .slider:before { + position: absolute; + content: ""; + height: var(--size-of-icon, 1.4em); + width: var(--size-of-icon, 1.4em); + border-radius: 20px; + left: var(--slider-offset, 0.3em); + top: 50%; + transform: translateY(-50%); + background: linear-gradient(40deg, #ff0080, #ff8c00 70%); + transition: .4s; +} + +.checkbox-wrapper-54 input:checked+.slider { + background-color: #303136; +} + +.checkbox-wrapper-54 input:checked+.slider:before { + left: calc(100% - (var(--size-of-icon, 1.4em) + var(--slider-offset, 0.3em))); + background: #303136; + box-shadow: inset -3px -2px 5px -2px #8983f7, inset -10px -4px 0 0 #a3dafb; +} + +.checkbox-wrapper { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.toggle-switch { + display: inline-block; + position: relative; + cursor: pointer; + outline: none; + user-select: none; +} + +.toggle-switch-input { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.toggle-switch-slider { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 50px; + background-color: #ddd; + -webkit-transition: .4s; + transition: .4s; +} + +.toggle-switch-slider:before { + position: absolute; + content: ""; + height: 22px; + width: 22px; + left: 2px; + bottom: 2px; + border-radius: 50%; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +.toggle-switch-input:checked+.toggle-switch-slider { + background-color: #2196F3; +} + +.toggle-switch-input:checked+.toggle-switch-slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +input:-webkit-autofill, +input:-webkit-autofill:focus, +input:-webkit-autofill:hover, +input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 1000px var(--color-background-primary) inset; + -webkit-text-fill-color: var(--color); +} + +@media (max-width: 450px) { + .checkbox-wrapper-54 { + position: absolute; + top: 0; + } + form { + width: 100%; + height: 100%; + margin: 0; + justify-content: center; + border-radius: 0; + gap: 50px; + } + form .inputBox { + width: 90%; + } + form .submit-button { + width: 90%; + } +} \ No newline at end of file diff --git a/src/public/register/script.js b/src/public/register/script.js new file mode 100644 index 0000000..bd2a6ed --- /dev/null +++ b/src/public/register/script.js @@ -0,0 +1,79 @@ +// Definir los elementos del DOM +const body = document.body; +const darkModeToggle = document.querySelector(".dark-mode-toggle"); +const form = document.querySelector("form"); +const nickname = document.querySelector(".nickname"); +const email = document.querySelector(".email"); +const password = document.querySelector(".password"); +const passwordConfirmation = document.querySelector(".passwordConfirmation"); +const eye = document.querySelector(".seePassword"); + +document.addEventListener("DOMContentLoaded", () => { + // Verificar si la cookie está habilitada para establecer el modo oscuro + const darkModeCookie = document.cookie.split(";").find((cookie) => cookie.trim().startsWith("darkMode=")); + const isDarkModeEnabled = darkModeCookie && darkModeCookie.split("=")[1] === "true"; + darkModeToggle.checked = isDarkModeEnabled; + toggleDarkMode(); + + // Evento de cambio para el interruptor de modo oscuro + darkModeToggle.addEventListener("change", toggleDarkMode); +}); + +// Función para cambiar el modo oscuro +function toggleDarkMode() { + const darkModeEnabled = darkModeToggle.checked; + body.classList.toggle("dark-mode", darkModeEnabled); + // Establecer la cookie darkMode en true o false + document.cookie = `darkMode=${darkModeEnabled}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`; +} + +eye.addEventListener("click", () => { + if (password.type == "password") { + password.type = "text"; + passwordConfirmation.type = "text"; + eye.style.opacity = 0.8; + } else { + password.type = "password"; + passwordConfirmation.type = "password"; + eye.style.opacity = 0.2; + } +}); + +function createNotification(type, notificationText) { + let background; + let color; + + if (type == "success") { + background = "#d4edda"; + color = "#155724"; + } else if ((type = "error")) { + background = "#f8d7da"; + color = "#721c24"; + } + return Toastify({ + text: notificationText, + duration: 3000, + style: { background, color }, + }).showToast(); +} + +function register() { + const data = { + nickname: nickname.value, + email: email.value, + password: password.value, + passwordConfirmation: passwordConfirmation.value + }; + fetch("/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }) + .then((response) => response.json()).then((data) => { + if(!data.success) return createNotification("error", data.message) + window.location.href = "/chat"; + }) + .catch((error) => { + console.error(error); + }); +} \ No newline at end of file diff --git a/src/public/register/style.css b/src/public/register/style.css new file mode 100644 index 0000000..e0bb63c --- /dev/null +++ b/src/public/register/style.css @@ -0,0 +1,310 @@ +body { + padding: 0; + margin: 0; + font-family: "Josefin Sans", sans-serif; + background-color: var(--color-body); + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + overflow: hidden; +} + +form { + width: 90%; + max-width: 450px; + height: 550px; + margin: 50px auto; + background-color: var(--color-background-primary); + border-radius: 15px; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + box-shadow: 0px 0px 10px var(--color-terciary); + color: var(--color); +} + +form .logo { + width: 90%; + margin: 30px auto 30px; + max-width: 350px; + user-select: none; +} + +.dark-mode .logo { + filter: invert(100%); +} + +form .inputBox { + position: relative; + width: 60%; + display: flex; + justify-content: center; +} + +form .inputBox input { + font-family: "Josefin Sans", sans-serif; + font-size: 16px; + width: 100%; + height: 45px; + padding: 0 10px; + border-radius: 5px; + border: 1px solid var(--color-terciary); + background-color: transparent; + outline: none; + color: inherit; +} + +form .inputBox span { + position: absolute; + left: 7px; + top: 16px; + pointer-events: none; + font-size: 1em; + color: #888; + transition: transform 0.2s; + padding: 0 10px; +} + +.seePassword { + position: absolute; + top: 50%; + right: 8px; + transform: translateY(-50%); + cursor: pointer; + opacity: 0.2; +} + +.seePassword:hover { + opacity: 0.8; +} + +form .inputBox input:focus~span, +form .inputBox input:valid~span { + color: var(--color-primary); + transform: translate(10px, -23px); + font-size: 0.85em; + background-color: var(--color-background-primary); +} + +form .inputBox input:focus { + border: 1px solid var(--color-primary) +} + +form .submit-button { + font-family: "Josefin Sans", sans-serif; + width: 65%; + height: 45px; + background-color: var(--color-primary); + color: #fff; + font-size: 20px; + font-weight: 500; + border: none; + padding: 0 10px; + border-radius: 5px; +} + +form .submit-button:hover { + opacity: 0.9; + cursor: pointer; +} + +form .subtitle { + font-size: 18px; +} + +form a, form a:visited { + font-size: 18px; + text-decoration: none; + color: var(--color-primary); +} + +form .inputBox input.invalidInput { + border: 1px solid red !important; +} + +form .inputBox input.invalidInput~span { + color: red !important; +} + +/*Mensaje de error*/ +.error-div { + position: absolute; + top: 50px; + right: 50px; + position: absolute; + width: 90%; + max-width: 450px; + display: flex; + padding: 10px; + background-color: #ffd0d8; + border: none; + border-radius: 5px; +} + +.error-text { + font-size: 15px; + color: #A64955; +} + +.close-button { + background-color: transparent; + border: none; + cursor: pointer; + position: absolute; + top: 35%; + right: 5px; + font-size: 18px; + font-weight: bold; + color: #cf8593; +} + +/* Checkbox del modo oscuro */ + +.checkbox-wrapper-54 input[type="checkbox"] { + visibility: hidden; + display: none; +} + +.checkbox-wrapper-54 *, +.checkbox-wrapper-54 ::after, +.checkbox-wrapper-54 ::before { + box-sizing: border-box; + position: absolute; + top: 40px; +} + +.checkbox-wrapper-54 .switch { + --width-of-switch: 3.5em; + --height-of-switch: 2em; + --size-of-icon: 1.4em; + --slider-offset: 0.3em; + position: relative; + width: var(--width-of-switch); + height: var(--height-of-switch); + display: inline-block; +} + +.checkbox-wrapper-54 .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #f4f4f5; + transition: .4s; + border-radius: 30px; + border: 1px solid #888; +} + +.checkbox-wrapper-54 .slider:before { + position: absolute; + content: ""; + height: var(--size-of-icon, 1.4em); + width: var(--size-of-icon, 1.4em); + border-radius: 20px; + left: var(--slider-offset, 0.3em); + top: 50%; + transform: translateY(-50%); + background: linear-gradient(40deg, #ff0080, #ff8c00 70%); + transition: .4s; +} + +.checkbox-wrapper-54 input:checked+.slider { + background-color: #303136; +} + +.checkbox-wrapper-54 input:checked+.slider:before { + left: calc(100% - (var(--size-of-icon, 1.4em) + var(--slider-offset, 0.3em))); + background: #303136; + box-shadow: inset -3px -2px 5px -2px #8983f7, inset -10px -4px 0 0 #a3dafb; +} + +.checkbox-wrapper { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.toggle-switch { + display: inline-block; + position: relative; + cursor: pointer; + outline: none; + user-select: none; +} + +.toggle-switch-input { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.toggle-switch-slider { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 50px; + background-color: #ddd; + -webkit-transition: .4s; + transition: .4s; +} + +.toggle-switch-slider:before { + position: absolute; + content: ""; + height: 22px; + width: 22px; + left: 2px; + bottom: 2px; + border-radius: 50%; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +.toggle-switch-input:checked+.toggle-switch-slider { + background-color: #2196F3; +} + +.toggle-switch-input:checked+.toggle-switch-slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +input:-webkit-autofill, +input:-webkit-autofill:focus, +input:-webkit-autofill:hover, +input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 1000px var(--color-background-primary) inset; + -webkit-text-fill-color: var(--color); +} + +@media (max-width: 450px) { + .checkbox-wrapper-54 { + position: absolute; + top: 0; + } + form { + width: 100%; + height: 100%; + margin: 0; + justify-content: center; + border-radius: 0; + gap: 30px; + } + form .inputBox { + width: 90%; + } + form .submit-button { + width: 90%; + } +} \ No newline at end of file diff --git a/src/public/variables.css b/src/public/variables.css new file mode 100644 index 0000000..8917421 --- /dev/null +++ b/src/public/variables.css @@ -0,0 +1,31 @@ +:root { + --color: #000; + --color-body: #f0f2f5; + --chat-body: #dedfdd; + --color-primary: #1877f2; + --color-terciary: #ccc; + --color-background-primary: #fff; + --chat-background-primary: #fff; + --color-background-secondary: #f0f2f5; + --color-selected: #d3d8e0; + --color-soft: #ebeef0; + --color-welcomeBox: #f0f2f5; + --color-messageBox: #efeae2; + --chatBackground: url("../Files/chat-background.png"); +} + +.dark-mode { + --color: #fff; + --color-body: #111322; + --chat-body: #000; + --color-primary: #997af1; + --color-terciary: #2b1b6c; + --color-background-primary: #000; + --chat-background-primary: #111; + --color-background-secondary: #222; + --color-selected: #333; + --color-soft: #222; + --color-welcomeBox: #10151d; + --color-messageBox: #10151d; + --chatBackground: url("../Files/chat-background-oscuro.png"); +} \ No newline at end of file diff --git a/src/routes/chat.js b/src/routes/chat.js new file mode 100644 index 0000000..d27cc57 --- /dev/null +++ b/src/routes/chat.js @@ -0,0 +1,6 @@ +module.exports = (app, path, requireLogout) => { + //GET + app.get("/chat", requireLogout, (req, res) => { + res.render(path.join(__dirname, "../views/chat")); + }); +}; \ No newline at end of file diff --git a/src/routes/createChannel.js b/src/routes/createChannel.js new file mode 100644 index 0000000..20e490f --- /dev/null +++ b/src/routes/createChannel.js @@ -0,0 +1,63 @@ +module.exports = (app, db, parseChannelsData, fs, path, io) => { + //POST + app.post("/createChannel", async (req, res) => { + const user = req.session.user; + if(!user) return + let { name, description, photo, participants } = req.body; + + participants.push(req.session.user.email); + const uniqueParticipants = participants.filter((participant, index) => { + return participants.indexOf(participant) === index; + }); + + if (uniqueParticipants.length < 2 || !name) return res.status(400).json({message: "El nombre del canal y los participantes son requeridos."}); + if (name.length > 70) return res.status(400).json({message: "El nombre del grupo no puede tener más de 70 caracteres."}); + if (description && description.length > 200)return res.status(400).json({message: "La descripción del grupo no puede tener más de 200 caracteres."}); + if (photo) { + if (!/^data:image\/[A-Za-z]+;base64,/.test(photo)) return res.status(400).json({ message: "El archivo enviado no es una imagen." }); + + const imageType = photo.split(";")[0].split("/")[1]; + const binary = Buffer.from(photo.split(",")[1], "base64"); + const imageFileName = `${Date.now()}-${Math.floor(Math.random() * 1000000)}.${imageType}`; + const imagePath = path.resolve(__dirname, "../Images", imageFileName); + + try { + await fs.promises.writeFile(imagePath, binary, "binary"); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Error al guardar la imagen." }); + } + photo = `Images/${imageFileName}`; + } + + let participantsIds = []; + + try { + for (const participant of uniqueParticipants) { + const query = await db.findUser("email", participant); + if (!query) return res.status(404).json({message: `El participante ${participant} no existe en la base de datos.`}); + participantsIds.push(query._id); + } + } catch (error) { + console.error(error); + return res.status(500).json({message: "Error al encontrar los participantes en la base de datos."}); + } + + try { + const createdChannel = await db.createChannel(name, description, photo, participantsIds); + const parsedChannel = await parseChannelsData([createdChannel]); + const dataObject = { + type: "channel", + channels: parsedChannel, + }; + for (const participant of participantsIds) { + io.to(participant.toString()).emit("renderData", dataObject); + } + res.status(200).json({success: true, message: "Se ha creado el canal exitosamente."}); + console.log(`Nuevo canal creado : ${name}`) + } catch (error) { + console.error(error); + return res.status(500).json({message: "Error al crear el canal en la base de datos."}); + } + }); +}; \ No newline at end of file diff --git a/src/routes/editUser.js b/src/routes/editUser.js new file mode 100644 index 0000000..9cdae4a --- /dev/null +++ b/src/routes/editUser.js @@ -0,0 +1,57 @@ +module.exports = (app, db, bcrypt, path, fs) => { + //PUT + app.put("/editUser", async (req, res) => { + const user = req.session.user; + if (!req.session.user) return; + let { photo, nickname, email, password, passwordConfirmation } = req.body; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + // Validaciones básicas + if (!nickname || !email) return res.status(400).json({ message: "No puedes tener un apodo o un correo vacío." }); + if (email && !emailRegex.test(email)) return res.status(400).json({ message: "El correo electrónico no es válido." }); + if (password && password.length < 8) return res.status(400).json({ message: "La contraseña debe tener al menos 8 caracteres." }); + if (password && passwordConfirmation && password !== passwordConfirmation) return res.status(400).json({ message: "Las contraseñas no coinciden." }); + if (user.email != email && (await db.findUser("email", email))) return res.status(400).json({ message: "El correo electrónico ya está registrado." }); + if (photo) { + if (!/^data:image\/[A-Za-z]+;base64,/.test(photo)) return res.status(400).json({ message: "El archivo enviado no es una imagen." }); + + const imageType = photo.split(";")[0].split("/")[1]; + const binary = Buffer.from(photo.split(",")[1], "base64"); + const imageFileName = `${Date.now()}-${Math.floor(Math.random() * 1000000)}.${imageType}`; + const imagePath = path.resolve(__dirname, "../Images", imageFileName); + + try { + await fs.promises.writeFile(imagePath, binary, "binary"); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Error al guardar la imagen." }); + } + photo = `Images/${imageFileName}`; + } + + try { + const updateData = { + photo, + nickname, + email, + }; + + if (password) { + const hashedPassword = await bcrypt.hash(password, 10); + updateData.passwordHash = hashedPassword; + } + + await db.updateUser(user._id, updateData); + + req.session.user = { + ...req.session.user, + ...updateData, + }; + + res.status(200).json({ success: true, message: "Se ha actualizado el usuario exitosamente." }); + console.log(`Se ha actualizado el usuario con el email ${email}`); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Error al actualizar el usuario en la base de datos." }); + } + }); +}; diff --git a/src/routes/getMessages.js b/src/routes/getMessages.js new file mode 100644 index 0000000..fa7ce6e --- /dev/null +++ b/src/routes/getMessages.js @@ -0,0 +1,14 @@ +module.exports = (app, db) => { + //GET + app.get("/messages", async (req, res) => { + // if (!req.session.user) return res.status(401).json({ error: "Usuario no autenticado." }); + try { + const { channel, limit, offset } = req.query; + const messages = await db.getMessages(channel, limit, offset); + res.status(200).json(messages); + } catch (error) { + console.error(`Error al obtener los mensajes del canal con la id "${channel}": \n${error}`); + res.status(500).json({message: "Error al obtener los mensajes" }); + } + }); +}; \ No newline at end of file diff --git a/src/routes/getUser.js b/src/routes/getUser.js new file mode 100644 index 0000000..529eac5 --- /dev/null +++ b/src/routes/getUser.js @@ -0,0 +1,30 @@ +module.exports = (app, db, fs, path) => { + //GET + app.get("/user", async (req, res) => { + if (!req.session.user) return res.status(401).json({ error: "Usuario no autenticado." }); + + const queryParams = req.query; + let key = Object.keys(queryParams)[0]; + let value = queryParams[key]; + + if (!key && !value) { + key = "email"; + value = req.session.user.email; + } + + let query = await db.findUser(key, value); + if (!query) return res.status(404).json({ error: "Usuario no encontrado." }); + + if (query.photo && fs.existsSync(path.resolve(__dirname, "..", query.photo))) { + let img = fs.readFileSync(path.resolve(__dirname, "..", query.photo)); + const base64Img = img.toString("base64"); + query.photo = `data:image/png;base64,${base64Img}`; + } else query.photo = "../Files/sin-foto.png"; + + return res.status(200).json({ + nickname: query.nickname, + email: query.email, + photo: query.photo, + }); + }); +}; diff --git a/src/routes/login.js b/src/routes/login.js new file mode 100644 index 0000000..7a110e5 --- /dev/null +++ b/src/routes/login.js @@ -0,0 +1,30 @@ +module.exports = (app, db, bcrypt, requireLogin, path) => { + + app.get("/login", requireLogin, (req, res) => { + res.render(path.join(__dirname, "../views/login")); + }); + + app.post("/login", async (req, res) => { + const { email, password } = req.body; + const query = await db.findUser("email", email); + + // Validaciones básicas + if (!email || !password) return res.status(400).json({ message: "Todos los campos son requeridos." }); + if (!query) return res.status(401).json({ message: "No existe una cuenta con esos datos." }); + + // Validar la contraseña + bcrypt.compare(password, query.passwordHash, (err, result) => { + if (err) { + console.error(err); + return res.status(500).json({ message: "Hubo un error en el servidor." }); + } + if (result === true) { + req.session.user = query; + res.status(200).json({ success: true, message: "Inicio de sesión exitoso." }); + console.log(`El usuario ${query.email} ha iniciado sesión.`); + } else { + return res.status(401).json({ message: "El correo electrónico o la contraseña son incorrectos." }); + } + }); + }); +}; \ No newline at end of file diff --git a/src/routes/logout.js b/src/routes/logout.js new file mode 100644 index 0000000..37f83e6 --- /dev/null +++ b/src/routes/logout.js @@ -0,0 +1,15 @@ +module.exports = (app) => { + app.get("/logout", (req, res) => { + if(!req.session.user) return + let email; + if (req.session.user) email = req.session.user.email; + req.session.destroy((error) => { + if (error) { + console.error(error); + } else { + if (typeof email !== "undefined") console.log(`El usuario ${email} ha cerrado sesión`); + res.redirect("/login"); + } + }); + }); +}; \ No newline at end of file diff --git a/src/routes/register.js b/src/routes/register.js new file mode 100644 index 0000000..9c0d74a --- /dev/null +++ b/src/routes/register.js @@ -0,0 +1,31 @@ +module.exports = (app, db, bcrypt, requireLogin, path) => { + //GET + app.get("/register", requireLogin, (req, res) => { + res.render(path.join(__dirname, "../views/register")); + }); + + //POST + app.post("/register", async (req, res) => { + const { nickname, email, password, passwordConfirmation } = req.body; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!nickname || !email || !password || !passwordConfirmation) return res.status(400).json({ message: "Todos los campos son requeridos." }); + if (email && !emailRegex.test(email)) return res.status(400).json({ message: "El correo electrónico debe tener un formato válido." }); + if (password && password.length < 8) return res.status(400).json({ message: "La contraseña debe tener al menos 8 caracteres." }); + if (password && passwordConfirmation && password !== passwordConfirmation) return res.status(400).json({ message: "Las contraseñas no coinciden." }); + if (await db.findUser("email", email)) return res.status(400).json({ message: "El correo electrónico ya está registrado." }); + + // Si no hay un error, encriptar la contraseña y registrar el usuario en la base de datos. + bcrypt.hash(password, 10, async (err, hashedPassword) => { + if (err) { + console.error(err); + return res.status(500).json({ message: "Hubo un error interno del servidor." }); + } else { + const query = await db.createUser(nickname, email, hashedPassword); + req.session.user = query; + res.status(200).json({ success: true, message: "Registro exitoso." }); + console.log(`Se ha registrado un nuevo usuario con el email ${email}`); + } + }); + }); +}; \ No newline at end of file diff --git a/src/views/chat.ejs b/src/views/chat.ejs new file mode 100644 index 0000000..be447bb --- /dev/null +++ b/src/views/chat.ejs @@ -0,0 +1,122 @@ + + + + + + + ChatApp + + + + + + + +
+
+
+ Foto de perfil +
+ more_vert + +
+
+ +
+
+
+
+
+ +

Envía mensajes de texto, fotos y videos de manera segura y rápida. Mantén conversaciones individuales o grupales.

+
+
+ add_circle + + send +
+
+
+ +
+

Mi perfil

+
+
+ +
+
+
+ + Apodo +
+
+ + Correo electrónico +
+
+ + Cambiar contraseña +
visibility
+
+
+ + Repetir contraseña +
+
+
+
+ + +
+
+
+ +
+
+
+
Información general
+ +
+ + Nombre +
+
+ + Descripción +
+
+
+
Participantes
+
+ + +
+
+
+ Foto de perfil +

+
+
+
+
+
+ + +
+
+
+ + + + + \ No newline at end of file diff --git a/src/views/login.ejs b/src/views/login.ejs new file mode 100644 index 0000000..f31637c --- /dev/null +++ b/src/views/login.ejs @@ -0,0 +1,33 @@ + + + + + + + ChatApp | Iniciar sesión + + + + + + + +
+
+ +
+ + Correo electrónico +
+
+ + Contraseña +
visibility
+
+ +

¿No tienes una cuenta? Regístrate

+
+ + + + \ No newline at end of file diff --git a/src/views/register.ejs b/src/views/register.ejs new file mode 100644 index 0000000..231278e --- /dev/null +++ b/src/views/register.ejs @@ -0,0 +1,45 @@ + + + + + + + ChatApp | Registrarse + + + + + + + +
+
+ +
+ + Apodo +
+ +
+ + Correo electrónico +
+ +
+ + Contraseña +
visibility
+
+ +
+ + Confirmar contraseña +
+ + +

¿Ya tienes una cuenta? Inicia sesión

+
+ + + + \ No newline at end of file