├── .gitignore ├── config ├── appConfig.js └── dbconfig.json ├── public ├── images │ ├── 1.jpg │ ├── 2.jpg │ └── 3.jpg ├── login.ejs └── chat.ejs ├── debug.log ├── migrations ├── 20170203112134-users-migration.js └── 20170204111728-messages-migration.js ├── routes ├── index.js ├── authenticationRoutes.js └── chatRoutes.js ├── package.json ├── models ├── index.js ├── messages.js └── users.js ├── seeders └── 20170825054100-createNewUsersToLoginWith.js ├── README.md └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea/* -------------------------------------------------------------------------------- /config/appConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'secret': 'iamthebest', 3 | }; -------------------------------------------------------------------------------- /public/images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirhamdy/real-time-chat/HEAD/public/images/1.jpg -------------------------------------------------------------------------------- /public/images/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirhamdy/real-time-chat/HEAD/public/images/2.jpg -------------------------------------------------------------------------------- /public/images/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirhamdy/real-time-chat/HEAD/public/images/3.jpg -------------------------------------------------------------------------------- /debug.log: -------------------------------------------------------------------------------- 1 | [0724/221757.280:ERROR:crash_report_database_win.cc(428)] unexpected header 2 | [0724/222830.241:ERROR:crash_report_database_win.cc(428)] unexpected header 3 | [0724/223359.930:ERROR:crash_report_database_win.cc(428)] unexpected header 4 | -------------------------------------------------------------------------------- /migrations/20170203112134-users-migration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var models = require("../models/index.js"); 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | 6 | return queryInterface.createTable(models.users.tableName, 7 | models.users.attributes); 8 | }, 9 | 10 | down: function (queryInterface, Sequelize) { 11 | return queryInterface.dropTable('users'); 12 | } 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /migrations/20170204111728-messages-migration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var models = require("../models/index.js"); 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | return queryInterface.createTable(models.messages.tableName, 6 | models.messages.attributes); 7 | }, 8 | 9 | down: function (queryInterface, Sequelize) { 10 | return queryInterface.dropTable('messages'); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /config/dbconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "root", 4 | "password": null, 5 | "database": "chat", 6 | "host": "127.0.0.1", 7 | "dialect": "mysql" 8 | }, 9 | "test": { 10 | "username": "root", 11 | "password": null, 12 | "database": "chat", 13 | "host": "127.0.0.1", 14 | "dialect": "mysql" 15 | }, 16 | "production": { 17 | "username": "root", 18 | "password": null, 19 | "database": "chat", 20 | "host": "127.0.0.1", 21 | "dialect": "mysql" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | var authenticationRouter =require("./authenticationRoutes"); 3 | var chatRouter = require('./chatRoutes'); 4 | // import cors from "cors"; 5 | 6 | module.exports= function ConfigApiRoutes(app) { 7 | // app.use(cors()); 8 | app.use('/api',authenticationRouter); 9 | app.use('/api',chatRouter); 10 | 11 | app.get('/', (req, res) => { 12 | res.render('login'); 13 | }); 14 | 15 | app.get('/chat', (req, res) => { 16 | res.render('chat'); 17 | }); 18 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat", 3 | "version": "1.0.0", 4 | "description": "Real Time chat using sockets.io", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "author": "amgad atef mohamed", 10 | "license": "ISC", 11 | "dependencies": { 12 | "body-parser": "^1.17.2", 13 | "ejs": "^2.5.7", 14 | "express": "^4.15.4", 15 | "jsonwebtoken": "^7.4.3", 16 | "md5": "^2.2.1", 17 | "mysql2": "^1.4.1", 18 | "open": "0.0.5", 19 | "sequelize": "^4.7.5", 20 | "socket.io": "^2.0.3" 21 | }, 22 | "devDependencies": { 23 | "nodemon": "^1.11.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var Sequelize = require('sequelize'); 6 | var basename = path.basename(module.filename); 7 | var env = process.env.NODE_ENV || 'development'; 8 | var config = require(__dirname + '/../config/dbconfig.json')[env]; 9 | var db = {}; 10 | 11 | if (config.use_env_variable) { 12 | var sequelize = new Sequelize(process.env[config.use_env_variable]); 13 | } else { 14 | var sequelize = new Sequelize(config.database, config.username, config.password, config); 15 | } 16 | 17 | fs 18 | .readdirSync(__dirname) 19 | .filter(function(file) { 20 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); 21 | }) 22 | .forEach(function(file) { 23 | var model = sequelize['import'](path.join(__dirname, file)); 24 | db[model.name] = model; 25 | }); 26 | 27 | Object.keys(db).forEach(function(modelName) { 28 | if (db[modelName].associate) { 29 | db[modelName].associate(db); 30 | } 31 | }); 32 | 33 | db.sequelize = sequelize; 34 | db.Sequelize = Sequelize; 35 | 36 | module.exports = db; 37 | -------------------------------------------------------------------------------- /routes/authenticationRoutes.js: -------------------------------------------------------------------------------- 1 | "user strict"; 2 | var express = require('express'); 3 | var authenticationRouter = express.Router(); 4 | var jwt = require('jsonwebtoken'); 5 | var appSeckertKey = require('../config/appConfig').secret; 6 | var models = require('../models/index'); 7 | var md5 = require('md5'); 8 | 9 | authenticationRouter.post('/login', function (req, res) { 10 | try { 11 | models.users.findOne({where:{email: req.body.email, password: md5(req.body.password) }}).then( user => { 12 | if (!user) 13 | return res.status(401).json({ success: false, message: 'Authentication failed. User not found.' }); 14 | 15 | jwt.sign({'user_id': user.id,'first_name':user.first_name,'email': user.email,'avatar_path': user.avatar_path}, 16 | appSeckertKey, {expiresIn: 60 * 60* 60*24}, (err, token) =>{ 17 | if (err) 18 | console.log(err); 19 | return res.status(200).json({ success: true,'userToken': token }); 20 | }); 21 | }).catch(error => res.status(400).json(error)); 22 | } 23 | catch (err) { 24 | return res.status(500).json({success: false, message: "There was an error attempting to login. Please try again later."}); 25 | } 26 | }); 27 | 28 | module.exports = authenticationRouter; -------------------------------------------------------------------------------- /seeders/20170825054100-createNewUsersToLoginWith.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var md5 = require('md5'); 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | 6 | return queryInterface.bulkInsert('users', [{ 7 | first_name: 'Amir Hamdy', 8 | email: 'amirhamdy4@gmail.com', 9 | password: md5('123456'), 10 | remember_token: 'remember_token', 11 | avatarPath: '1.jpg', 12 | isActive: 0, 13 | socketID: null 14 | },{ 15 | first_name: 'Ahmed Hamdy', 16 | email: 'ahmedhamdy4@gmail.com', 17 | password: md5('123456'), 18 | remember_token: 'remember_token', 19 | avatarPath: '2.jpg', 20 | isActive: 0, 21 | socketID: null 22 | },{ 23 | first_name: 'Abdo Hamdy', 24 | email: 'abdohamdy4@gmail.com', 25 | password: md5('123456'), 26 | remember_token: 'remember_token', 27 | avatarPath: '3.jpg', 28 | isActive: 0, 29 | socketID: null 30 | }], {}); 31 | }, 32 | 33 | down: function (queryInterface, Sequelize) { 34 | /* 35 | Add reverting commands here. 36 | Return a promise to correctly handle asynchronicity. 37 | 38 | Example: 39 | return queryInterface.bulkDelete('Person', null, {}); 40 | */ 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /models/messages.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function(sequelize, DataTypes) { 4 | var messages = sequelize.define("messages", { 5 | id: { 6 | type: DataTypes.INTEGER.UNSIGNED , 7 | primaryKey: true , 8 | autoIncrement: true 9 | }, 10 | message_subject : { 11 | type:DataTypes.STRING 12 | }, 13 | message_body :{ 14 | type: DataTypes.TEXT, 15 | allowNull:false 16 | } , 17 | sender_id : { 18 | type: DataTypes.INTEGER.UNSIGNED , 19 | allowNull: false, 20 | references: { 21 | model: "users", 22 | key: "id" 23 | } 24 | }, 25 | receiver_id :{ 26 | type: DataTypes.INTEGER.UNSIGNED , 27 | allowNull: false, 28 | references: { 29 | model: "users", 30 | key: "id" 31 | } 32 | }, 33 | conversation_id : { 34 | type:DataTypes.STRING, 35 | allowNull : false 36 | } , 37 | created_at : { 38 | type: 'TIMESTAMP' , 39 | allowNull: false, 40 | defaultValue: DataTypes.NOW 41 | }, 42 | updated_at :{ 43 | type: 'TIMESTAMP' , 44 | allowNull: false, 45 | defaultValue: DataTypes.NOW 46 | }, 47 | viewed : DataTypes.BOOLEAN 48 | }, 49 | { 50 | timestamps: false, 51 | freezeTableName:true, 52 | tableName: 'messages' 53 | }, { 54 | classMethods: { 55 | associate: function(models) { 56 | } 57 | } 58 | }); 59 | messages.associate = function(models) { 60 | messages.belongsTo(models.users, { 61 | onDelete: "CASCADE", 62 | foreignKey: 'sender_id', 63 | targetKey: 'id', 64 | as :'user' 65 | }); 66 | }; 67 | return messages; 68 | }; 69 | -------------------------------------------------------------------------------- /models/users.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var md5 = require('md5'); 3 | module.exports = function(sequelize, DataTypes) { 4 | var users = sequelize.define("users", { 5 | id: { 6 | type: DataTypes.INTEGER.UNSIGNED , 7 | primaryKey: true , 8 | autoIncrement: true 9 | }, 10 | first_name : { 11 | type:DataTypes.STRING, 12 | allowNull: false, 13 | }, 14 | email : { 15 | type:DataTypes.STRING, 16 | allowNull: false, 17 | unique:true 18 | } , 19 | password : { 20 | type:DataTypes.STRING, 21 | allowNull: false, 22 | set(val) { 23 | this.setDataValue('password', md5(val)); 24 | } 25 | }, 26 | remember_token :{ 27 | type:DataTypes.STRING, 28 | allowNull: false, 29 | }, 30 | created_at :{ 31 | type: 'TIMESTAMP' , 32 | allowNull: false, 33 | defaultValue: DataTypes.NOW 34 | }, 35 | updated_at :{ 36 | type: 'TIMESTAMP' , 37 | allowNull: false, 38 | defaultValue: DataTypes.NOW 39 | }, 40 | avatarPath : DataTypes.TEXT , 41 | isActive: DataTypes.BOOLEAN, 42 | socketID: { 43 | type:DataTypes.STRING(255), 44 | defaultValue:null, 45 | allowNull:true 46 | } 47 | 48 | } 49 | , 50 | { 51 | timestamps: false, 52 | freezeTableName:true, 53 | tableName: 'users' 54 | } 55 | , { 56 | classMethods: { 57 | associate: function(models) { 58 | } 59 | } 60 | }); 61 | users.associate = function(models) { 62 | users.hasMany(models.messages, { 63 | onDelete: "CASCADE", 64 | foreignKey:'sender_id', 65 | targetKey: 'id', 66 | as: 'userMessages' 67 | }); 68 | }; 69 | return users; 70 | }; 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A Real Time Chat Application built using Node.js, Express, MYSQL , Socket.io, JWT 2 | # Features 3 | * Uses Express as the application Framework. 4 | * Manages authentication using [express jwt package](https://www.npmjs.com/package/jwt-express). 5 | * Passwords are hashed using [md5 package](https://www.npmjs.com/package/md5) . 6 | * Real-time communication between a client and a server using [Socket.io](https://socket.io/). 7 | * Uses [Sequelize ORM](https://github.com/sequelize/sequelize) for storing and querying data to MYSQL. 8 | * - [x] After User Logging to the system , he can see all users in the system. 9 | * - [x] Can define each user is online or offline. 10 | * - [x] User can messaging with any user in the system , see all previous messages (private messages). 11 | * - [x] When user send a new message to another user , he get notification that he has a new message. 12 | 13 | # incomming Features: 14 | * - [ ] When user send message to another one and that one is offline , get get notification 15 | after login agian that he has new message from sender user. 16 | 17 | 18 | > # Installation steps: 19 | 20 | * make sure you have mysql , node js and npm installed in your operation system . 21 | * Create new database named chat . 22 | * Clone or Download the repository 23 | ``` 24 | git clone https://github.com/amirhamdy4b/real-time-chat.git 25 | ``` 26 | * Install Dependencies 27 | ``` 28 | npm install 29 | npm install sequelize-cli -g 30 | ``` 31 | * Edit configuration file in config/dbconfig.json with your credentials. 32 | * run migrations to create tables in database. 33 | ``` 34 | * sequelize db:migrate --config config/dbconfig.json 35 | ``` 36 | * run seeds to insert 5 new user in database to be used in the system. 37 | ``` 38 | * sequelize db:seed:all --config config/dbconfig.json 39 | ``` 40 | * Start the application 41 | ``` 42 | * npm start 43 | ``` 44 | Your app should now be running on localhost:3000. 45 | -------------------------------------------------------------------------------- /public/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Signin Template for Bootstrap 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 |
38 | 39 |
40 | 41 |

