├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── server ├── config │ ├── main.js │ └── passport.js ├── controllers │ ├── authentication.js │ ├── chat.js │ └── user.js ├── index.js ├── models │ ├── channel.js │ ├── conversation.js │ ├── guest.js │ ├── message.js │ └── user.js ├── package-lock.json ├── package.json ├── router.js └── socketEvents.js └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── AddChannelBtn.js ├── AddDMBtn.js ├── Alert.js ├── ChatBox.js ├── ChatLists.js ├── ChatSelector.js ├── LoginForm.js ├── Navigation.js ├── PrivateMessaging.js ├── RegisterForm.js └── container │ ├── ChatUIContainer.js │ └── PrivateMessageContainer.js ├── index.css ├── index.js ├── logo.svg └── registerServiceWorker.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | server/node_modules 6 | client/node_modules 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Timothy Ip 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rr_challenge 2 | 3 | ![alt tag](https://user-images.githubusercontent.com/22341088/32709405-27d9da74-c7fe-11e7-9578-b40fefddfc77.png) 4 | 5 | Real-Time Chat app built using React, Express, Socket.io, Mongodb, Node.Js
6 | Features: 7 | - User Account Creation/Login 8 | - Guest Login 9 | - Real-time chat using socket.io 10 | - Tokens for API calls to backend 11 | - Cookies for saved session on browser refresh 12 | - Multiple custom channels 13 | - Private Messaging other users
14 | 15 | 16 | ### Installing 17 | ``` 18 | git clone https://github.com/TimothyIp/rr_challenge.git . 19 | npm install 20 | cd server (server has own dependencies) 21 | npm install 22 | cd .. 23 | npm start 24 | Go to http://localhost:8080/ 25 | ``` 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rrocket_code_challenge", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.17.0", 7 | "moment": "^2.19.1", 8 | "prop-types": "^15.6.0", 9 | "react": "^16.0.0", 10 | "react-cookie": "^2.1.1", 11 | "react-dom": "^16.0.0", 12 | "react-router": "^4.2.0", 13 | "react-scripts": "1.0.14", 14 | "socket.io-client": "^2.0.4" 15 | }, 16 | "scripts": { 17 | "start": "concurrently 'npm run react' 'npm run server'", 18 | "react": "PORT=8080 react-scripts start", 19 | "server": "nodemon server/index.js", 20 | "heroku-prebuild": "npm install && npm install --only=dev", 21 | "heroku-postbuild": "cd server/ && npm install && npm install --only=dev --no-shrinkwrap && npm run build", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test --env=jsdom", 24 | "eject": "react-scripts eject" 25 | }, 26 | "devDependencies": { 27 | "concurrently": "^3.5.0", 28 | "nodemon": "^1.12.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimothyIp/rr_challenge/f3d880acb1219ce30e5ffcf17e85e35c989a74fe/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Real-time Chatroom 12 | 13 | 14 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /server/config/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'secret': 'R_RocketChallenge', 3 | 'database': 'mongodb://bojang:rrcode1@code-challenge-shard-00-00-qthjy.mongodb.net:27017,code-challenge-shard-00-01-qthjy.mongodb.net:27017,code-challenge-shard-00-02-qthjy.mongodb.net:27017/rrcode?ssl=true&replicaSet=code-challenge-shard-0&authSource=admin', 4 | 'port': process.env.PORT || 3000 5 | } -------------------------------------------------------------------------------- /server/config/passport.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'), 2 | User = require('../models/user'), 3 | Guest = require('../models/guest'), 4 | config = require('./main'), 5 | JwtStrategy = require('passport-jwt').Strategy, 6 | ExtractJwt = require('passport-jwt').ExtractJwt, 7 | LocalStrategy = require('passport-local'); 8 | 9 | const localOptions = { 10 | usernameField: 'username' 11 | }; 12 | 13 | //Setting up local login strategy 14 | const localLogin = new LocalStrategy(localOptions, function(username, password, done) { 15 | User.findOne({ username }, function(err, user) { 16 | if (err) { 17 | return done(err); 18 | } 19 | if (!user) { 20 | return done(null, false, { 21 | error: 'Your login details could not be verified. Please try again.' 22 | }); 23 | } 24 | 25 | user.comparePassword(password, function(err, isMatch) { 26 | if (err) { 27 | return done(err); 28 | } 29 | 30 | if (!isMatch) { 31 | return done(null, false, { 32 | error: 'Your login details could not be verified. Please try again.' 33 | }); 34 | } 35 | 36 | return done(null, user); 37 | }); 38 | }); 39 | }); 40 | 41 | const jwtOptions = { 42 | // Tells passport to check authorization headers for JWT 43 | jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('jwt'), 44 | // Tells passport where to find secret 45 | secretOrKey: config.secret 46 | }; 47 | 48 | // JWT login strategy setup 49 | const jwtLogin = new JwtStrategy(jwtOptions, function(payload, done) { 50 | console.log(payload); 51 | User.findById(payload._id, function(err, user) { 52 | if (err) { 53 | return done(err, false); 54 | } 55 | 56 | if (user) { 57 | done(null, user); 58 | } else { 59 | // Reads guest token for api calls 60 | Guest.findOne(payload.guestName, function(err, guest) { 61 | if (err) { 62 | return done(err, false); 63 | } 64 | 65 | if (guest) { 66 | done(null, guest) 67 | } else { 68 | done(null, false); 69 | } 70 | }); 71 | } 72 | }); 73 | }); 74 | 75 | passport.use(jwtLogin); 76 | passport.use(localLogin); -------------------------------------------------------------------------------- /server/controllers/authentication.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const jwt = require('jsonwebtoken'), 4 | User = require('../models/user'), 5 | Guest = require('../models/guest'), 6 | config = require('../config/main'); 7 | 8 | function generateToken(user) { 9 | return jwt.sign(user, config.secret, { 10 | expiresIn: 7200 11 | }); 12 | } 13 | 14 | function setUserInfo(req) { 15 | return { 16 | _id: req._id, 17 | username: req.username, 18 | usersChannels: req.usersChannels 19 | } 20 | } 21 | 22 | // LOGIN ROUTE 23 | exports.login = function(req, res, next) { 24 | console.log(req.user) 25 | let userInfo = setUserInfo(req.user); 26 | 27 | res.status(200).json({ 28 | token: 'JWT ' + generateToken(userInfo), 29 | user: userInfo 30 | }) 31 | } 32 | 33 | // REGISTRATION ROUTE 34 | exports.register = function(req, res, next) { 35 | const username = req.body.username; 36 | const password = req.body.password; 37 | 38 | // Validating username and password 39 | if(!username) { 40 | return res.status(422).send({ 41 | error: 'You must enter a username.' 42 | }); 43 | } 44 | 45 | if (!password) { 46 | return res.status(422).send({ 47 | error: 'You must enter a password.' 48 | }) 49 | } 50 | 51 | // Looks for existing username and makes user account if no duplicate are found 52 | User.findOne({ username }, function(err, existingUser) { 53 | if (err) { 54 | return next(err); 55 | } 56 | 57 | if (existingUser) { 58 | return res.status(422).send({ 59 | error: 'That username is already in use.' 60 | }); 61 | } 62 | 63 | //If email is unique and password is provied -> create account 64 | let user = new User({ 65 | username: username, 66 | password: password, 67 | }); 68 | 69 | user.save(function(err, user) { 70 | if (err) { 71 | return next(err); 72 | } 73 | 74 | let userInfo = setUserInfo(user); 75 | 76 | res.status(200).json({ 77 | token: 'JWT ' + generateToken(userInfo), 78 | user: userInfo, 79 | message: "Successfully created your account." 80 | }); 81 | }); 82 | }); 83 | } 84 | 85 | // Guest login route 86 | exports.guestSignup = function(req, res, next) { 87 | const guestName = req.body.guestInputName; 88 | 89 | if (!guestName) { 90 | return res.status(422).json({ 91 | error: 'You must enter a username.' 92 | }); 93 | } 94 | 95 | // Looks for existing guest name in database 96 | Guest.findOne({ guestName }, function(err, existingGuest) { 97 | if (err) { 98 | return next(err); 99 | } 100 | 101 | if (existingGuest) { 102 | return res.status(422).send({ 103 | error: 'That Guest name is already taken.' 104 | }) 105 | } 106 | 107 | let guest = new Guest({ 108 | guestName 109 | }); 110 | 111 | // Checks against Usernames so there is no overlap 112 | User.findOne({ username: guestName}, function(err, existingUser) { 113 | if (err) { 114 | return next(err); 115 | } 116 | 117 | if (existingUser) { 118 | return res.status(422).send({ 119 | error: 'That Guest name is already taken.' 120 | }) 121 | } else { 122 | guest.save(function(err, user) { 123 | if (err) { 124 | return next(err); 125 | } 126 | 127 | // Generates a token for guests to be able to make certain api calls to the backend 128 | res.status(200).json({ 129 | token: 'JWT ' + generateToken({guest}), 130 | guestUser: {guest}, 131 | message: 'Sucessfully created a guest account' 132 | }) 133 | 134 | }); 135 | } 136 | }); 137 | 138 | }); 139 | } -------------------------------------------------------------------------------- /server/controllers/chat.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const Conversation = require('../models/conversation'), 4 | Message = require('../models/message'), 5 | User = require('../models/user'), 6 | Channel = require('../models/channel'); 7 | 8 | // Creates a conversation link between user and recipient for private messaging 9 | exports.newConversation = function(req, res, next) { 10 | const recipient = req.body.startDmInput; 11 | const userId = req.user._id 12 | 13 | if (!recipient) { 14 | res.status(422).send({ 15 | error: "Enter a valid recipient." 16 | }); 17 | return next(); 18 | } 19 | 20 | // Looks for a username with recipient name then creates a new Conversation schema with both user and 21 | // recipient in the participants array in the conversation model. 22 | User.findOne({ username: recipient }, function(err, foundRecipient) { 23 | if (err) { 24 | res.send({ 25 | error: err 26 | }); 27 | return next(err); 28 | } 29 | 30 | if (!foundRecipient) { 31 | return res.status(422).send({ 32 | error: 'Could not find recipient.' 33 | }); 34 | } 35 | 36 | // Adds both user id and recipient id to a participants array 37 | const conversation = new Conversation({ 38 | participants: [ req.user._id , foundRecipient._id ] 39 | }) 40 | 41 | conversation.save(function(err, newConversation) { 42 | if (err) { 43 | res.send({ 44 | error: err 45 | }); 46 | return next(err); 47 | } 48 | 49 | res.status(200).json({ 50 | message: `Started conversation with ${foundRecipient.username}`, 51 | recipientId: foundRecipient._id, 52 | recipient: foundRecipient.username, 53 | }) 54 | 55 | }); 56 | 57 | }); 58 | } 59 | 60 | // Lets users remove the current conversation in their user panel. 61 | exports.leaveConversation = function(req, res, next) { 62 | const conversationToLeave = req.body.conversationId; 63 | 64 | // Is given the recipient id and then looks in participants array in conversations 65 | Conversation.findOneAndRemove({ participants: conversationToLeave }, function(err, foundConversation){ 66 | if (err) { 67 | res.send({ 68 | error: err 69 | }); 70 | return next(err); 71 | } 72 | 73 | res.status(200).json({ 74 | message: 'Left from the Conversation.' 75 | }); 76 | return next(); 77 | }); 78 | } 79 | 80 | // Takes a channel name and message, then saves a new message with the id from the saved model of channel. 81 | exports.postToChannel = function(req, res, next) { 82 | const channelName = req.params.channelName; 83 | const composedMessage = req.body.composedMessage; 84 | 85 | if (!channelName) { 86 | res.status(422).json({ 87 | error: 'Enter a valid channel name.' 88 | }); 89 | return next(); 90 | } 91 | 92 | if (!composedMessage) { 93 | res.status(422).json({ 94 | error: 'Please enter a message.' 95 | }); 96 | } 97 | 98 | const channel = new Channel({ 99 | channelName 100 | }); 101 | 102 | channel.save(function(err, channelPost) { 103 | if (err) { 104 | res.send({ error: err }); 105 | return next(err); 106 | } 107 | 108 | // Tells mongodb which schema to reference, a guest or user collection to display the correct author for messages. 109 | const checkAuthor = () => { 110 | if (req.user.username) { 111 | let author = { 112 | kind: 'User', 113 | item: req.user._id 114 | } 115 | return author; 116 | } else { 117 | let guestAuthor = { 118 | kind: 'Guest', 119 | item: req.user._id 120 | } 121 | return guestAuthor; 122 | } 123 | }; 124 | 125 | const post = new Message({ 126 | conversationId: channelPost._id, 127 | body: composedMessage, 128 | author: [ checkAuthor() ], 129 | channelName, 130 | guestPost: req.user.guestName || "" 131 | }); 132 | 133 | post.save(function(err, newPost) { 134 | if (err) { 135 | res.send({ error: err }) 136 | return next(err); 137 | } 138 | 139 | res.status(200).json({ 140 | message: `Posted to channel ${channelName}`, 141 | conversationId: newPost._id, 142 | postedMessage: composedMessage 143 | }) 144 | }); 145 | }) 146 | } 147 | 148 | // Looks for channel conversations by looking up all the messages 149 | // that has the requested channel name when the message was saved. 150 | exports.getChannelConversations = function(req, res, next) { 151 | const channelName = req.params.channelName; 152 | 153 | Message.find({ channelName }) 154 | .select('createdAt body author guestPost') 155 | .sort('-createdAt') 156 | .populate('author.item') 157 | .exec((err, messages) => { 158 | if (err) { 159 | res.send({ error: err }); 160 | return next(err); 161 | } 162 | 163 | // Reversed the array so you get most recent messages on the button 164 | const getRecent = messages.reverse(); 165 | 166 | return res.status(200).json({ 167 | channelMessages: getRecent 168 | }) 169 | }) 170 | } 171 | 172 | // Gets a over log of active conversations the user is in. 173 | // Looks up the all the places where the participants is the user in conversation model 174 | // Returns all the different conversations where the participant is the user. 175 | exports.getConversations = function (req, res, next) { 176 | const username = req.user.username; 177 | 178 | // Show recent message from each conversation 179 | Conversation.find({ participants: req.user._id }) 180 | .sort('_id') 181 | .populate({ 182 | path: 'participants', 183 | select: 'username' 184 | }) 185 | .exec((err, conversations) => { 186 | if (err) { 187 | res.send({ error: err }); 188 | return next(err); 189 | } 190 | 191 | if (conversations.length === 0) { 192 | return res.status(200).json({ 193 | message: 'No conversations yet' 194 | }) 195 | } 196 | 197 | const conversationList = []; 198 | conversations.forEach((conversation) => { 199 | const conversationWith = conversation.participants.filter(item => { 200 | return item.username !== username 201 | }); 202 | 203 | conversationList.push(conversationWith[0]); 204 | if (conversationList.length === conversations.length) { 205 | return res.status(200).json({ 206 | conversationsWith: conversationList 207 | }) 208 | } 209 | }); 210 | }); 211 | }; 212 | 213 | // Takes a message, recipient id, and user id 214 | // Looks at all conversations where the recipient id and user id match in the participants array then gets that id 215 | // Using the conversation id, a reply is made with a new message with the same conversation Id 216 | exports.sendReply = function(req, res, next) { 217 | const privateMessage = req.body.privateMessageInput; 218 | const recipientId = req.body.recipientId; 219 | const userId = req.user._id; 220 | 221 | Conversation.findOne({ participants: {$all: [ userId, recipientId]} }, function(err, foundConversation) { 222 | if (err) { 223 | res.send({ 224 | errror: err 225 | }); 226 | return next(err); 227 | } 228 | 229 | if (!foundConversation) { 230 | return res.status(200).json({ 231 | message: 'Could not find conversation' 232 | }) 233 | } 234 | 235 | const reply = new Message({ 236 | conversationId: foundConversation._id, 237 | body: privateMessage, 238 | author: { 239 | kind: 'User', 240 | item: req.user._id 241 | } 242 | }) 243 | 244 | reply.save(function(err, sentReply) { 245 | if (err) { 246 | res.send({ 247 | error: err 248 | }); 249 | return next(err); 250 | } 251 | 252 | res.status(200).json({ 253 | message: 'Reply sent.' 254 | }); 255 | return next(); 256 | }); 257 | }); 258 | } 259 | 260 | // Gets a user id and recipient Id 261 | // Looks at all the conversations where the participants are the user id and recipient - this returns the conversation id if found 262 | // Using that conversation id, it looks through the messages for that conversation Id and returns all the messages for that conersation 263 | exports.getPrivateMessages = function(req, res, next) { 264 | const userId = req.user._id; 265 | const recipientId = req.params.recipientId; 266 | 267 | Conversation.findOne({ participants: {$all: [ userId, recipientId]}}, function(err, foundConversation) { 268 | if (err) { 269 | res.send({ 270 | error: err 271 | }); 272 | return next(err); 273 | } 274 | 275 | if (!foundConversation) { 276 | return res.status(200).json({ 277 | message: 'Could not find conversation' 278 | }) 279 | } 280 | 281 | Message.find({ conversationId: foundConversation._id }) 282 | .select('createdAt body author') 283 | .sort('-createdAt') 284 | .populate('author.item') 285 | .exec(function(err, message) { 286 | if (err) { 287 | res.send({ 288 | error: err 289 | }); 290 | return next(); 291 | } 292 | 293 | // Reverse to show most recent messages 294 | const sortedMessage = message.reverse(); 295 | 296 | res.status(200).json({ 297 | conversation: sortedMessage, 298 | conversationId: foundConversation._id 299 | }); 300 | }); 301 | }); 302 | } -------------------------------------------------------------------------------- /server/controllers/user.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const User = require('../models/user'); 4 | 5 | // Takes a channel name and the current username 6 | // If a user is found, that channel is pushed into the user's userChannel array 7 | exports.addChannel = function(req, res, next) { 8 | 9 | const channelToAdd = req.body.createInput; 10 | const username = req.user.username; 11 | 12 | User.findOne({ username }, function(err, user) { 13 | if (err) { 14 | res.send({ 15 | error: err 16 | }); 17 | return next(err); 18 | } 19 | 20 | if (!user) { 21 | return res.status(422).json({ 22 | error: 'Could not find user.' 23 | }) 24 | } 25 | 26 | // This prevents the user from joining duplicate channels 27 | if (user.usersChannels.indexOf(channelToAdd) == -1 ) { 28 | user.usersChannels.push(channelToAdd); 29 | } else { 30 | return res.status(422).json({ 31 | error: 'Already joined that channel.' 32 | }) 33 | } 34 | 35 | user.save(function(err, updatedUser) { 36 | if (err) { 37 | res.send({ 38 | error: err 39 | }); 40 | return next(err); 41 | } 42 | 43 | res.status(200).json({ 44 | message: 'Successfully joined channel.', 45 | channels: user.usersChannels 46 | }); 47 | }); 48 | }); 49 | } 50 | 51 | // Takes a channel name and username 52 | // If a user is found, it looks through the user's usersChannel array 53 | // The request channel to remove is filtered from the array and the user's info is saved again 54 | // Returns the new usersChannel array in the json response 55 | exports.removeChannel = function(req, res, next) { 56 | const channelName = req.body.channel; 57 | const username = req.user.username; 58 | 59 | User.findOne({ username }, function(err, user) { 60 | if (err) { 61 | res.send({ 62 | error: err 63 | }); 64 | return next(err); 65 | } 66 | 67 | if (!user) { 68 | return res.status(422).json({ 69 | error: 'Could not find user.' 70 | }); 71 | } 72 | 73 | // Removes the channel that was requested 74 | const removedChannel = user.usersChannels.filter(function(channel) { 75 | return channel !== channelName 76 | }) 77 | 78 | user.usersChannels = removedChannel; 79 | 80 | 81 | user.save(function(err, updatedUser) { 82 | if (err) { 83 | res.send({ 84 | error: err 85 | }); 86 | return next(err); 87 | } 88 | 89 | res.status(200).json({ 90 | message: `Removed channel: ${channelName}`, 91 | updatedChannels: user.usersChannels 92 | }); 93 | }); 94 | }); 95 | } 96 | 97 | // Given a username 98 | // Looks through Users for the username 99 | // If it can find a user, it returns all their current userChannels in a json response 100 | exports.getChannels = function(req, res, next) { 101 | const username = req.user.username; 102 | 103 | User.findOne({ username }, function(err, user) { 104 | if (err) { 105 | res.send({ 106 | error: err 107 | }); 108 | return next(err); 109 | } 110 | 111 | if (!user) { 112 | return res.status(422).json({ 113 | error: 'Could not find user.' 114 | }); 115 | } 116 | 117 | res.status(200).json({ 118 | message: 'Here are the users channels', 119 | usersChannels 120 | }); 121 | }); 122 | } -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'), 2 | app = express(); 3 | bodyParser = require('body-parser'), 4 | logger = require('morgan'), 5 | mongoose = require('mongoose'), 6 | config = require('./config/main'), 7 | router = require('./router'), 8 | socketEvents = require('./socketEvents'); 9 | 10 | // Connect to the database 11 | mongoose.connect(config.database); 12 | 13 | // Start the server 14 | const server = app.listen(config.port); 15 | console.log('The server is running on port ' + config.port + '.'); 16 | 17 | const io = require('socket.io').listen(server); 18 | socketEvents(io); 19 | 20 | // Middleware for Express requests 21 | app.use(logger('dev')); 22 | app.use(bodyParser.urlencoded({ extended: false })); 23 | app.use(bodyParser.json()); 24 | 25 | //Enables CORS from client-side 26 | app.use(function(req, res, next) { 27 | res.header("Access-Control-Allow-Origin", "*"); 28 | res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS'); 29 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials"); 30 | res.header("Access-Control-Allow-Credentials", "true"); 31 | next(); 32 | }); 33 | 34 | router(app); -------------------------------------------------------------------------------- /server/models/channel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'), 2 | Schema = mongoose.Schema; 3 | 4 | const ChannelSchema = new Schema({ 5 | channelName: { 6 | type: String, 7 | required: true 8 | }, 9 | }); 10 | 11 | module.exports = mongoose.model('Channel', ChannelSchema); -------------------------------------------------------------------------------- /server/models/conversation.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'), 2 | Schema = mongoose.Schema; 3 | 4 | const ConversationSchema = new Schema({ 5 | participants: [{ 6 | type: Schema.Types.ObjectId, 7 | ref: 'User' 8 | }], 9 | channelName: { 10 | type: String 11 | } 12 | }); 13 | 14 | module.exports = mongoose.model('Conversation', ConversationSchema); -------------------------------------------------------------------------------- /server/models/guest.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'), 2 | Schema = mongoose.Schema; 3 | 4 | // Creates a guest account that will expire in 2 hours 5 | const GuestSchema = new Schema({ 6 | guestName: { 7 | type: String, 8 | required: true, 9 | unique: true 10 | }, 11 | messages: { 12 | type: String 13 | }, 14 | // expire_at: { 15 | // type: Date, 16 | // default: Date.now, 17 | // expires: 7200 18 | // } 19 | }, 20 | { 21 | timestamps: true 22 | }) 23 | 24 | module.exports = mongoose.model('Guest', GuestSchema); -------------------------------------------------------------------------------- /server/models/message.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'), 2 | Schema = mongoose.Schema; 3 | 4 | const MessageSchema = new Schema({ 5 | conversationId: { 6 | type: Schema.Types.ObjectId, 7 | required: true 8 | }, 9 | body: { 10 | type: String 11 | }, 12 | author: [{ 13 | kind: String, 14 | item: { 15 | type: String, refPath: 'author.kind' 16 | } 17 | }], 18 | channelName: { 19 | type: String 20 | }, 21 | guestPost: { 22 | type: String 23 | } 24 | }, 25 | { 26 | timestamps: true 27 | }); 28 | 29 | module.exports = mongoose.model('Message', MessageSchema); -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'), 2 | Schema = mongoose.Schema, 3 | bcrypt = require('bcrypt-nodejs'); 4 | 5 | const UserSchema = new Schema({ 6 | username: { 7 | type: String, 8 | required: true, 9 | unique: true, 10 | }, 11 | password: { 12 | type: String, 13 | required: true 14 | }, 15 | messages: { 16 | type: String 17 | }, 18 | usersChannels: { 19 | type: Array, 20 | default: ['Public-Main'] 21 | } 22 | }, 23 | { 24 | timestamps: true 25 | }); 26 | 27 | // Hash password before saving if new or modified 28 | UserSchema.pre('save', function(next) { 29 | const user = this, 30 | SALT_FACTOR = 5; 31 | 32 | if (!user.isModified('password')) { 33 | return next(); 34 | } 35 | 36 | bcrypt.genSalt(SALT_FACTOR, function(err, salt) { 37 | if (err) { 38 | return next(err); 39 | } 40 | 41 | bcrypt.hash(user.password, salt, null, function(err, hash) { 42 | if (err) { 43 | return next(err); 44 | } 45 | user.password = hash; 46 | next(); 47 | }); 48 | }); 49 | }); 50 | 51 | //Compares password for login 52 | UserSchema.methods.comparePassword = function(candidatePassword, cb) { 53 | bcrypt.compare(candidatePassword, this.password, function(err, isMatch) { 54 | if (err) { 55 | return cb(err); 56 | } 57 | 58 | cb(null, isMatch); 59 | }); 60 | } 61 | 62 | module.exports = mongoose.model('User', UserSchema); -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filmsta_server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.4", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", 10 | "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", 11 | "requires": { 12 | "mime-types": "2.1.17", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "after": { 17 | "version": "0.8.2", 18 | "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", 19 | "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" 20 | }, 21 | "array-flatten": { 22 | "version": "1.1.1", 23 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 24 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 25 | }, 26 | "arraybuffer.slice": { 27 | "version": "0.0.6", 28 | "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", 29 | "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=" 30 | }, 31 | "async": { 32 | "version": "2.1.4", 33 | "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz", 34 | "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=", 35 | "requires": { 36 | "lodash": "4.17.4" 37 | } 38 | }, 39 | "backo2": { 40 | "version": "1.0.2", 41 | "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", 42 | "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" 43 | }, 44 | "base64-arraybuffer": { 45 | "version": "0.1.5", 46 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", 47 | "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" 48 | }, 49 | "base64id": { 50 | "version": "1.0.0", 51 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", 52 | "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" 53 | }, 54 | "base64url": { 55 | "version": "2.0.0", 56 | "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", 57 | "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" 58 | }, 59 | "basic-auth": { 60 | "version": "2.0.0", 61 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", 62 | "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", 63 | "requires": { 64 | "safe-buffer": "5.1.1" 65 | } 66 | }, 67 | "bcrypt-nodejs": { 68 | "version": "0.0.3", 69 | "resolved": "https://registry.npmjs.org/bcrypt-nodejs/-/bcrypt-nodejs-0.0.3.tgz", 70 | "integrity": "sha1-xgkX8m3CNWYVZsaBBhwwPCsohCs=" 71 | }, 72 | "better-assert": { 73 | "version": "1.0.2", 74 | "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", 75 | "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", 76 | "requires": { 77 | "callsite": "1.0.0" 78 | } 79 | }, 80 | "blob": { 81 | "version": "0.0.4", 82 | "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", 83 | "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=" 84 | }, 85 | "bluebird": { 86 | "version": "3.5.1", 87 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", 88 | "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" 89 | }, 90 | "body-parser": { 91 | "version": "1.18.2", 92 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 93 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 94 | "requires": { 95 | "bytes": "3.0.0", 96 | "content-type": "1.0.4", 97 | "debug": "2.6.9", 98 | "depd": "1.1.1", 99 | "http-errors": "1.6.2", 100 | "iconv-lite": "0.4.19", 101 | "on-finished": "2.3.0", 102 | "qs": "6.5.1", 103 | "raw-body": "2.3.2", 104 | "type-is": "1.6.15" 105 | } 106 | }, 107 | "bson": { 108 | "version": "1.0.4", 109 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", 110 | "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=" 111 | }, 112 | "buffer-equal-constant-time": { 113 | "version": "1.0.1", 114 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 115 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 116 | }, 117 | "buffer-shims": { 118 | "version": "1.0.0", 119 | "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", 120 | "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" 121 | }, 122 | "bytes": { 123 | "version": "3.0.0", 124 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 125 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 126 | }, 127 | "callsite": { 128 | "version": "1.0.0", 129 | "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", 130 | "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" 131 | }, 132 | "component-bind": { 133 | "version": "1.0.0", 134 | "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", 135 | "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" 136 | }, 137 | "component-emitter": { 138 | "version": "1.2.1", 139 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", 140 | "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" 141 | }, 142 | "component-inherit": { 143 | "version": "0.0.3", 144 | "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", 145 | "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" 146 | }, 147 | "content-disposition": { 148 | "version": "0.5.2", 149 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 150 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 151 | }, 152 | "content-type": { 153 | "version": "1.0.4", 154 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 155 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 156 | }, 157 | "cookie": { 158 | "version": "0.3.1", 159 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 160 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 161 | }, 162 | "cookie-signature": { 163 | "version": "1.0.6", 164 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 165 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 166 | }, 167 | "core-util-is": { 168 | "version": "1.0.2", 169 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 170 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 171 | }, 172 | "debug": { 173 | "version": "2.6.9", 174 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 175 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 176 | "requires": { 177 | "ms": "2.0.0" 178 | } 179 | }, 180 | "depd": { 181 | "version": "1.1.1", 182 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 183 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 184 | }, 185 | "destroy": { 186 | "version": "1.0.4", 187 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 188 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 189 | }, 190 | "ecdsa-sig-formatter": { 191 | "version": "1.0.9", 192 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", 193 | "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", 194 | "requires": { 195 | "base64url": "2.0.0", 196 | "safe-buffer": "5.1.1" 197 | } 198 | }, 199 | "ee-first": { 200 | "version": "1.1.1", 201 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 202 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 203 | }, 204 | "encodeurl": { 205 | "version": "1.0.1", 206 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", 207 | "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" 208 | }, 209 | "engine.io": { 210 | "version": "3.1.3", 211 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.1.3.tgz", 212 | "integrity": "sha1-euz3G/ijEPn6IUYZmcT8wDX4qHc=", 213 | "requires": { 214 | "accepts": "1.3.3", 215 | "base64id": "1.0.0", 216 | "cookie": "0.3.1", 217 | "debug": "2.6.9", 218 | "engine.io-parser": "2.1.1", 219 | "uws": "0.14.5", 220 | "ws": "2.3.1" 221 | }, 222 | "dependencies": { 223 | "accepts": { 224 | "version": "1.3.3", 225 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", 226 | "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", 227 | "requires": { 228 | "mime-types": "2.1.17", 229 | "negotiator": "0.6.1" 230 | } 231 | } 232 | } 233 | }, 234 | "engine.io-client": { 235 | "version": "3.1.3", 236 | "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.3.tgz", 237 | "integrity": "sha1-1wXkiYXf6LVKmMn3cFK4sIJYvgU=", 238 | "requires": { 239 | "component-emitter": "1.2.1", 240 | "component-inherit": "0.0.3", 241 | "debug": "2.6.9", 242 | "engine.io-parser": "2.1.1", 243 | "has-cors": "1.1.0", 244 | "indexof": "0.0.1", 245 | "parseqs": "0.0.5", 246 | "parseuri": "0.0.5", 247 | "ws": "2.3.1", 248 | "xmlhttprequest-ssl": "1.5.4", 249 | "yeast": "0.1.2" 250 | } 251 | }, 252 | "engine.io-parser": { 253 | "version": "2.1.1", 254 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.1.tgz", 255 | "integrity": "sha1-4Ps/DgRi9/WLt3waUun1p+JuRmg=", 256 | "requires": { 257 | "after": "0.8.2", 258 | "arraybuffer.slice": "0.0.6", 259 | "base64-arraybuffer": "0.1.5", 260 | "blob": "0.0.4", 261 | "has-binary2": "1.0.2" 262 | } 263 | }, 264 | "es6-promise": { 265 | "version": "3.2.1", 266 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", 267 | "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" 268 | }, 269 | "escape-html": { 270 | "version": "1.0.3", 271 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 272 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 273 | }, 274 | "etag": { 275 | "version": "1.8.1", 276 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 277 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 278 | }, 279 | "express": { 280 | "version": "4.16.2", 281 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", 282 | "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", 283 | "requires": { 284 | "accepts": "1.3.4", 285 | "array-flatten": "1.1.1", 286 | "body-parser": "1.18.2", 287 | "content-disposition": "0.5.2", 288 | "content-type": "1.0.4", 289 | "cookie": "0.3.1", 290 | "cookie-signature": "1.0.6", 291 | "debug": "2.6.9", 292 | "depd": "1.1.1", 293 | "encodeurl": "1.0.1", 294 | "escape-html": "1.0.3", 295 | "etag": "1.8.1", 296 | "finalhandler": "1.1.0", 297 | "fresh": "0.5.2", 298 | "merge-descriptors": "1.0.1", 299 | "methods": "1.1.2", 300 | "on-finished": "2.3.0", 301 | "parseurl": "1.3.2", 302 | "path-to-regexp": "0.1.7", 303 | "proxy-addr": "2.0.2", 304 | "qs": "6.5.1", 305 | "range-parser": "1.2.0", 306 | "safe-buffer": "5.1.1", 307 | "send": "0.16.1", 308 | "serve-static": "1.13.1", 309 | "setprototypeof": "1.1.0", 310 | "statuses": "1.3.1", 311 | "type-is": "1.6.15", 312 | "utils-merge": "1.0.1", 313 | "vary": "1.1.2" 314 | }, 315 | "dependencies": { 316 | "setprototypeof": { 317 | "version": "1.1.0", 318 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 319 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 320 | }, 321 | "statuses": { 322 | "version": "1.3.1", 323 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 324 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 325 | } 326 | } 327 | }, 328 | "finalhandler": { 329 | "version": "1.1.0", 330 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", 331 | "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", 332 | "requires": { 333 | "debug": "2.6.9", 334 | "encodeurl": "1.0.1", 335 | "escape-html": "1.0.3", 336 | "on-finished": "2.3.0", 337 | "parseurl": "1.3.2", 338 | "statuses": "1.3.1", 339 | "unpipe": "1.0.0" 340 | }, 341 | "dependencies": { 342 | "statuses": { 343 | "version": "1.3.1", 344 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 345 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 346 | } 347 | } 348 | }, 349 | "forwarded": { 350 | "version": "0.1.2", 351 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 352 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 353 | }, 354 | "fresh": { 355 | "version": "0.5.2", 356 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 357 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 358 | }, 359 | "has-binary2": { 360 | "version": "1.0.2", 361 | "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.2.tgz", 362 | "integrity": "sha1-6D26SfC5vk0CbSc2U1DZ8D9Uvpg=", 363 | "requires": { 364 | "isarray": "2.0.1" 365 | }, 366 | "dependencies": { 367 | "isarray": { 368 | "version": "2.0.1", 369 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", 370 | "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" 371 | } 372 | } 373 | }, 374 | "has-cors": { 375 | "version": "1.1.0", 376 | "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", 377 | "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" 378 | }, 379 | "hoek": { 380 | "version": "2.16.3", 381 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", 382 | "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" 383 | }, 384 | "hooks-fixed": { 385 | "version": "2.0.0", 386 | "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.0.tgz", 387 | "integrity": "sha1-oB2JTVKsf2WZu7H2PfycQR33DLo=" 388 | }, 389 | "http-errors": { 390 | "version": "1.6.2", 391 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 392 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 393 | "requires": { 394 | "depd": "1.1.1", 395 | "inherits": "2.0.3", 396 | "setprototypeof": "1.0.3", 397 | "statuses": "1.4.0" 398 | } 399 | }, 400 | "iconv-lite": { 401 | "version": "0.4.19", 402 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 403 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 404 | }, 405 | "indexof": { 406 | "version": "0.0.1", 407 | "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", 408 | "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" 409 | }, 410 | "inherits": { 411 | "version": "2.0.3", 412 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 413 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 414 | }, 415 | "ipaddr.js": { 416 | "version": "1.5.2", 417 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", 418 | "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" 419 | }, 420 | "isarray": { 421 | "version": "1.0.0", 422 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 423 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 424 | }, 425 | "isemail": { 426 | "version": "1.2.0", 427 | "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", 428 | "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" 429 | }, 430 | "joi": { 431 | "version": "6.10.1", 432 | "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", 433 | "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", 434 | "requires": { 435 | "hoek": "2.16.3", 436 | "isemail": "1.2.0", 437 | "moment": "2.19.1", 438 | "topo": "1.1.0" 439 | } 440 | }, 441 | "jsonwebtoken": { 442 | "version": "8.1.0", 443 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz", 444 | "integrity": "sha1-xjl80uX9WD1lwAeoPce7eOaYK4M=", 445 | "requires": { 446 | "jws": "3.1.4", 447 | "lodash.includes": "4.3.0", 448 | "lodash.isboolean": "3.0.3", 449 | "lodash.isinteger": "4.0.4", 450 | "lodash.isnumber": "3.0.3", 451 | "lodash.isplainobject": "4.0.6", 452 | "lodash.isstring": "4.0.1", 453 | "lodash.once": "4.1.1", 454 | "ms": "2.0.0", 455 | "xtend": "4.0.1" 456 | } 457 | }, 458 | "jwa": { 459 | "version": "1.1.5", 460 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", 461 | "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", 462 | "requires": { 463 | "base64url": "2.0.0", 464 | "buffer-equal-constant-time": "1.0.1", 465 | "ecdsa-sig-formatter": "1.0.9", 466 | "safe-buffer": "5.1.1" 467 | } 468 | }, 469 | "jws": { 470 | "version": "3.1.4", 471 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", 472 | "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", 473 | "requires": { 474 | "base64url": "2.0.0", 475 | "jwa": "1.1.5", 476 | "safe-buffer": "5.1.1" 477 | } 478 | }, 479 | "kareem": { 480 | "version": "1.5.0", 481 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.5.0.tgz", 482 | "integrity": "sha1-4+QQHZ3P3imXadr0tNtk2JXRdEg=" 483 | }, 484 | "lodash": { 485 | "version": "4.17.4", 486 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 487 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 488 | }, 489 | "lodash.includes": { 490 | "version": "4.3.0", 491 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 492 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" 493 | }, 494 | "lodash.isboolean": { 495 | "version": "3.0.3", 496 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 497 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" 498 | }, 499 | "lodash.isinteger": { 500 | "version": "4.0.4", 501 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 502 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" 503 | }, 504 | "lodash.isnumber": { 505 | "version": "3.0.3", 506 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 507 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" 508 | }, 509 | "lodash.isplainobject": { 510 | "version": "4.0.6", 511 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 512 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 513 | }, 514 | "lodash.isstring": { 515 | "version": "4.0.1", 516 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 517 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 518 | }, 519 | "lodash.once": { 520 | "version": "4.1.1", 521 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 522 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 523 | }, 524 | "media-typer": { 525 | "version": "0.3.0", 526 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 527 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 528 | }, 529 | "merge-descriptors": { 530 | "version": "1.0.1", 531 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 532 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 533 | }, 534 | "methods": { 535 | "version": "1.1.2", 536 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 537 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 538 | }, 539 | "mime": { 540 | "version": "1.4.1", 541 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 542 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 543 | }, 544 | "mime-db": { 545 | "version": "1.30.0", 546 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", 547 | "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" 548 | }, 549 | "mime-types": { 550 | "version": "2.1.17", 551 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", 552 | "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", 553 | "requires": { 554 | "mime-db": "1.30.0" 555 | } 556 | }, 557 | "moment": { 558 | "version": "2.19.1", 559 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.1.tgz", 560 | "integrity": "sha1-VtoaLRy/AdOLfhr8McELz6GSkWc=" 561 | }, 562 | "mongodb": { 563 | "version": "2.2.33", 564 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.33.tgz", 565 | "integrity": "sha1-tTfEcdNKZlG0jzb9vyl1A0Dgi1A=", 566 | "requires": { 567 | "es6-promise": "3.2.1", 568 | "mongodb-core": "2.1.17", 569 | "readable-stream": "2.2.7" 570 | } 571 | }, 572 | "mongodb-core": { 573 | "version": "2.1.17", 574 | "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.17.tgz", 575 | "integrity": "sha1-pBizN6FKFJkPtRC5I97mqBMXPfg=", 576 | "requires": { 577 | "bson": "1.0.4", 578 | "require_optional": "1.0.1" 579 | } 580 | }, 581 | "mongoose": { 582 | "version": "4.12.4", 583 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.12.4.tgz", 584 | "integrity": "sha512-3QTbQ/+wRe8Lr1IGTBAu2gyx+7VgvnCGmY2N1HSW8nsvfMGO0QW9iNDzZT3ceEY2Wctlt28wNavHivcLDO76bQ==", 585 | "requires": { 586 | "async": "2.1.4", 587 | "bson": "1.0.4", 588 | "hooks-fixed": "2.0.0", 589 | "kareem": "1.5.0", 590 | "mongodb": "2.2.33", 591 | "mpath": "0.3.0", 592 | "mpromise": "0.5.5", 593 | "mquery": "2.3.2", 594 | "ms": "2.0.0", 595 | "muri": "1.3.0", 596 | "regexp-clone": "0.0.1", 597 | "sliced": "1.0.1" 598 | } 599 | }, 600 | "morgan": { 601 | "version": "1.9.0", 602 | "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", 603 | "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", 604 | "requires": { 605 | "basic-auth": "2.0.0", 606 | "debug": "2.6.9", 607 | "depd": "1.1.1", 608 | "on-finished": "2.3.0", 609 | "on-headers": "1.0.1" 610 | } 611 | }, 612 | "mpath": { 613 | "version": "0.3.0", 614 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.3.0.tgz", 615 | "integrity": "sha1-elj3iem1/TyUUgY0FXlg8mvV70Q=" 616 | }, 617 | "mpromise": { 618 | "version": "0.5.5", 619 | "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.5.tgz", 620 | "integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY=" 621 | }, 622 | "mquery": { 623 | "version": "2.3.2", 624 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.2.tgz", 625 | "integrity": "sha512-KXWMypZSvhCuqRtza+HMQZdYw7PfFBjBTFvP31NNAq0OX0/NTIgpcDpkWQ2uTxk6vGQtwQ2elhwhs+ZvCA8OaA==", 626 | "requires": { 627 | "bluebird": "3.5.1", 628 | "debug": "2.6.9", 629 | "regexp-clone": "0.0.1", 630 | "sliced": "0.0.5" 631 | }, 632 | "dependencies": { 633 | "sliced": { 634 | "version": "0.0.5", 635 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", 636 | "integrity": "sha1-XtwETKTrb3gW1Qui/GPiXY/kcH8=" 637 | } 638 | } 639 | }, 640 | "ms": { 641 | "version": "2.0.0", 642 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 643 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 644 | }, 645 | "muri": { 646 | "version": "1.3.0", 647 | "resolved": "https://registry.npmjs.org/muri/-/muri-1.3.0.tgz", 648 | "integrity": "sha512-FiaFwKl864onHFFUV/a2szAl7X0fxVlSKNdhTf+BM8i8goEgYut8u5P9MqQqIYwvaMxjzVESsoEm/2kfkFH1rg==" 649 | }, 650 | "negotiator": { 651 | "version": "0.6.1", 652 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 653 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 654 | }, 655 | "object-component": { 656 | "version": "0.0.3", 657 | "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", 658 | "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" 659 | }, 660 | "on-finished": { 661 | "version": "2.3.0", 662 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 663 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 664 | "requires": { 665 | "ee-first": "1.1.1" 666 | } 667 | }, 668 | "on-headers": { 669 | "version": "1.0.1", 670 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", 671 | "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" 672 | }, 673 | "parseqs": { 674 | "version": "0.0.5", 675 | "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", 676 | "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", 677 | "requires": { 678 | "better-assert": "1.0.2" 679 | } 680 | }, 681 | "parseuri": { 682 | "version": "0.0.5", 683 | "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", 684 | "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", 685 | "requires": { 686 | "better-assert": "1.0.2" 687 | } 688 | }, 689 | "parseurl": { 690 | "version": "1.3.2", 691 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 692 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 693 | }, 694 | "passport": { 695 | "version": "0.4.0", 696 | "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", 697 | "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=", 698 | "requires": { 699 | "passport-strategy": "1.0.0", 700 | "pause": "0.0.1" 701 | } 702 | }, 703 | "passport-jwt": { 704 | "version": "3.0.0", 705 | "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-3.0.0.tgz", 706 | "integrity": "sha1-fZ5P8PoFIhCFQM4fz6g7vY+qKck=", 707 | "requires": { 708 | "jsonwebtoken": "7.4.3", 709 | "passport-strategy": "1.0.0" 710 | }, 711 | "dependencies": { 712 | "jsonwebtoken": { 713 | "version": "7.4.3", 714 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.3.tgz", 715 | "integrity": "sha1-d/UCHeBYtgWheD+hKD6ZgS5kVjg=", 716 | "requires": { 717 | "joi": "6.10.1", 718 | "jws": "3.1.4", 719 | "lodash.once": "4.1.1", 720 | "ms": "2.0.0", 721 | "xtend": "4.0.1" 722 | } 723 | } 724 | } 725 | }, 726 | "passport-local": { 727 | "version": "1.0.0", 728 | "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", 729 | "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", 730 | "requires": { 731 | "passport-strategy": "1.0.0" 732 | } 733 | }, 734 | "passport-strategy": { 735 | "version": "1.0.0", 736 | "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", 737 | "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" 738 | }, 739 | "path-to-regexp": { 740 | "version": "0.1.7", 741 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 742 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 743 | }, 744 | "pause": { 745 | "version": "0.0.1", 746 | "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", 747 | "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" 748 | }, 749 | "process-nextick-args": { 750 | "version": "1.0.7", 751 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", 752 | "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" 753 | }, 754 | "proxy-addr": { 755 | "version": "2.0.2", 756 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", 757 | "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", 758 | "requires": { 759 | "forwarded": "0.1.2", 760 | "ipaddr.js": "1.5.2" 761 | } 762 | }, 763 | "qs": { 764 | "version": "6.5.1", 765 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 766 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 767 | }, 768 | "range-parser": { 769 | "version": "1.2.0", 770 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 771 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 772 | }, 773 | "raw-body": { 774 | "version": "2.3.2", 775 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 776 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 777 | "requires": { 778 | "bytes": "3.0.0", 779 | "http-errors": "1.6.2", 780 | "iconv-lite": "0.4.19", 781 | "unpipe": "1.0.0" 782 | } 783 | }, 784 | "readable-stream": { 785 | "version": "2.2.7", 786 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", 787 | "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", 788 | "requires": { 789 | "buffer-shims": "1.0.0", 790 | "core-util-is": "1.0.2", 791 | "inherits": "2.0.3", 792 | "isarray": "1.0.0", 793 | "process-nextick-args": "1.0.7", 794 | "string_decoder": "1.0.3", 795 | "util-deprecate": "1.0.2" 796 | } 797 | }, 798 | "regexp-clone": { 799 | "version": "0.0.1", 800 | "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", 801 | "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" 802 | }, 803 | "require_optional": { 804 | "version": "1.0.1", 805 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", 806 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", 807 | "requires": { 808 | "resolve-from": "2.0.0", 809 | "semver": "5.4.1" 810 | } 811 | }, 812 | "resolve-from": { 813 | "version": "2.0.0", 814 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", 815 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" 816 | }, 817 | "safe-buffer": { 818 | "version": "5.1.1", 819 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 820 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 821 | }, 822 | "semver": { 823 | "version": "5.4.1", 824 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", 825 | "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" 826 | }, 827 | "send": { 828 | "version": "0.16.1", 829 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", 830 | "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", 831 | "requires": { 832 | "debug": "2.6.9", 833 | "depd": "1.1.1", 834 | "destroy": "1.0.4", 835 | "encodeurl": "1.0.1", 836 | "escape-html": "1.0.3", 837 | "etag": "1.8.1", 838 | "fresh": "0.5.2", 839 | "http-errors": "1.6.2", 840 | "mime": "1.4.1", 841 | "ms": "2.0.0", 842 | "on-finished": "2.3.0", 843 | "range-parser": "1.2.0", 844 | "statuses": "1.3.1" 845 | }, 846 | "dependencies": { 847 | "statuses": { 848 | "version": "1.3.1", 849 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 850 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 851 | } 852 | } 853 | }, 854 | "serve-static": { 855 | "version": "1.13.1", 856 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", 857 | "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", 858 | "requires": { 859 | "encodeurl": "1.0.1", 860 | "escape-html": "1.0.3", 861 | "parseurl": "1.3.2", 862 | "send": "0.16.1" 863 | } 864 | }, 865 | "setprototypeof": { 866 | "version": "1.0.3", 867 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 868 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 869 | }, 870 | "sliced": { 871 | "version": "1.0.1", 872 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", 873 | "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" 874 | }, 875 | "socket.io": { 876 | "version": "2.0.4", 877 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.4.tgz", 878 | "integrity": "sha1-waRZDO/4fs8TxyZS8Eb3FrKeYBQ=", 879 | "requires": { 880 | "debug": "2.6.9", 881 | "engine.io": "3.1.3", 882 | "socket.io-adapter": "1.1.1", 883 | "socket.io-client": "2.0.4", 884 | "socket.io-parser": "3.1.2" 885 | } 886 | }, 887 | "socket.io-adapter": { 888 | "version": "1.1.1", 889 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", 890 | "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" 891 | }, 892 | "socket.io-client": { 893 | "version": "2.0.4", 894 | "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.4.tgz", 895 | "integrity": "sha1-CRilUkBtxeVAs4Dc2Xr8SmQzL44=", 896 | "requires": { 897 | "backo2": "1.0.2", 898 | "base64-arraybuffer": "0.1.5", 899 | "component-bind": "1.0.0", 900 | "component-emitter": "1.2.1", 901 | "debug": "2.6.9", 902 | "engine.io-client": "3.1.3", 903 | "has-cors": "1.1.0", 904 | "indexof": "0.0.1", 905 | "object-component": "0.0.3", 906 | "parseqs": "0.0.5", 907 | "parseuri": "0.0.5", 908 | "socket.io-parser": "3.1.2", 909 | "to-array": "0.1.4" 910 | } 911 | }, 912 | "socket.io-parser": { 913 | "version": "3.1.2", 914 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz", 915 | "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I=", 916 | "requires": { 917 | "component-emitter": "1.2.1", 918 | "debug": "2.6.9", 919 | "has-binary2": "1.0.2", 920 | "isarray": "2.0.1" 921 | }, 922 | "dependencies": { 923 | "isarray": { 924 | "version": "2.0.1", 925 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", 926 | "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" 927 | } 928 | } 929 | }, 930 | "statuses": { 931 | "version": "1.4.0", 932 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 933 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 934 | }, 935 | "string_decoder": { 936 | "version": "1.0.3", 937 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", 938 | "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", 939 | "requires": { 940 | "safe-buffer": "5.1.1" 941 | } 942 | }, 943 | "to-array": { 944 | "version": "0.1.4", 945 | "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", 946 | "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" 947 | }, 948 | "topo": { 949 | "version": "1.1.0", 950 | "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", 951 | "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", 952 | "requires": { 953 | "hoek": "2.16.3" 954 | } 955 | }, 956 | "type-is": { 957 | "version": "1.6.15", 958 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", 959 | "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", 960 | "requires": { 961 | "media-typer": "0.3.0", 962 | "mime-types": "2.1.17" 963 | } 964 | }, 965 | "ultron": { 966 | "version": "1.1.0", 967 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz", 968 | "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ=" 969 | }, 970 | "unpipe": { 971 | "version": "1.0.0", 972 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 973 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 974 | }, 975 | "util-deprecate": { 976 | "version": "1.0.2", 977 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 978 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 979 | }, 980 | "utils-merge": { 981 | "version": "1.0.1", 982 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 983 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 984 | }, 985 | "uws": { 986 | "version": "0.14.5", 987 | "resolved": "https://registry.npmjs.org/uws/-/uws-0.14.5.tgz", 988 | "integrity": "sha1-Z6rzPEaypYel9mZtAPdpEyjxSdw=", 989 | "optional": true 990 | }, 991 | "vary": { 992 | "version": "1.1.2", 993 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 994 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 995 | }, 996 | "ws": { 997 | "version": "2.3.1", 998 | "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz", 999 | "integrity": "sha1-a5Sz5EfLajY/eF6vlK9jWejoHIA=", 1000 | "requires": { 1001 | "safe-buffer": "5.0.1", 1002 | "ultron": "1.1.0" 1003 | }, 1004 | "dependencies": { 1005 | "safe-buffer": { 1006 | "version": "5.0.1", 1007 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", 1008 | "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" 1009 | } 1010 | } 1011 | }, 1012 | "xmlhttprequest-ssl": { 1013 | "version": "1.5.4", 1014 | "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.4.tgz", 1015 | "integrity": "sha1-BPVgkVcks4kIhxXMDteBPpZ3v1c=" 1016 | }, 1017 | "xtend": { 1018 | "version": "4.0.1", 1019 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 1020 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" 1021 | }, 1022 | "yeast": { 1023 | "version": "0.1.2", 1024 | "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", 1025 | "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" 1026 | } 1027 | } 1028 | } 1029 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filmsta_server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Timothy Ip", 10 | "license": "ISC", 11 | "dependencies": { 12 | "bcrypt-nodejs": "0.0.3", 13 | "body-parser": "^1.18.2", 14 | "express": "^4.16.2", 15 | "jsonwebtoken": "^8.1.0", 16 | "mongoose": "^4.12.3", 17 | "morgan": "^1.9.0", 18 | "passport": "^0.4.0", 19 | "passport-jwt": "^3.0.0", 20 | "passport-local": "^1.0.0", 21 | "socket.io": "^2.0.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/router.js: -------------------------------------------------------------------------------- 1 | const AuthenticationController = require('./controllers/authentication'), 2 | express = require('express'), 3 | passportService = require('./config/passport'), 4 | passport = require('passport'), 5 | ChatController = require('./controllers/chat'), 6 | UserController = require('./controllers/user'); 7 | 8 | // Middleware for login/auth 9 | const requireAuth = passport.authenticate('jwt', { session: false }); 10 | const requireLogin = passport.authenticate('local', { session: false }); 11 | 12 | module.exports = function(app) { 13 | const apiRoutes = express.Router(), 14 | authRoutes = express.Router(), 15 | chatRoutes = express.Router(), 16 | userRoutes = express.Router(); 17 | 18 | // Auth Routes 19 | apiRoutes.use('/auth', authRoutes); 20 | 21 | // Registration Route 22 | authRoutes.post('/register', AuthenticationController.register); 23 | 24 | authRoutes.post('/login', requireLogin, AuthenticationController.login); 25 | 26 | authRoutes.post('/guest', AuthenticationController.guestSignup); 27 | 28 | // Chat Routes 29 | apiRoutes.use('/chat', chatRoutes); 30 | 31 | // View messages from users 32 | chatRoutes.get('/', requireAuth, ChatController.getConversations); 33 | 34 | // Gets individual conversations 35 | // chatRoutes.get('/:conversationId', requireAuth, ChatController.getConversation); 36 | 37 | // Gets Private conversations 38 | chatRoutes.get('/privatemessages/:recipientId', requireAuth, ChatController.getPrivateMessages); 39 | 40 | // Start new conversation 41 | chatRoutes.post('/new', requireAuth, ChatController.newConversation); 42 | 43 | chatRoutes.post('/leave', requireAuth, ChatController.leaveConversation); 44 | 45 | // Reply to conversations 46 | chatRoutes.post('/reply', requireAuth, ChatController.sendReply); 47 | 48 | // View Chat Channel messages 49 | chatRoutes.get('/channel/:channelName', ChatController.getChannelConversations); 50 | 51 | // Post to Channel 52 | chatRoutes.post('/postchannel/:channelName', requireAuth, ChatController.postToChannel); 53 | 54 | // User Routes 55 | apiRoutes.use('/user', userRoutes); 56 | 57 | // Gets user's joined channels 58 | userRoutes.get('/getchannels', requireAuth, UserController.getChannels); 59 | 60 | // Add to user's channels 61 | userRoutes.post('/addchannel', requireAuth, UserController.addChannel); 62 | 63 | // Remove from user's channels 64 | userRoutes.post('/removechannel', requireAuth, UserController.removeChannel) 65 | 66 | // Set URL for API groups 67 | app.use('/api', apiRoutes); 68 | 69 | } -------------------------------------------------------------------------------- /server/socketEvents.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = function(io) { 2 | //Set Listeners 3 | io.on('connection', (socket)=> { 4 | console.log('a user has connected'); 5 | 6 | socket.on('enter channel', (channel, username) => { 7 | if (username) { 8 | socket.join(channel); 9 | io.sockets.in(channel).emit('user joined', `${username} has joined the channel`) 10 | console.log('user has joined channel' , channel, username) 11 | } else { 12 | return false 13 | } 14 | }); 15 | 16 | socket.on('leave channel', (channel, username) => { 17 | socket.leave(channel); 18 | io.sockets.in(channel).emit('user left', `${username} has left the channel`); 19 | console.log('user has left channel', channel, username) 20 | }); 21 | 22 | socket.on('new message', (socketMsg) => { 23 | io.sockets.in(socketMsg.channel).emit('refresh messages', socketMsg); 24 | console.log('new message received in channel', socketMsg) 25 | }); 26 | 27 | socket.on('enter privateMessage', (conversationId) => { 28 | socket.join(conversationId); 29 | }); 30 | 31 | socket.on('leave privateMessage', (conversationId) => { 32 | socket.leave(conversationId); 33 | }) 34 | 35 | socket.on('new privateMessage', (socketMsg) => { 36 | io.sockets.in(socketMsg.conversationId).emit('refresh privateMessages', socketMsg); 37 | }) 38 | 39 | socket.on('user typing', (data) => { 40 | io.sockets.in(data.conversationId).emit('typing', data) 41 | }) 42 | 43 | socket.on('disconnect', () => { 44 | console.log('user disconnected') 45 | }); 46 | }); 47 | } -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | article,aside,details,figcaption,figure,footer,header,hgroup,nav,section,summary{display:block;}audio,canvas,video{display:inline-block;}audio:not([controls]){display:none;height:0;}[hidden]{display:none;}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}a:focus{outline:thin dotted;}a:active,a:hover{outline:0;}h1{font-size:2em;}abbr[title]{border-bottom:1px dotted;}b,strong{font-weight:700;}dfn{font-style:italic;}mark{background:#ff0;color:#000;}code,kbd,pre,samp{font-family:monospace, serif;font-size:1em;}pre{white-space:pre-wrap;word-wrap:break-word;}q{quotes:\201C \201D \2018 \2019;}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sup{top:-.5em;}sub{bottom:-.25em;}img{border:0;}svg:not(:root){overflow:hidden;}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em;}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;}button,input{line-height:normal;}button,html input[type=button],/* 1 */ 2 | input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;}button[disabled],input[disabled]{cursor:default;}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0;}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none;}textarea{overflow:auto;vertical-align:top;}table{border-collapse:collapse;border-spacing:0;}body,figure{margin:0;}legend,button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;} 3 | 4 | .clearfix:after {visibility: hidden; display: block; font-size: 0; content: " "; clear: both; height: 0; } 5 | 6 | * { box-sizing: border-box; } 7 | 8 | /* NORMALIZE */ 9 | 10 | html, body { 11 | height: 100%; 12 | font-size: 16px; 13 | font-family: 'Open Sans', sans-serif; 14 | } 15 | 16 | button { 17 | padding: 0; 18 | } 19 | 20 | #root { 21 | height: 100%; 22 | } 23 | 24 | .app--container { 25 | height: 85vh; 26 | } 27 | 28 | .app--container h2, 29 | .app--container p { 30 | text-align: center; 31 | } 32 | 33 | .app--container--btn { 34 | text-align: center; 35 | } 36 | 37 | .app--container--btn button { 38 | display: block; 39 | margin: 0 auto; 40 | background-color: #3ebfac; 41 | border: none; 42 | outline: none; 43 | padding: 1rem; 44 | color: #fff; 45 | border-radius: 4px; 46 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 47 | 0 1px 5px 0 rgba(0, 0, 0, .12), 48 | 0 3px 1px -2px rgba(0, 0, 0, .2); 49 | } 50 | 51 | .chatapp__container { 52 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 53 | border-radius: 18px; 54 | display: flex; 55 | flex-direction: column; 56 | margin: 2rem; 57 | min-height: 80vh; 58 | height: 100%; 59 | position: relative; 60 | } 61 | 62 | .chatapp__navigation--container { 63 | display: flex; 64 | justify-content: space-between; 65 | align-items: center; 66 | /* background-color: #3ebfac; */ 67 | background-color: #50586b; 68 | border-bottom: 6px solid #3ebfac; 69 | border-top-left-radius: 18px; 70 | border-top-right-radius: 18px; 71 | height: 5rem; 72 | color: #fff; 73 | font-size: 1rem; 74 | letter-spacing: 1.5px; 75 | font-weight: 300; 76 | padding-left: 1.5rem; 77 | padding-right: 1.5rem; 78 | align-items: center; 79 | } 80 | 81 | .chatapp__navigation--logo:hover { 82 | cursor: pointer; 83 | color: #3ebfac; 84 | } 85 | 86 | .chatapp__navigation--user { 87 | display: flex; 88 | flex-direction: row; 89 | align-items: center; 90 | } 91 | 92 | .chatapp__navigation--user span { 93 | display: block; 94 | margin-right: 1.5rem; 95 | } 96 | 97 | .chatapp__navigation--user button { 98 | background-color: #3ebfac; 99 | border: none; 100 | border-radius: 2px; 101 | height: 2rem; 102 | min-width: 4rem; 103 | font-size: 0.875rem; 104 | color: #fff; 105 | padding-left: 1rem; 106 | padding-right: 1rem; 107 | } 108 | 109 | .chatapp__navigation--user button:nth-of-type(1) { 110 | background-color: inherit; 111 | border: 1px solid #3ebfac; 112 | } 113 | 114 | .chatapp__navigation--user button:nth-of-type(1):hover { 115 | background-color: #3ebfac; 116 | } 117 | 118 | .chatapp__navigation--user button:nth-of-type(2) { 119 | margin-left: 1rem; 120 | } 121 | 122 | .chatapp__form--container { 123 | background: linear-gradient(135deg, rgba(36,46,77, 0.9), rgba(137,126,121,0.9)); 124 | padding-left: 1.5rem; 125 | padding-right: 1.5rem; 126 | border-bottom-left-radius: 18px; 127 | border-bottom-right-radius: 18px; 128 | height: calc(100% - 4.5rem); 129 | } 130 | 131 | .chatapp__form--modal form { 132 | display: flex; 133 | flex-direction: column; 134 | width: 100%; 135 | } 136 | 137 | .chatapp__form--modal input { 138 | font-family: FontAwesome, 'Open Sans', sans-serif; 139 | border: none; 140 | outline: none; 141 | width: 100%; 142 | height: 20rem; 143 | margin-bottom: 1rem; 144 | height: 3rem; 145 | padding: 1rem; 146 | } 147 | 148 | .chatapp__form--container input::placeholder { 149 | color: #8f8f8f; 150 | } 151 | 152 | .chatapp__form--modal { 153 | font-size: 16px; 154 | position: absolute; 155 | top: 50%; 156 | left: 50%; 157 | transform: translate(-50%, -50%); 158 | width: 28.5rem; 159 | padding: 3rem; 160 | height: auto; 161 | background: rgba(0,0,0,0.2); 162 | } 163 | 164 | .chatapp__form--modal:before { 165 | content: ""; 166 | position: absolute; 167 | top: -2.5px; 168 | left: 0; 169 | height: 2.5px; 170 | width: 100%; 171 | background: linear-gradient(to right, #35c3c1, #3ebfac) 172 | } 173 | 174 | .chatapp__form--modal button { 175 | background-color: #3ebfac; 176 | border: 0; 177 | outline: 0; 178 | color: #fff; 179 | padding: 1rem; 180 | } 181 | 182 | .chatapp__form--guest { 183 | position: absolute; 184 | top: 50%; 185 | left: 50%; 186 | transform: translate(-50%, -50%); 187 | background: #fff; 188 | width: 60%; 189 | padding: 1.5rem; 190 | text-align: center; 191 | border-bottom-left-radius: 2px; 192 | border-bottom-right-radius: 2px; 193 | } 194 | 195 | .chatapp__form--guest:before { 196 | content: ""; 197 | position: absolute; 198 | top: -4px; 199 | left: 0; 200 | height: 4px; 201 | width: 100%; 202 | background: linear-gradient(to right, #35c3c1, #3ebfac) 203 | } 204 | 205 | .chatapp__form--guest input { 206 | font-family: FontAwesome, 'Open Sans', sans-serif; 207 | border: none; 208 | outline: none; 209 | height: 20rem; 210 | margin-right: 0; 211 | margin-bottom: 1rem; 212 | height: 3rem; 213 | border-bottom: 1px solid #8f8f8f; 214 | /* padding: 1rem; */ 215 | } 216 | 217 | .chatapp__form--guest form { 218 | display: flex; 219 | flex-direction: column; 220 | max-width: 20rem; 221 | margin: 0 auto; 222 | } 223 | 224 | .chatapp__form--guest button { 225 | background-color: #3ebfac; 226 | border: 0; 227 | outline: 0; 228 | color: #fff; 229 | padding: 1rem; 230 | } 231 | 232 | .alert { 233 | color: #3ebfac; 234 | position: relative; 235 | } 236 | 237 | .alert h3 { 238 | text-align: center; 239 | } 240 | 241 | .chatapp__userpanel--container { 242 | width: calc(33% / 1.1618); 243 | height: 100%; 244 | display: flex; 245 | flex-direction: column; 246 | flex-grow: 1; 247 | min-width: 265px; 248 | } 249 | 250 | .chatapp__userpanel--container button { 251 | border: none; 252 | outline: none; 253 | background-color: inherit; 254 | color: #3ebfac; 255 | } 256 | 257 | .userpanel__channels--container p { 258 | text-align: left; 259 | padding-left: 1.5rem; 260 | margin: 0; 261 | } 262 | 263 | .userpanel__channels--add { 264 | display: flex; 265 | flex-direction: row; 266 | padding-top: 1rem; 267 | justify-content: space-between; 268 | padding-right: 1.5rem; 269 | align-items: center; 270 | padding-bottom: 1rem; 271 | height: 100%; 272 | min-height: 7rem; 273 | border-bottom: 1px solid #f0f0f0; 274 | } 275 | 276 | .userpanel__channels--add .add__btn, 277 | .userpanel__channels--add .close__btn { 278 | font-family: FontAwesome, 'Open Sans', sans-serif; 279 | color: #3ebfac; 280 | } 281 | 282 | .userpanel__channels--add .close__btn { 283 | padding-left: 0.875rem; 284 | } 285 | 286 | .userpanel__channels--add button:hover { 287 | color: #a77fb2; 288 | } 289 | 290 | .channel__add--popup input { 291 | background-color: #f3f3f3; 292 | border: none; 293 | outline: none; 294 | padding: 0.5rem; 295 | border-radius: 12px; 296 | } 297 | 298 | .channel__search { 299 | display: flex; 300 | flex-direction: row; 301 | } 302 | 303 | .channel__search input { 304 | font-family: FontAwesome, 'Open Sans', sans-serif; 305 | } 306 | 307 | .userpanel__channels--list ul { 308 | padding: 0; 309 | margin: 0; 310 | list-style: none; 311 | overflow-y: auto; 312 | max-height: 20rem; 313 | } 314 | 315 | .userpanel__channels--list li { 316 | display: flex; 317 | flex-direction: row; 318 | justify-content: space-between; 319 | padding-bottom: 2.5rem; 320 | align-items: center; 321 | padding-top: 2.5rem; 322 | cursor: pointer; 323 | color: #8f8f8f; 324 | } 325 | 326 | .userpanel__channels--list li:hover { 327 | background-color: #f1f1f1; 328 | color: #000; 329 | } 330 | 331 | .userpanel__channels--list button { 332 | font-family: FontAwesome, 'Open Sans', sans-serif; 333 | padding-right: 1.5rem; 334 | color: #3ebfac; 335 | } 336 | 337 | .userpanel__channels--list button:hover { 338 | color: red; 339 | } 340 | 341 | .chatapp__mainchat--container { 342 | display: flex; 343 | height: 100%; 344 | flex-grow: 1; 345 | } 346 | 347 | .chatapp__chatbox { 348 | background-color: #fff; 349 | width: 100%; 350 | border-left: 1px solid #eee; 351 | height: 100%; 352 | flex-grow: 1; 353 | display: flex; 354 | flex-direction: column; 355 | align-items: stretch; 356 | border-bottom-right-radius: 18px; 357 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 358 | 0 1px 5px 0 rgba(0, 0, 0, .12), 359 | 0 3px 1px -2px rgba(0, 0, 0, .2); 360 | } 361 | 362 | .chatapp__chatbox h3 { 363 | padding-right: 1.5rem; 364 | text-align: right; 365 | margin-top: 0; 366 | margin-bottom: 0; 367 | height: 3.5rem; 368 | line-height: 3.5rem; 369 | border-bottom:2px solid #a77fb2; 370 | } 371 | 372 | .chatapp__chatbox form { 373 | display: flex; 374 | padding: 1.5rem; 375 | height: 100%; 376 | max-height: 5rem; 377 | border-top: 1px solid #f1f1f1; 378 | width: 100%; 379 | } 380 | 381 | .chatapp__chatbox form input { 382 | flex-grow: 1; 383 | border: none; 384 | padding: 1rem; 385 | border: 1px solid #8f8f8f; 386 | outline: none; 387 | border-radius: 40px; 388 | } 389 | 390 | .chatapp__chatbox input:focus { 391 | border: 1px solid #3ebfac; 392 | } 393 | 394 | .chatapp__chatbox--messages { 395 | background-color: #f3f3f3; 396 | overflow-y: scroll; 397 | flex-grow: 1; 398 | } 399 | 400 | .chatapp__chatbox--messages ul { 401 | margin: 0; 402 | padding: 0; 403 | display: flex; 404 | flex-direction: column; 405 | padding-top: 1rem; 406 | padding-bottom: 1rem; 407 | padding-left: 3rem; 408 | padding-right: 3rem; 409 | } 410 | 411 | .chatapp__chatbox--messages li { 412 | display: inline-block; 413 | /* margin: 0 auto; */ 414 | align-self: flex-end; 415 | text-align: right; 416 | } 417 | 418 | .chatapp__chatbox--messages .chat--received { 419 | text-align: left; 420 | align-self: flex-start; 421 | border-left: 0px solid; 422 | } 423 | 424 | .chatapp__chatbox--messages .chat--received .speech--bubble { 425 | border-left: 2.25px solid #a77fb2; 426 | border-right: 0; 427 | } 428 | 429 | .chatapp__chatbox--messages .user--joined { 430 | align-self: center; 431 | padding-bottom: 1.5rem; 432 | } 433 | 434 | .chatapp__chatbox--messages .user--joined .speech--bubble--date, 435 | .chatapp__chatbox--messages .user--joined .speech--bubble--author { 436 | padding: 0; 437 | text-align: center; 438 | width: 100%; 439 | } 440 | 441 | .chatapp__chatbox--messages .user--joined p { 442 | margin: 0; 443 | } 444 | 445 | .speech--bubble { 446 | background-color: #fff; 447 | padding: 0.15rem; 448 | display: inline-block; 449 | min-width: 3rem; 450 | color: #000; 451 | border-right: 2.25px solid #3ebfac; 452 | margin-bottom: 1rem; 453 | } 454 | 455 | .speech--bubble p { 456 | margin: 0; 457 | padding: 0.875rem; 458 | } 459 | 460 | .speech--bubble--author p { 461 | display: inline-block; 462 | /* padding: 0.25rem; */ 463 | font-weight: 300; 464 | text-align: right; 465 | } 466 | 467 | .speech--bubble--date { 468 | padding-left: 1rem; 469 | color: #8f8f8f; 470 | font-weight: 300; 471 | } 472 | 473 | .private__message--container { 474 | position: absolute; 475 | top: 0; 476 | left: 0; 477 | right: 0; 478 | bottom: 0; 479 | background-color: rgba(0,0,0,.9); 480 | } 481 | 482 | .private__message--window button { 483 | font-family: FontAwesome, 'Open Sans', sans-serif; 484 | color: #fff; 485 | background: none; 486 | border: none; 487 | outline: none; 488 | font-size: 2rem; 489 | position: absolute; 490 | right: -3.5rem; 491 | top: -3rem; 492 | } 493 | 494 | .private__message--window { 495 | position: absolute; 496 | height: 85%; 497 | width: 75vw; 498 | /* max-height: 20rem; */ 499 | background-color: #fff; 500 | top: 50%; 501 | left: 50%; 502 | transform: translate(-50%,-50%); 503 | } 504 | 505 | .private__message--window:before { 506 | content: ""; 507 | position: absolute; 508 | top: -4px; 509 | left: 0; 510 | height: 4px; 511 | width: 100%; 512 | background: linear-gradient(to right, #35c3c1, #3ebfac) 513 | } 514 | 515 | #private__message--input { 516 | border-bottom-right-radius: 0; 517 | } 518 | 519 | .active__typing { 520 | padding-left: 3rem; 521 | position: fixed; 522 | left: 0; 523 | bottom: 5rem; 524 | } 525 | 526 | .active__typing p { 527 | color: #8f8f8f; 528 | text-align: left; 529 | padding-bottom: 0.25rem; 530 | } 531 | 532 | #private__chatbox { 533 | position: relative; 534 | } 535 | 536 | @media (max-width: 730px) { 537 | .userpanel__channels--add { 538 | flex-direction: column; 539 | } 540 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | import ChatUIContainer from './components/container/ChatUIContainer'; 4 | import { CookiesProvider } from 'react-cookie'; 5 | 6 | class App extends Component { 7 | constructor() { 8 | super(); 9 | 10 | this.state = { 11 | chatShown: true 12 | } 13 | } 14 | 15 | displayChat = () => { 16 | this.setState(prevState => ({ 17 | chatShown: !prevState.chatShown 18 | })) 19 | } 20 | 21 | render() { 22 | return ( 23 | 24 |
25 |

26 | Real Time Chat App Component 27 |

28 |

Built with React, Socket.io, Express, Node.js, MongoDB

29 | { 30 | (this.state.chatShown) 31 | ? 32 | : null 33 | } 34 |
35 | { 36 | (this.state.chatShown) 37 | ? 38 | : 39 | } 40 |
41 |
42 |
43 | ); 44 | } 45 | } 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/AddChannelBtn.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class AddChannelBtn extends Component { 4 | 5 | state = { showMenu: false }; 6 | 7 | handleClick = () => { 8 | this.setState(prevState => ({ 9 | showMenu: !prevState.showMenu 10 | }) 11 | )} 12 | 13 | closeMenu = () => { 14 | this.setState({ 15 | showMenu: false 16 | }) 17 | } 18 | 19 | render() { 20 | const { handleChange, createChannel } = this.props; 21 | 22 | return ( 23 |
24 | { 25 | (this.state.showMenu) 26 | ?
27 |
28 | 29 |
30 | 31 |
32 | : 33 | } 34 |
35 | ) 36 | } 37 | } -------------------------------------------------------------------------------- /src/components/AddDMBtn.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Alert from './Alert'; 3 | 4 | export default class AddDMBtn extends Component { 5 | 6 | state = { showMenu: false }; 7 | 8 | handleClick = () => { 9 | this.setState(prevState => ({ 10 | showMenu: !prevState.showMenu 11 | }) 12 | )} 13 | 14 | closeMenu = () => { 15 | this.setState({ 16 | showMenu: false 17 | }) 18 | } 19 | 20 | render() { 21 | const { handleChange, startConversation, directMessageErrorLog } = this.props; 22 | 23 | return ( 24 |
25 | { 26 | (this.state.showMenu) 27 | ?
28 | { 29 | (directMessageErrorLog.length) 30 | ? 34 | : null 35 | } 36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 | : 44 | } 45 |
46 | ) 47 | } 48 | } -------------------------------------------------------------------------------- /src/components/Alert.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Alert = ({ header, content }) => { 4 | return( 5 |
6 |

{header}

7 |

{content}

8 |
9 | ) 10 | } 11 | 12 | export default Alert; -------------------------------------------------------------------------------- /src/components/ChatBox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Moment from 'moment'; 3 | import ChatLists from './ChatLists'; 4 | 5 | 6 | export default class ChatBox extends Component { 7 | 8 | scrollDown = () => { 9 | const { chat_container } = this.refs; 10 | chat_container.scrollTop = chat_container.scrollHeight; 11 | } 12 | 13 | componentDidMount() { 14 | this.scrollDown(); 15 | } 16 | 17 | componentDidUpdate(prevProps, prevState) { 18 | this.scrollDown(); 19 | } 20 | 21 | render() { 22 | const { handleSubmit, handleChange, currentChannel, channelConversations, id, getUsersConversations, hasToken, socketConversations, composedMessage, username, guestUsername } = this.props; 23 | 24 | return ( 25 |
26 | { 27 | (id) 28 | ? 33 | : null 34 | } 35 |
36 |

Channel: {currentChannel}

37 |
38 | { 39 | (channelConversations) 40 | ?
    41 | {channelConversations.map((message, index) => { 42 | return ( 43 |
  • 44 |
    45 | { 46 | (username === message.author[0].item.username ) 47 | ?

    You

    48 | :

    {message.author[0].item.username || message.author[0].item.guestName}

    49 | } 50 |

    {Moment(message.createdAt).fromNow()}

    51 |
    52 |
    53 |

    {message.body}

    54 |
    55 |
  • 56 | ) 57 | })} 58 |
59 | :

Nothing has been posted in this channel yet.

60 | } 61 | { 62 | (socketConversations) 63 | ?
    64 | {socketConversations.map((message, index) => { 65 | return ( 66 |
  • 67 | { 68 | (username !== message.userJoined) 69 | ?

    {message.userJoined}

    70 | : null 71 | } 72 |
    73 | { 74 | (username === message.author) 75 | ?

    You

    76 | :

    {message.author}

    77 | } 78 |

    {Moment(message.date).fromNow()}

    79 |
    80 | { 81 | (!message.userJoined) 82 | ?
    83 |

    {message.composedMessage}

    84 |
    85 | : null 86 | } 87 |
  • 88 | 89 | ) 90 | })} 91 |
92 | : null 93 | } 94 |
95 |
96 | 97 |
98 |
99 |
100 | ) 101 | } 102 | } -------------------------------------------------------------------------------- /src/components/ChatLists.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import AddChannelBtn from './AddChannelBtn'; 3 | import AddDMBtn from './AddDMBtn'; 4 | 5 | export default class ChatLists extends Component { 6 | 7 | componentDidMount() { 8 | // Gets most recent conversations 9 | this.props.getUsersConversations(); 10 | } 11 | 12 | render() { 13 | const { usersChannels, handleChange, handleSubmit, createChannel, removeChannel, joinChannel, usersDirectMessages, leaveConversation, choosePrivateMessageRecipient } = this.props; 14 | 15 | return ( 16 |
17 |
18 |
19 |

Channels

20 | 25 |
26 |
27 | { 28 | (usersChannels) 29 | ?
    30 | {usersChannels.map((channel, index) => { 31 | return( 32 |
  • {joinChannel(channel)}} key={`usersChannelId-${index}`}> 33 |

    34 | {channel} 35 |

    36 | { 37 | (channel !== "Public-Main") 38 | ? 39 | : null 40 | } 41 |
  • 42 | ) 43 | })} 44 |
45 | : null 46 | } 47 |
48 |
49 |
50 |
51 |

Private Messages

52 | 55 |
56 |
57 | { 58 | (usersDirectMessages) 59 | ?
    60 | {usersDirectMessages.map((conversation, index) => { 61 | return( 62 |
  • { choosePrivateMessageRecipient(conversation) }} key={`convoId-${index}`}> 63 |

    64 | {conversation.username} 65 |

    66 |
    {e.stopPropagation()}}> 67 | 68 |
    69 |
  • 70 | ) 71 | })} 72 |
