├── .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 | 
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 |
15 | You need to enable JavaScript to run this app.
16 |
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 | ? Leave the Chat!
38 | : Join the Chat!
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 |
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 |
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 |
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 |
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 |
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 | ? {removeChannel(channel)}}>
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 | {leaveConversation(conversation._id, conversation.username)}}>
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 |
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 |
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 | ?
Logout
25 | :
26 | {displayForms("login")}}>Login
27 | {displayForms("register")}}>Sign Up
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 |
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 |
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 |
--------------------------------------------------------------------------------