To Enter System chat

42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | "user strict"; 2 | 3 | var express = require('express'); 4 | var app = express(); 5 | var bodyParser = require('body-parser'); 6 | var routesConfig = require('./routes/index'); 7 | var open = require("open") 8 | , server = require('http').createServer(app) 9 | , io = require("socket.io")(server) 10 | ,model = require('./models/index'); 11 | var jwt = require('jsonwebtoken'); 12 | var appSeckertKey = require('./config/appConfig').secret; 13 | 14 | const PORT = process.env.port || 7000; 15 | const URL = '127.0.0.1'; 16 | app.set('views', __dirname + '/public'); 17 | app.use(express.static(__dirname + '/public')); 18 | app.use(express.static(__dirname+'/public/images')); 19 | app.use(bodyParser.urlencoded({ extend: false})); 20 | app.use(bodyParser()); 21 | app.set('view engine', 'ejs'); 22 | 23 | 24 | 25 | 26 | routesConfig(app); 27 | 28 | io.use((socket, next) => { 29 | let token = socket.handshake.query.token; 30 | jwt.verify(token, appSeckertKey, (err, decoded) => { 31 | if (err) { 32 | console.log(err); 33 | return next(new Error('authentication error')); 34 | } 35 | 36 | socket.decodedtoken = decoded; 37 | return next(); 38 | }); 39 | }); 40 | io.of('/chat').on('connection', function(socket) { 41 | console.log('a user connected'); 42 | console.log(socket.id); 43 | 44 | jwt.verify(socket.handshake.query.token, appSeckertKey, (err, decoded) => { 45 | let decodedToken = decoded; 46 | 47 | model.users.update({'isActive': 1, 'socketID': socket.id}, {where: {'id': decodedToken.user_id}}) 48 | .then(user => { 49 | console.log('user is online'); 50 | socket.broadcast.emit('online User', decodedToken.user_id); 51 | }); 52 | 53 | socket.on('disconnect', function (data) { 54 | model.users.update({'isActive': 0, 'socketID': null}, {where: {'id': decodedToken.user_id}}).then(user => { 55 | console.log('user is offline'); 56 | socket.broadcast.emit('disconnected User', decodedToken.user_id); 57 | }); 58 | console.log('user disconnected'); 59 | socket.emit('disconnected'); 60 | }); 61 | 62 | socket.on('message', function (msg) { 63 | console.log('new message'); 64 | model.users.findOne({where: {'id': decodedToken.user_id}}).then(user => { 65 | socket.to(msg.conversation_id).emit('new message', 66 | { 67 | message_body: msg.message_body, user: {avatarPath: user.avatarPath, first_name: user.first_name}, 68 | created_at: msg.created_at 69 | }); 70 | socket.emit(msg.conversation_id,message); 71 | }); 72 | 73 | model.users.findOne({where: {'id': msg.receiver_id}}).then(user => { 74 | socket.to(user.socketID).emit('notify', { 75 | message_body: msg.message_body, sender_id: decodedToken.user_id, 76 | created_at: msg.created_at 77 | }); 78 | }); 79 | }); 80 | 81 | 82 | socket.on('join room', function (roomname) { 83 | console.log('joined room ' + roomname); 84 | socket.join(roomname); 85 | }); 86 | 87 | 88 | }); 89 | }); 90 | 91 | server.listen(PORT, function (error) { 92 | if(error) 93 | console.log(error); 94 | console.log(`The Server Is Running ${URL}:${PORT}`); 95 | open(`http://${URL}:${PORT}`, "chrome"); 96 | }); -------------------------------------------------------------------------------- /routes/chatRoutes.js: -------------------------------------------------------------------------------- 1 | "user strict"; 2 | var express = require('express'); 3 | var chatRouter = express.Router(); 4 | var jwt = require('jsonwebtoken'); 5 | var appSeckertKey = require('../config/appConfig').secret; 6 | var model = require('../models/index'); 7 | 8 | chatRouter.use(function(req, res, next) { 9 | var token = req.body.token || req.query.token || req.headers['x-access-token']; 10 | if(!token) 11 | return res.status(403).json({success: false, message: 'No token provided.'}); 12 | 13 | jwt.verify(token, appSeckertKey, (err, decoded) => { 14 | if (err) { 15 | return res.status(403).json({ success: false, message: 'Failed to authenticate token.' }); 16 | } else { 17 | // if everything is good, save to request for use in other routes 18 | req.decoded = decoded; 19 | next(); 20 | } 21 | }); 22 | 23 | }); 24 | 25 | chatRouter.post('/users', function(req, res){ 26 | model.users.findAll({ where: {'id':{$ne: req.decoded.user_id}}, 27 | include:[{ 28 | model: model.messages, 29 | as: 'userMessages', 30 | order: [['id', 'DESC']], 31 | limit: 1}], 32 | order: [['id', 'DESC'],] 33 | }).then( users => { 34 | return res.status(200).json({success: true, message: users}); 35 | }).catch( error => { 36 | return res.status(400).json({success: false, message: error }); 37 | }); 38 | }); 39 | 40 | 41 | chatRouter.post('/messages', function(req, res){ 42 | var message = {'message_subject':'private Message', 'message_body':req.body.message_body, 43 | 'sender_id': req.decoded.user_id, 'receiver_id':req.body.receiver_id, 'conversation_id': req.body.conversation_id, 44 | 'delivered': 0}; 45 | model.messages.create(message) 46 | .then(userMessages => { 47 | model.users.findOne({where: { 'id': req.decoded.user_id }}).then( user => { 48 | message['user']= {avatarPath: user.avatarPath, first_name: user.first_name}; 49 | message['created_at']= userMessages.created_at; 50 | console.log(message); 51 | return res.status(200).json(message ); 52 | }); 53 | }).catch(error => res.status(400).json(error)); 54 | }); 55 | 56 | 57 | chatRouter.post('/chat/:id', function(req, res){ 58 | return model.messages.findAll({ 59 | where:{ 60 | $or:[ 61 | {'sender_id': req.params.id, 'receiver_id': req.decoded.user_id}, 62 | {'sender_id': req.decoded.user_id, 'receiver_id': req.params.id}, 63 | ] 64 | }, include: [{ 65 | model: model.users, 66 | as :'user' 67 | }] 68 | }).then(userMessages => res.status(200).json( userMessages)) 69 | .catch(error => res.status(400).json(error)); 70 | }); 71 | 72 | 73 | 74 | // chatRouter.post('/mymessages', function(req, res){ 75 | // // console.log(req.decoded); 76 | // model.users.findAll({where:{'id':{$ne: req.decoded.user_id}},include:[{ 77 | // model: model.messages, 78 | // as: 'userMessages', 79 | // order: [ 80 | // ['id', 'DESC'], 81 | // ], 82 | // limit: 1 83 | // }],order: [ 84 | // ['id', 'DESC'], 85 | // ],}).then( users => { 86 | // model.messages.findAll({where:{ $or: [ 87 | // {'sender_id': users[0].id, 'receiver_id': '2'}, 88 | // {'sender_id': '2', 'receiver_id': users[0].id} 89 | // ]}, 90 | // include:[{ 91 | // model:model.users, 92 | // as: 'user' 93 | // }]}).then( userMessages => { 94 | // // res.render('chat', {'users': users,userMessages: userMessages}); 95 | // return res.status(200).json({success: true, users: users, userMessages: userMessages}); 96 | // }); 97 | // }); 98 | // 99 | // }); 100 | 101 | // chatRouter.post('/singleThread', function(req, res){ 102 | // var msg = req.body; 103 | // return model.messages.findAll({ 104 | // where:{ 105 | // $or:[ 106 | // {'sender_id': msg.talkTo, 'receiver_id': req.decoded.user_id}, 107 | // {'sender_id': req.decoded.user_id, 'receiver_id': msg.talkTo}, 108 | // 109 | // ] 110 | // }, 111 | // include: [ 112 | // { 113 | // model: model.users, 114 | // as :'user' 115 | // } 116 | // ]}).then(userMessages => res.status(200).json(userMessages)) 117 | // .catch(error => res.status(400).json(error)); 118 | // }); 119 | module.exports = chatRouter; -------------------------------------------------------------------------------- /public/chat.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | chat room - Bootdey.com 16 | 17 | 18 | 19 | 20 | 296 | 297 | 298 | 336 | 337 | 338 |
339 |
340 |
341 |
342 | Messages 343 |
344 | 345 | 346 | 347 | 351 |
    352 | 353 |
354 |
355 | 356 | 357 | 358 |
359 |
360 |
    361 | 362 |
363 |
364 |
365 |
366 | 367 | 368 | 369 | 370 | 371 | 372 |
373 |
374 |
375 |
376 |
377 | 378 | 379 | 534 | 535 | 536 | --------------------------------------------------------------------------------