73 | :

No Active Conversations

74 | } 75 |
76 |
77 |
78 | ) 79 | } 80 | } -------------------------------------------------------------------------------- /src/components/ChatSelector.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Alert from './Alert'; 3 | 4 | const ChatSelector = (props) => { 5 | const { handleChange, guestLogin, loginError } = props; 6 | return ( 7 |
8 |
9 |

Guest Login

10 |

You can either login, register or enter as a guest

11 |

Guests cannot change channels or private message other users

12 |
13 | 14 | 15 |
16 | { 17 | (loginError.length) 18 | ? 22 | :null 23 | } 24 |
25 |
26 | ) 27 | } 28 | 29 | export default ChatSelector; -------------------------------------------------------------------------------- /src/components/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Alert from './Alert'; 4 | 5 | 6 | 7 | export default class LoginForm extends Component { 8 | constructor(){ 9 | super(); 10 | 11 | this.state = { 12 | username: "", 13 | password: "", 14 | } 15 | } 16 | 17 | handleChange = (event) => { 18 | this.setState({ 19 | [event.target.name]: event.target.value 20 | }) 21 | } 22 | 23 | handleSubmit = (e) => { 24 | const { username, password } = this.state; 25 | e.preventDefault(); 26 | 27 | this.props.userLogin({ username, password }); 28 | } 29 | 30 | 31 | render() { 32 | return ( 33 |
34 |
35 |
36 | 37 | 38 | { 39 | (this.props.loginError.length) 40 | ? 44 | : null 45 | } 46 | 47 | 48 |
49 |
50 | ) 51 | } 52 | } 53 | 54 | LoginForm.propTypes = { 55 | username: PropTypes.string, 56 | password: PropTypes.string, 57 | } -------------------------------------------------------------------------------- /src/components/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Navigation = (props) => { 4 | const { displayForms, id, userLogout, username, guestUsername, closeForm } = props; 5 | 6 | return ( 7 |
8 |
{if(!username){closeForm()}}}> 9 | Live Chat 10 |
11 |
12 | { 13 | (username) 14 | ? {username} 15 | : null 16 | } 17 | { 18 | (guestUsername) 19 | ? Guest-{guestUsername} 20 | : null 21 | } 22 | { 23 | (id) 24 | ? 25 | :
26 | 27 | 28 |
29 | } 30 |
31 |
32 | ) 33 | } 34 | 35 | export default Navigation; -------------------------------------------------------------------------------- /src/components/PrivateMessaging.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Moment from 'moment'; 3 | 4 | export default class PrivateMessaging extends Component{ 5 | constructor() { 6 | super(); 7 | 8 | this.state = { 9 | isTyping: false, 10 | } 11 | } 12 | 13 | // Scrolls down to the bottom of chat log for most recent messages 14 | scrollDown = () => { 15 | const { chat_container } = this.refs; 16 | chat_container.scrollTop = chat_container.scrollHeight; 17 | } 18 | 19 | // Checks if the user is typing, if they are, it sets the state of isTyping to true, 20 | // then it calls the startCheckTyping function 21 | // Tells server sockets, that someone is typing. 22 | sendTyping = () => { 23 | this.lastUpdateTime = Date.now(); 24 | 25 | if (!this.state.isTyping) { 26 | this.setState({ 27 | isTyping: true 28 | }) 29 | this.props.userTyping(true) 30 | this.startCheckTyping(); 31 | } 32 | } 33 | 34 | // Sets up intervals which will set the typing to false, if there is no typing after 300ms, 35 | // it sets state of isTyping to false and calls the stopCheckTyping function 36 | startCheckTyping = () => { 37 | this.typingInterval = setInterval(() => { 38 | if((Date.now() - this.lastUpdateTime) > 300) { 39 | this.setState({ 40 | isTyping: false 41 | }); 42 | this.stopCheckTyping(); 43 | } 44 | }, 300) 45 | } 46 | 47 | // If there are active intervals running, it clears them and sends the socket that no more user is typing 48 | stopCheckTyping = () => { 49 | if (this.typingInterval) { 50 | clearInterval(this.typingInterval) 51 | this.props.userTyping(false); 52 | } 53 | } 54 | 55 | componentDidMount() { 56 | this.scrollDown(); 57 | } 58 | 59 | componentDidUpdate(prevProps, prevState) { 60 | this.scrollDown(); 61 | } 62 | 63 | // If there are active intervals running, we clear them on dismount 64 | componentWillUnmount() { 65 | this.stopCheckTyping(); 66 | } 67 | 68 | render() { 69 | const { handlePrivateInput, handlePrivateSubmit, closePM, currentPrivateRecipient, privateMessageLog, socketPMs, privateMessageInput, showTyping, activeUserTyping, username } = this.props; 70 | 71 | return ( 72 |
{e.stopPropagation()})}> 73 |
74 | 75 |

Conversation with {currentPrivateRecipient.username}

76 |
77 | { 78 | (privateMessageLog.length) 79 | ?
    80 | {privateMessageLog.map((message, index) => { 81 | return ( 82 |
  • 83 |
    84 | { 85 | (username === message.author[0].item.username) 86 | ?

    You

    87 | :

    {message.author[0].item.username}

    88 | } 89 |

    {Moment(message.createdAt).fromNow()}

    90 |
    91 |
    92 |

    {message.body}

    93 |
    94 |
  • 95 | ) 96 | })} 97 |
98 | : null 99 | } 100 | { 101 | (socketPMs.length) 102 | ?
    103 | {socketPMs.map((message, index) => { 104 | return ( 105 |
  • 106 |
    107 | { 108 | (username === message.author[0].item.username) 109 | ?

    You

    110 | :

    {message.author[0].item.username}

    111 | } 112 |

    {Moment(message.createdAt).fromNow()}

    113 |
    114 |
    115 |

    {message.body}

    116 |
    117 |
  • 118 | ) 119 | })} 120 |
121 | : null 122 | } 123 | { 124 | (activeUserTyping !== this.props.username) 125 | ?
126 | { 127 | (showTyping) 128 | ?

129 | {`${activeUserTyping} is typing...`} 130 |

131 | : null 132 | } 133 |
134 | : null 135 | } 136 |
137 |
138 | { e.keyCode !== 13 && this.sendTyping()} } 146 | /> 147 |
148 |
149 |
150 | ) 151 | } 152 | } -------------------------------------------------------------------------------- /src/components/RegisterForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Alert from './Alert'; 4 | 5 | export default class RegisterForm extends Component { 6 | constructor(){ 7 | super(); 8 | 9 | this.state = { 10 | username: "", 11 | password: "", 12 | } 13 | } 14 | 15 | handleChange = (event) => { 16 | this.setState({ 17 | [event.target.name]: event.target.value 18 | }) 19 | } 20 | 21 | handleSubmit = (e) => { 22 | e.preventDefault(); 23 | 24 | const { username, password } = this.state; 25 | 26 | this.props.userRegistration({ username, password }); 27 | } 28 | 29 | render() { 30 | return ( 31 |
32 |
33 |
34 | 35 | 36 | { 37 | (this.props.registrationError.length) 38 | ? 42 | : null 43 | } 44 | 45 | 46 |
47 |
48 | ) 49 | } 50 | } 51 | 52 | RegisterForm.propTypes = { 53 | username: PropTypes.string, 54 | password: PropTypes.string, 55 | } -------------------------------------------------------------------------------- /src/components/container/ChatUIContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import LoginForm from '../LoginForm'; 4 | import RegisterForm from '../RegisterForm'; 5 | import { withCookies } from 'react-cookie' 6 | import axios from 'axios'; 7 | import Navigation from '../Navigation'; 8 | import ChatBox from '../ChatBox'; 9 | import ChatSelector from '../ChatSelector'; 10 | import io from 'socket.io-client'; 11 | import Moment from 'moment'; 12 | import PrivateMessagingContainer from './PrivateMessageContainer'; 13 | 14 | 15 | const API_URL = 'http://localhost:3000/api'; 16 | const SOCKET_URL = "http://localhost:3000"; 17 | const socket = io(SOCKET_URL); 18 | 19 | class ChatUIContainer extends Component { 20 | constructor(){ 21 | super(); 22 | 23 | this.userLogin = this.userLogin.bind(this); 24 | this.guestLogin = this.guestLogin.bind(this); 25 | 26 | this.state = { 27 | username: "", 28 | id: "", 29 | loginError: [], 30 | registrationError: [], 31 | formsShown: false, 32 | formsMethod: "guest", 33 | chatsShown: false, 34 | socket: null, 35 | composedMessage: "", 36 | currentChannel: "Public-Main", 37 | conversations: [], 38 | channelConversations: [], 39 | guestSignup: "", 40 | guestUsername: "", 41 | socketConversations: [], 42 | usersChannels: [], 43 | createInput: "", 44 | startDmInput: "", 45 | usersDirectMessages:[], 46 | directMessageErrorLog: [], 47 | currentPrivateRecipient: {}, 48 | token:"" 49 | } 50 | } 51 | 52 | componentDidMount() { 53 | // Logs user or guest in if they have a token after refreshing or revisiting page. 54 | this.hasToken(); 55 | 56 | // Initialize the socket listeners for events from the backend 57 | this.initSocket(); 58 | 59 | // Get current channels messages 60 | this.getChannelConversations(); 61 | } 62 | 63 | // Sets up socket listeners to listen for when to refresh messages, when a new user has joined, 64 | // or when a user has left the channel 65 | initSocket = () => { 66 | this.setState({ 67 | socket 68 | }) 69 | 70 | socket.on('refresh messages', (data) => { 71 | const newSocketConversations = Array.from(this.state.socketConversations); 72 | 73 | newSocketConversations.push(data) 74 | 75 | this.setState({ 76 | socketConversations: newSocketConversations 77 | }) 78 | }); 79 | 80 | socket.on('user joined', data => { 81 | const userJoined = Array.from(this.state.socketConversations); 82 | 83 | userJoined.push({ 84 | userJoined: data 85 | }) 86 | 87 | this.setState({ 88 | socketConversations: userJoined 89 | }) 90 | }); 91 | 92 | socket.on('user left', data => { 93 | const userJoined = Array.from(this.state.socketConversations); 94 | 95 | userJoined.push({ 96 | userJoined: data 97 | }); 98 | 99 | this.setState({ 100 | socketConversations: userJoined 101 | }); 102 | }); 103 | } 104 | 105 | componentDidUpdate(prevProps, prevState) { 106 | // Tells socket when a user has left channel by comparing against previous and new channel states 107 | if (prevState.currentChannel !== this.state.currentChannel) { 108 | socket.emit('leave channel', prevState.currentChannel, this.setUsername()) 109 | } 110 | } 111 | 112 | // If a token can be found, it will populate back the user's information on browser refresh 113 | hasToken = () => { 114 | const { cookies } = this.props; 115 | const token = cookies.get('token'); 116 | const guestToken = cookies.get('guestToken'); 117 | const tokenUser = cookies.get('user'); 118 | const tokenGuestUser = cookies.get('guestUser'); 119 | const usersChannels = cookies.get('usersChannels'); 120 | const currentChannel = cookies.get('channel'); 121 | 122 | if (token) { 123 | this.setState({ 124 | username: tokenUser.username, 125 | guestUsername: "", 126 | guestSignup: "", 127 | id: tokenUser._id, 128 | token, 129 | usersChannels, 130 | currentChannel: currentChannel || "Public-Main", 131 | formsMethod:"", 132 | formsShown: false 133 | }); 134 | } else if (guestToken) { 135 | this.setState({ 136 | guestUsername: tokenGuestUser, 137 | token: guestToken, 138 | formsMethod: "", 139 | formsShown: false 140 | }); 141 | } 142 | }; 143 | 144 | // Checks username, then return whether it current is a username or guestname 145 | setUsername = () => { 146 | const username = this.state.username; 147 | const guestUsername = this.state.guestUsername; 148 | 149 | if (!username) { 150 | return guestUsername 151 | } else { 152 | return username 153 | } 154 | } 155 | 156 | // Takes a username and password 157 | // POST calls to the backend api with that information 158 | // If the login is successful, cookies are set with the token, user's data, and their channels 159 | async userLogin({ username, password }) { 160 | const { cookies } = this.props; 161 | const currentChannel = this.state.currentChannel; 162 | 163 | try { 164 | const userData = await axios.post(`${API_URL}/auth/login`, { username, password }); 165 | cookies.set('token', userData.data.token, { path: "/", maxAge: 7200 }); 166 | cookies.set('user', userData.data.user, { path: "/", maxAge: 7200 }); 167 | cookies.set('usersChannels', userData.data.user.usersChannels, { path: "/", maxAge: 7200 }); 168 | 169 | this.setState({ 170 | guestUsername:"", 171 | username: userData.data.user.username, 172 | formsShown: false, 173 | token: userData.data.token, 174 | id: userData.data.user._id, 175 | loginError:[], 176 | guestSignup: "", 177 | usersChannels: userData.data.user.usersChannels, 178 | formsMethod:"", 179 | }, () => { 180 | // After the login state is set, tell the backend sockets that a new user has entered 181 | socket.emit('enter channel', currentChannel, this.setUsername()); 182 | }); 183 | } catch(error) { 184 | const errorLog = Array.from(this.state.loginError); 185 | 186 | // Always show most recent errors 187 | errorLog.length = []; 188 | errorLog.push(error); 189 | 190 | this.setState({ 191 | loginError: errorLog 192 | }); 193 | } 194 | } 195 | 196 | // On logout, remove all cookies that were saved and emit to the backend socket listener 197 | // that a user has left the current channel. 198 | // Sets the state back to fresh 199 | userLogout = () => { 200 | const { cookies } = this.props; 201 | const currentChannel = this.state.currentChannel; 202 | cookies.remove('token', { path: '/' }); 203 | cookies.remove('user', { path: '/' }); 204 | cookies.remove('guestToken', { path: "/" }); 205 | cookies.remove('guestUser', { path: "/" }); 206 | cookies.remove('usersChannels', { path: "/" }); 207 | cookies.remove('channel', { path: "/" }); 208 | 209 | socket.emit('leave channel', currentChannel, this.setUsername()) 210 | 211 | this.setState({ 212 | username: "", 213 | id: "", 214 | guestUsername: "", 215 | token: "", 216 | usersChannels: [], 217 | socketConversations: [], 218 | guestSignup: "", 219 | currentChannel: "Public-Main", 220 | formsMethod: "login", 221 | formsShown: true 222 | }); 223 | } 224 | 225 | // Takes a username and password, then makes a POST call to our api which returns a token and that user's info 226 | // Then sets cookies of the given token, user data, and users channels 227 | userRegistration = ({ username, password }) => { 228 | const { cookies } = this.props; 229 | const currentChannel = this.state.currentChannel; 230 | 231 | axios.post(`${API_URL}/auth/register`, { username, password }) 232 | .then(res => { 233 | cookies.set('token', res.data.token, { path: "/", maxAge: 7200 }) 234 | cookies.set('user', res.data.user, { path: "/", maxAge: 7200 }) 235 | cookies.set('usersChannels', res.data.user.usersChannels, { path: "/", maxAge: 7200 }) 236 | 237 | this.setState({ 238 | username: res.data.user.username, 239 | id: res.data.user._id, 240 | registrationError:[], 241 | token:res.data.token, 242 | formsShown: false, 243 | guestUsername:"", 244 | guestSignup: "", 245 | usersChannels: res.data.user.usersChannels, 246 | formsMethod:"" 247 | }, () => { 248 | // Tells the backend sockets that a user has entered a channel 249 | socket.emit('enter channel', currentChannel, this.setUsername()); 250 | }); 251 | }) 252 | .catch(error => { 253 | // Always show most recent errors 254 | const errorLog = Array.from(this.state.registrationError); 255 | 256 | errorLog.length = []; 257 | errorLog.push(error); 258 | 259 | this.setState({ 260 | registrationError: errorLog 261 | }); 262 | }); 263 | } 264 | 265 | // Guest signup, on successful POST call to our api, it returns a token and guest's data 266 | // Saves the token and guestname as cookies 267 | async guestLogin(e) { 268 | e.preventDefault(); 269 | const { cookies } = this.props; 270 | const guestInputName = this.state.guestSignup; 271 | const currentChannel = this.state.currentChannel; 272 | 273 | try { 274 | const guestInfo = await axios.post(`${API_URL}/auth/guest`, { guestInputName }) 275 | 276 | cookies.set('guestToken', guestInfo.data.token, { path: "/", maxAge: 7200 }) 277 | cookies.set('guestUser', guestInfo.data.guestUser.guest.guestName, { path: "/", maxAge: 7200 }) 278 | 279 | this.setState({ 280 | guestUsername: guestInfo.data.guestUser.guest.guestName, 281 | token: guestInfo.data.token, 282 | loginError: [], 283 | guestSignup: "", 284 | formsMethod: "", 285 | formsShown: false, 286 | }, () => { 287 | // Tells backend sockets that a new user has entered the channel 288 | socket.emit('enter channel', currentChannel, this.setUsername()); 289 | }) 290 | } catch(error) { 291 | const guestError = Array.from(this.state.loginError); 292 | 293 | guestError.push(error); 294 | 295 | this.setState({ 296 | loginError: guestError 297 | }) 298 | } 299 | } 300 | 301 | // GET calls to backend API with the given current channel name. 302 | // responds back with all the conversations in that given channel 303 | getChannelConversations = () => { 304 | axios.get(`${API_URL}/chat/channel/${this.state.currentChannel}`) 305 | .then(res => { 306 | const currentChannel = this.state.currentChannel; 307 | 308 | socket.emit('enter channel', currentChannel, this.setUsername()); 309 | 310 | this.setState({ 311 | channelConversations: res.data.channelMessages 312 | }); 313 | }) 314 | .catch(error => { 315 | console.log(error) 316 | }) 317 | } 318 | 319 | // GET call to the backend api with the token given from login in the header. 320 | // this returns a list of all the user's active conversations 321 | getUsersConversations = () => { 322 | axios.get(`${API_URL}/chat`, { 323 | headers: { Authorization: this.state.token } 324 | }) 325 | .then(res => { 326 | const updatedUsersDirectMessages = res.data.conversationsWith; 327 | 328 | this.setState({ 329 | usersDirectMessages: updatedUsersDirectMessages || [] 330 | }); 331 | }) 332 | .catch(err => { 333 | console.log(err) 334 | }); 335 | } 336 | 337 | // Takes a message and recipient, then makes a POST call with the message in the body of the call 338 | // as well as the token given on login in the header. 339 | // On successful post call, the message is saved to mongodb 340 | // This then emits to the socket listeners on the server that a message was sent, 341 | // which returns a refresh message message for us to get updated messages from the mongodb. 342 | sendMessage = (composedMessage, recipient) => { 343 | const socket = this.state.socket; 344 | const currentChannel = this.state.currentChannel; 345 | 346 | axios.post(`${API_URL}/chat/postchannel/${this.state.currentChannel}`, { composedMessage }, { 347 | headers: { Authorization: this.state.token } 348 | }) 349 | .then(res => { 350 | const socketMsg = { 351 | composedMessage, 352 | channel: currentChannel, 353 | author: this.state.guestUsername || this.state.username, 354 | date: Moment().format() 355 | } 356 | socket.emit('new message', socketMsg) 357 | 358 | this.setState({ 359 | composedMessage: "" 360 | }) 361 | }) 362 | .catch(err => { 363 | console.log(err) 364 | }) 365 | 366 | } 367 | 368 | handleChange = (event) => { 369 | this.setState({ 370 | [event.target.name]: event.target.value 371 | }); 372 | } 373 | 374 | handleSubmit = (e) => { 375 | e.preventDefault(); 376 | 377 | this.sendMessage(this.state.composedMessage); 378 | } 379 | 380 | // Takes a channel name and then makes a POST call to the backend API, 381 | // requires a token for authorization to create a channel. 382 | // On success, the new array is pushed into the user's current user channel, 383 | // and saves the new channel list in cookie and refreshes channel conversations. 384 | createChannel = (e) => { 385 | const { cookies } = this.props; 386 | const createInput = this.state.createInput; 387 | e.preventDefault(); 388 | 389 | axios.post(`${API_URL}/user/addchannel`, { createInput }, { 390 | headers: { Authorization: this.state.token } 391 | }) 392 | .then(res => { 393 | const updatedUsersChannels = Array.from(this.state.usersChannels); 394 | 395 | updatedUsersChannels.push(this.state.createInput); 396 | 397 | cookies.set('usersChannels', updatedUsersChannels, { path: "/", maxAge: 7200 }); 398 | 399 | this.setState({ 400 | socketConversations:[], 401 | currentChannel: createInput, 402 | usersChannels: updatedUsersChannels 403 | }, () => {this.getChannelConversations()}) 404 | }) 405 | .catch(err => { 406 | console.log(err) 407 | }) 408 | } 409 | 410 | // Takes a channel name parameter, then a POST call with authorization token to backend API, 411 | // On success, cookies are set of the updated user channels 412 | removeChannel = (channel) => { 413 | const { cookies } = this.props; 414 | 415 | axios.post(`${API_URL}/user/removechannel`, { channel }, { 416 | headers: { Authorization: this.state.token } 417 | }) 418 | .then(res => { 419 | const updatedChannels = res.data.updatedChannels; 420 | 421 | cookies.set('usersChannels', updatedChannels, { path: "/", maxAge: 7200 }); 422 | 423 | this.joinChannel("Public-Main"); 424 | this.setState({ 425 | socketConversations: [], 426 | usersChannels: updatedChannels 427 | }) 428 | }) 429 | .catch(err => { 430 | console.log(err) 431 | }) 432 | } 433 | 434 | // Takes a channel name parameter, saves it as a cookie, then sets the state of current channel, 435 | // to that channel paramter. 436 | joinChannel = (channel) => { 437 | const { cookies } = this.props; 438 | 439 | cookies.set('channel', channel, { path: "/", maxAge: 7200 }); 440 | 441 | this.setState({ 442 | socketConversations: [], 443 | currentChannel: channel 444 | }, () => {this.getChannelConversations()}) 445 | } 446 | 447 | // Takes the input and checks against user's conversation to see if their are duplicates, 448 | // On success, a POST call is made with the message 449 | startConversation = (e) => { 450 | const startDmInput = this.state.startDmInput; 451 | const usersDirectMessages = this.state.usersDirectMessages; 452 | e.preventDefault(); 453 | 454 | const checkForCurrentConvos = usersDirectMessages.filter(directMessage => { 455 | return directMessage.username === startDmInput 456 | }) 457 | 458 | // Checks if already in current conversation with that person 459 | if (!checkForCurrentConvos.length || !usersDirectMessages.length) { 460 | axios.post(`${API_URL}/chat/new`, { startDmInput }, { 461 | headers: { Authorization: this.state.token } 462 | }) 463 | .then(res => { 464 | const newUsersDirectMessages = Array.from(this.state.usersDirectMessages) 465 | 466 | newUsersDirectMessages.push({ 467 | username: res.data.recipient, 468 | _id: res.data.recipientId, 469 | }) 470 | 471 | this.setState({ 472 | usersDirectMessages: newUsersDirectMessages, 473 | directMessageErrorLog: [] 474 | }) 475 | }) 476 | .catch(err => { 477 | const updatedErrorLog = Array.from(this.state.directMessageErrorLog); 478 | 479 | updatedErrorLog.push(err); 480 | 481 | this.setState({ 482 | directMessageErrorLog: updatedErrorLog 483 | }) 484 | 485 | }) 486 | } else { 487 | const updatedErrorLog = Array.from(this.state.directMessageErrorLog); 488 | 489 | updatedErrorLog.push({ 490 | //Had to emulate response from backend for error the alert component 491 | response:{ 492 | data: { 493 | error: 'Already in conversation with that person.' 494 | } 495 | } 496 | }); 497 | 498 | this.setState({ 499 | directMessageErrorLog: updatedErrorLog 500 | }) 501 | } 502 | } 503 | 504 | // Takes a conversation id and user parameter 505 | // POST calls with the conversation id to the backend 506 | // On success, it removes that conversation from the users data 507 | // then alter the current conversations state to reflect the new change, so we dont need to refresh. 508 | leaveConversation = (conversationId, user) => { 509 | axios.post(`${API_URL}/chat/leave`, {conversationId}, { 510 | headers: { Authorization: this.state.token } 511 | }) 512 | .then(res => { 513 | const directMessages = Array.from(this.state.usersDirectMessages); 514 | 515 | const newDirectMessages = directMessages.filter((directMessages) => { 516 | return directMessages.username !== user 517 | }) 518 | 519 | this.setState({ 520 | usersDirectMessages: newDirectMessages 521 | }) 522 | }) 523 | .catch(err => { 524 | console.log(err) 525 | }) 526 | } 527 | 528 | choosePrivateMessageRecipient = (recipient) => { 529 | this.setState({ 530 | currentPrivateRecipient: recipient 531 | }) 532 | } 533 | 534 | // Depending on the parameter, different pages are shown 535 | // The Login, Register or the Guest sign up page. 536 | displayForms = (method) => { 537 | if (method === "login") { 538 | this.setState({ 539 | loginError: [], 540 | formsMethod: "login", 541 | formsShown: true, 542 | guestUsername: "" 543 | }); 544 | } 545 | 546 | if (method === "register") { 547 | this.setState({ 548 | formsMethod: "register", 549 | formsShown: true, 550 | guestUsername: "" 551 | }); 552 | } 553 | 554 | if (method === "close") { 555 | this.setState({ 556 | formsMethod: "", 557 | formsShown: false 558 | }); 559 | } 560 | } 561 | 562 | closeForm = () => { 563 | this.setState({ 564 | formsMethod: "guest", 565 | formsShown: false 566 | }); 567 | } 568 | 569 | closePM = (e) => { 570 | e.stopPropagation(); 571 | this.setState({ 572 | currentPrivateRecipient: {} 573 | }) 574 | } 575 | 576 | // When the component unmounts, we detach all the listeners and give the server sockets a leave channel signal 577 | componentWillUnmount() { 578 | const currentChannel = this.state.currentChannel; 579 | 580 | socket.emit('leave channel', currentChannel, this.setUsername()); 581 | socket.off('refresh messages'); 582 | socket.off('user joined'); 583 | socket.off('user left'); 584 | } 585 | 586 | render() { 587 | return ( 588 |
589 | 595 | { 596 | (this.state.formsMethod === "login" && this.state.formsShown) 597 | ? 602 | : null 603 | } 604 | { 605 | (this.state.formsMethod === "register" && this.state.formsShown) 606 | ? 611 | : null 612 | } 613 | { 614 | (this.state.formsMethod === "guest") 615 | ? 620 | : null 621 | } 622 | { 623 | (this.state.id || this.state.guestUsername) 624 | ? 637 | : null 638 | } 639 | { 640 | (Object.getOwnPropertyNames(this.state.currentPrivateRecipient).length !== 0) 641 | ? 648 | 649 | : null 650 | } 651 |
652 | ) 653 | } 654 | } 655 | 656 | ChatUIContainer.propTypes = { 657 | username: PropTypes.string, 658 | id: PropTypes.string, 659 | loginError: PropTypes.array, 660 | registrationError: PropTypes.array, 661 | formsShown: PropTypes.bool, 662 | formsMethod: PropTypes.string, 663 | chatsShown: PropTypes.bool, 664 | composedMessage: PropTypes.string, 665 | currentChannel: PropTypes.string, 666 | conversations: PropTypes.array, 667 | channelConversations: PropTypes.array, 668 | guestSignup: PropTypes.string, 669 | guestUsername: PropTypes.string, 670 | socketConversations: PropTypes.array, 671 | usersChannels: PropTypes.array, 672 | createInput: PropTypes.string, 673 | startDmInput: PropTypes.string, 674 | usersDirectMessages:PropTypes.array, 675 | directMessageErrorLog: PropTypes.array, 676 | currentPrivateRecipient: PropTypes.object, 677 | token:PropTypes.string 678 | } 679 | 680 | export default withCookies(ChatUIContainer); -------------------------------------------------------------------------------- /src/components/container/PrivateMessageContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types' 3 | import PrivateMessaging from '../PrivateMessaging'; 4 | import axios from 'axios'; 5 | import io from 'socket.io-client'; 6 | 7 | const SOCKET_URL = "http://localhost:3000"; 8 | const socket = io(SOCKET_URL); 9 | const API_URL = 'http://localhost:3000/api'; 10 | 11 | export default class PrivateMessagingContainer extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | privateMessageInput: "", 17 | privateMessageLog: [], 18 | conversationId: "", 19 | socketPMs: [], 20 | currentPrivateRecipient: this.props.currentPrivateRecipient, 21 | showTyping: false, 22 | activeUserTyping: "" 23 | } 24 | } 25 | 26 | handlePrivateInput = (event) => { 27 | this.setState({ 28 | [event.target.name]: event.target.value 29 | }); 30 | } 31 | 32 | handlePrivateSubmit = (e) => { 33 | e.preventDefault(); 34 | 35 | this.sendPrivateMessage(); 36 | } 37 | 38 | // Makes a POST call with a private message and a recipient id to save a message. 39 | // On success, it emits to the server sockets, a socketMsg object containing what is sent to mongodb, so it can 40 | // send that information to the recipient if they are connected. 41 | sendPrivateMessage = () => { 42 | const privateMessageInput = this.state.privateMessageInput; 43 | const recipientId = this.props.currentPrivateRecipient._id; 44 | 45 | axios.post(`${API_URL}/chat/reply`, { privateMessageInput, recipientId }, { 46 | headers: { Authorization: this.props.token } 47 | }) 48 | .then(res => { 49 | const socketMsg = { 50 | body: privateMessageInput, 51 | conversationId: this.state.conversationId, 52 | author:[{ 53 | item:{ 54 | username: this.props.username 55 | } 56 | }] 57 | } 58 | socket.emit('new privateMessage', socketMsg); 59 | 60 | this.setState({ 61 | privateMessageInput: "" 62 | }) 63 | }) 64 | .catch(err => { 65 | console.log(err); 66 | }) 67 | } 68 | 69 | // Takes the current private recipient and makes a POST call taking the recipient id. 70 | // On success, it joins the user into a socket room with the conversation id returned as the room name. 71 | // Takes the response from the backend and sets the state with the conversation logs. 72 | getPrivateMessages = () => { 73 | const currentPrivateRecipient = this.props.currentPrivateRecipient; 74 | 75 | axios.get(`${API_URL}/chat/privatemessages/${currentPrivateRecipient._id}`, { 76 | headers: { Authorization: this.props.token } 77 | }) 78 | .then(res => { 79 | socket.emit('enter privateMessage', res.data.conversationId) 80 | this.setState({ 81 | privateMessageLog: res.data.conversation || [], 82 | conversationId: res.data.conversationId 83 | }) 84 | }) 85 | .catch(err => { 86 | console.log(err) 87 | }) 88 | } 89 | 90 | // Tells the socket when a user is current typing. 91 | // Sends the conversation id and username to display who is typing. 92 | userTyping = (isTyping) => { 93 | const conversationId = this.state.conversationId; 94 | const username = this.props.username; 95 | const data = { 96 | isTyping, 97 | conversationId, 98 | username 99 | } 100 | socket.emit('user typing', data) 101 | } 102 | 103 | // On different recipients, it will get new private messages. 104 | componentWillReceiveProps(nextProps) { 105 | this.setState({ 106 | currentPrivateRecipient: nextProps.currentPrivateRecipient 107 | }, () => { 108 | this.getPrivateMessages() 109 | }) 110 | } 111 | 112 | // On mount, it gets private messages, and adds socket listeners for new private messages. 113 | componentDidMount() { 114 | this.getPrivateMessages(); 115 | 116 | socket.on('refresh privateMessages', (data) => { 117 | const updatedSocketPMs = Array.from(this.state.socketPMs); 118 | 119 | updatedSocketPMs.push(data); 120 | 121 | this.setState({ 122 | socketPMs: updatedSocketPMs 123 | }) 124 | }); 125 | 126 | socket.on('typing', (data) => { 127 | 128 | this.setState({ 129 | showTyping: data.isTyping, 130 | activeUserTyping: data.username 131 | }); 132 | }) 133 | 134 | } 135 | 136 | // Removes socket listeners and tells sever sockets the user has left that private room. 137 | componentWillUnmount() { 138 | socket.emit('leave privateMessage', this.state.conversationId); 139 | socket.off('refresh privateMessages'); 140 | socket.off('typing'); 141 | } 142 | 143 | render() { 144 | const { closePM } = this.props; 145 | return ( 146 |
{closePM(e)}}> 147 | 154 |
155 | ) 156 | } 157 | } 158 | 159 | PrivateMessagingContainer.propTypes = { 160 | privateMessageInput: PropTypes.string, 161 | privateMessageLog: PropTypes.array, 162 | conversationId: PropTypes.string, 163 | socketPMs: PropTypes.array, 164 | currentPrivateRecipient: PropTypes.object, 165 | showTyping: PropTypes.bool, 166 | activeUserTyping: PropTypes.string 167 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | --------------------------------------------------------------------------------