├── .gitignore
├── README.md
├── app
├── .gitignore
├── Dockerfile
├── README.md
├── docker-compose.yml
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── components
│ │ ├── app.js
│ │ ├── messenger.js
│ │ ├── search-user.js
│ │ ├── user-bar.js
│ │ ├── user-form.js
│ │ └── user-menu.js
│ ├── config.js
│ ├── css
│ │ ├── .sass-cache
│ │ │ └── c6c61f1d3471adfa6b4f36ea934cb8d28f43b0b7
│ │ │ │ ├── _font.scssc
│ │ │ │ ├── _variable.scssc
│ │ │ │ └── app.scssc
│ │ ├── _font.scss
│ │ ├── _variable.scss
│ │ ├── app.css
│ │ ├── app.css.map
│ │ ├── app.scss
│ │ └── fonts
│ │ │ ├── chatapp.eot
│ │ │ ├── chatapp.svg
│ │ │ ├── chatapp.ttf
│ │ │ └── chatapp.woff
│ ├── helpers
│ │ ├── index.js
│ │ └── objectid.js
│ ├── images
│ │ └── avatar.png
│ ├── index.js
│ ├── realtime.js
│ ├── registerServiceWorker.js
│ ├── service.js
│ └── store.js
└── yarn.lock
├── deployment-to-digitalocean-hosting.md
└── server
├── Dockerfile
├── docker-compose.yml
├── package.json
└── src
├── app-router.js
├── database.js
├── helper.js
├── index.js
├── models
├── channel.js
├── connection.js
├── index.js
├── message.js
├── token.js
└── user.js
└── www
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 | .env.local
4 | .env.development.local
5 | .env.test.local
6 | .env.production.local
7 |
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 |
12 | server/node_modules
13 | app/node_modules
14 | server/dist
15 | server/package-lock.json
16 |
17 | app/node_modules
18 | app/package-lock.json
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nodejs-reactjs-chatapp
2 |
3 | Create messenger chat application use Nodejs Expressjs, Reactjs.
4 |
5 | ## Screenshot:
6 |
7 |
8 |
9 | ## Server
10 |
11 | ```
12 | cd server
13 | ```
14 | ```
15 | npm install
16 | ```
17 |
18 | ```
19 | npm run dev
20 | ```
21 | ### Reactjs App development
22 |
23 | ```
24 | cd app
25 | ```
26 |
27 | ```
28 | npm start
29 | ```
30 |
31 | ### Reactjs App development using docker-compose
32 |
33 | The docker-compose files are located in the two different application folders app and server. To run all the functions using docker run the follow commands:
34 | ```
35 | cd server
36 | ```
37 | ```
38 | docker-compose up
39 | ```
40 | At this moment the server application side will be running.
41 |
42 | Now it's time to run application front end. Open a new terminal (window or tab) and in the project folder use the following commands:
43 | ```
44 | cd app
45 | ```
46 | ```
47 | docker-compose up
48 | ```
49 |
50 | Attention: Deppending on the way you have installed the docker in your compile you may use **sudo** command to run docker, for example:
51 | ```
52 | sudo docker-compose up
53 | ```
54 |
55 | For more docker informations and how to install access https://www.docker.com/ .
56 |
57 | ## Tutorials
58 | * Checkout the video toturials list: https://www.youtube.com/playlist?list=PLFaW_8zE4amPaLyz5AyVT8B_wfOYwd8x8
59 | * My Facebook: https://www.facebook.com/TabvnGroup/
60 | * Youtube Chanel: https://youtube.com/tabvn
61 |
62 |
63 | ## Deploy Node.js React.js to DigitalOcean.com Ubuntu 16.04 Cloud VPS
64 |
65 | * Document
66 | * Video: https://www.youtube.com/watch?v=wJsH45eWNBo
67 |
68 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/app/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:11.12.0
2 |
3 | # Install a bunch of node modules that are commonly used.
4 | #ADD package.json /usr/app/
5 | ADD . /usr/app/
6 |
7 | EXPOSE 80
8 | ENV BIND_HOST=0.0.0.0
9 | CMD ["npm", "start"]
10 | WORKDIR /usr/app
11 |
12 | RUN npm install
13 |
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | ## Start app
2 |
3 | ```
4 | npm install
5 | ```
6 |
7 | ```
8 | npm start
9 | ```
--------------------------------------------------------------------------------
/app/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | app:
4 | build: .
5 | ports:
6 | - "3000:3000"
7 | command: npm start
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.17.1",
7 | "classnames": "^2.2.5",
8 | "immutable": "^3.8.2",
9 | "lodash": "^4.17.4",
10 | "moment": "^2.19.2",
11 | "react": "^16.1.1",
12 | "react-dom": "^16.1.1",
13 | "react-scripts": "1.0.17"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test --env=jsdom",
19 | "eject": "react-scripts eject"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tabvn/nodejs-reactjs-chatapp/4ff49c67588594195f09ae83b9b03e837813e0d4/app/public/favicon.ico
--------------------------------------------------------------------------------
/app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/components/app.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import Store from '../store'
3 | import Messenger from './messenger'
4 |
5 | export default class App extends Component{
6 |
7 | constructor(props){
8 | super(props);
9 |
10 | this.state = {
11 |
12 | store: new Store(this),
13 | }
14 | }
15 |
16 | render(){
17 |
18 | const {store} = this.state;
19 | return
20 |
21 |
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/components/messenger.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import classNames from 'classnames'
3 | import {OrderedMap} from 'immutable'
4 | import _ from 'lodash'
5 | import {ObjectID} from '../helpers/objectid'
6 | import SearchUser from './search-user'
7 | import moment from 'moment'
8 | import UserBar from './user-bar'
9 |
10 |
11 | export default class Messenger extends Component {
12 |
13 | constructor(props) {
14 |
15 | super(props);
16 |
17 | this.state = {
18 | height: window.innerHeight,
19 | newMessage: 'Hello there...',
20 | searchUser: "",
21 | showSearchUser: false,
22 | }
23 |
24 | this._onResize = this._onResize.bind(this);
25 | this.handleSend = this.handleSend.bind(this)
26 | this.renderMessage = this.renderMessage.bind(this);
27 | this.scrollMessagesToBottom = this.scrollMessagesToBottom.bind(this)
28 | this._onCreateChannel = this._onCreateChannel.bind(this);
29 | this.renderChannelTitle = this.renderChannelTitle.bind(this)
30 | this.renderChannelAvatars = this.renderChannelAvatars.bind(this);
31 | }
32 |
33 | renderChannelAvatars(channel){
34 | const {store} = this.props;
35 |
36 | const members = store.getMembersFromChannel(channel);
37 |
38 | const maxDisplay = 4;
39 | const total = members.size > maxDisplay ? maxDisplay : members.size;
40 |
41 | const avatars = members.map((user, index) => {
42 |
43 |
44 |
45 | return index < maxDisplay ?
: null
46 |
47 | });
48 |
49 |
50 | return {avatars}
51 | }
52 | renderChannelTitle(channel = null) {
53 |
54 | if (!channel) {
55 | return null;
56 | }
57 | const {store} = this.props;
58 |
59 | const members = store.getMembersFromChannel(channel);
60 |
61 |
62 | const names = [];
63 |
64 | members.forEach((user) => {
65 |
66 | const name = _.get(user, 'name');
67 | names.push(name);
68 | })
69 |
70 | let title = _.join(names, ',');
71 |
72 | if (!title && _.get(channel, 'isNew')) {
73 | title = 'New message';
74 | }
75 |
76 | return {title}
77 | }
78 |
79 | _onCreateChannel() {
80 |
81 | const {store} = this.props;
82 |
83 | const currentUser = store.getCurrentUser();
84 | const currentUserId = _.get(currentUser, '_id');
85 |
86 | const channelId = new ObjectID().toString();
87 | const channel = {
88 | _id: channelId,
89 | title: '',
90 | lastMessage: "",
91 | members: new OrderedMap(),
92 | messages: new OrderedMap(),
93 | isNew: true,
94 | userId: currentUserId,
95 | created: new Date(),
96 | };
97 |
98 | channel.members = channel.members.set(currentUserId, true);
99 |
100 |
101 | store.onCreateNewChannel(channel);
102 |
103 |
104 | }
105 |
106 | scrollMessagesToBottom() {
107 |
108 | if (this.messagesRef) {
109 |
110 | this.messagesRef.scrollTop = this.messagesRef.scrollHeight;
111 | }
112 | }
113 |
114 | renderMessage(message) {
115 |
116 | const text = _.get(message, 'body', '');
117 |
118 | const html = _.split(text, '\n').map((m, key) => {
119 |
120 | return
121 | })
122 |
123 |
124 | return html;
125 | }
126 |
127 | handleSend() {
128 |
129 | const {newMessage} = this.state;
130 | const {store} = this.props;
131 |
132 |
133 | // create new message
134 |
135 | if (_.trim(newMessage).length) {
136 |
137 | const messageId = new ObjectID().toString();
138 | const channel = store.getActiveChannel();
139 | const channelId = _.get(channel, '_id', null);
140 | const currentUser = store.getCurrentUser();
141 |
142 | const message = {
143 | _id: messageId,
144 | channelId: channelId,
145 | body: newMessage,
146 | userId: _.get(currentUser, '_id'),
147 | me: true,
148 |
149 | };
150 |
151 |
152 | store.addMessage(messageId, message);
153 |
154 | this.setState({
155 | newMessage: '',
156 | })
157 | }
158 |
159 |
160 | }
161 |
162 | _onResize() {
163 |
164 | this.setState({
165 | height: window.innerHeight
166 | });
167 | }
168 |
169 | componentDidUpdate() {
170 |
171 |
172 | this.scrollMessagesToBottom();
173 | }
174 |
175 | componentDidMount() {
176 |
177 |
178 | window.addEventListener('resize', this._onResize);
179 |
180 |
181 | }
182 |
183 |
184 | componentWillUnmount() {
185 |
186 | window.removeEventListener('resize', this._onResize)
187 |
188 | }
189 |
190 | render() {
191 |
192 | const {store} = this.props;
193 |
194 | const {height} = this.state;
195 |
196 | const style = {
197 | height: height,
198 | };
199 |
200 |
201 | const activeChannel = store.getActiveChannel();
202 | const messages = store.getMessagesFromChannel(activeChannel); //store.getMessages();
203 | const channels = store.getChannels();
204 | const members = store.getMembersFromChannel(activeChannel);
205 |
206 |
207 | return (
208 |
209 |
210 |
211 |
212 |
214 |
Messenger
215 |
216 |
217 |
218 | {_.get(activeChannel, 'isNew') ?
219 |
220 | {
221 | members.map((user, key) => {
222 |
223 | return {
224 |
225 | store.removeMemberFromChannel(activeChannel, user);
226 |
227 | }} key={key}>{_.get(user, 'name')}
228 | })
229 | }
230 | {
231 |
232 | const searchUserText = _.get(event, 'target.value');
233 |
234 | //console.log("searching for user with name: ", searchUserText)
235 |
236 | this.setState({
237 | searchUser: searchUserText,
238 | showSearchUser: true,
239 | }, () => {
240 |
241 |
242 | store.startSearchUsers(searchUserText);
243 | });
244 |
245 |
246 | }} type="text" value={this.state.searchUser}/>
247 |
248 | {this.state.showSearchUser ? {
250 |
251 | this.setState({
252 | showSearchUser: false,
253 | searchUser: '',
254 |
255 | }, () => {
256 |
257 |
258 | const userId = _.get(user, '_id');
259 | const channelId = _.get(activeChannel, '_id');
260 |
261 | store.addUserToChannel(channelId, userId);
262 |
263 | });
264 |
265 |
266 | }}
267 | store={store}/> : null}
268 |
269 |
: this.renderChannelTitle(activeChannel)}
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 | {channels.map((channel, key) => {
285 |
286 | return (
287 |
{
288 |
289 | store.setActiveChannelId(channel._id);
290 |
291 | }} key={channel._id}
292 | className={classNames('chanel', {'notify': _.get(channel, 'notify') === true},{'active': _.get(activeChannel, '_id') === _.get(channel, '_id', null)})}>
293 |
294 | {this.renderChannelAvatars(channel)}
295 |
296 |
297 | {this.renderChannelTitle(channel)}
298 |
{channel.lastMessage}
299 |
300 |
301 |
302 | )
303 |
304 | })}
305 |
306 |
307 |
308 |
309 |
310 |
this.messagesRef = ref} className="messages">
311 |
312 | {messages.map((message, index) => {
313 |
314 | const user = _.get(message, 'user');
315 |
316 |
317 | return (
318 |
319 |
320 |

321 |
322 |
323 |
{message.me ? 'You ' : _.get(message, 'user.name')} says:
325 |
326 |
327 | {this.renderMessage(message)}
328 |
329 |
330 |
331 | )
332 |
333 |
334 | })}
335 |
336 |
337 |
338 |
339 | {activeChannel && members.size > 0 ?
340 |
341 |
342 |
357 |
358 |
359 |
360 |
: null}
361 |
362 |
363 |
364 |
365 |
366 | {members.size > 0 ?
Members
367 |
368 |
369 | {members.map((member, key) => {
370 |
371 |
372 | const isOnline = _.get(member, 'online', false);
373 |
374 | return (
375 |
376 |
377 |

378 |
379 |
380 |
381 |
{member.name} - {isOnline ? 'Online': 'Offline'}
382 |
Joined: {moment(member.created).fromNow()}
383 |
384 |
385 |
386 | )
387 |
388 | })}
389 |
390 |
391 |
: null}
392 |
393 |
394 |
395 |
396 |
397 | )
398 | }
399 | }
--------------------------------------------------------------------------------
/app/src/components/search-user.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import _ from 'lodash'
3 |
4 | export default class SearchUser extends Component{
5 |
6 |
7 | constructor(props){
8 | super(props);
9 |
10 |
11 | this.handleOnClick = this.handleOnClick.bind(this);
12 |
13 |
14 | }
15 |
16 |
17 | handleOnClick(user){
18 |
19 |
20 | if(this.props.onSelect){
21 | this.props.onSelect(user);
22 | }
23 | }
24 | render(){
25 |
26 | const {store} = this.props;
27 |
28 |
29 | const users = store.getSearchUsers();
30 |
31 |
32 |
33 |
34 | return
35 |
36 |
37 |
38 | {users.map((user, index) => {
39 |
40 | return (
this.handleOnClick(user)} key={index} className="user">
41 |

42 |
{_.get(user, 'name')}
43 |
)
44 |
45 | })}
46 |
47 |
48 |
49 |
50 |
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/components/user-bar.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import _ from 'lodash'
3 | import avatar from '../images/avatar.png'
4 | import UserForm from './user-form'
5 | import UserMenu from './user-menu'
6 |
7 |
8 | export default class UserBar extends Component {
9 |
10 | constructor(props) {
11 | super(props);
12 |
13 | this.state = {
14 | showUserForm: false,
15 | showUserMenu: false,
16 | }
17 |
18 |
19 | }
20 |
21 | render() {
22 |
23 | const {store} = this.props;
24 |
25 | const me = store.getCurrentUser();
26 | const profilePicture = _.get(me, 'avatar');
27 | const isConnected = store.isConnected();
28 |
29 | return (
30 |
31 | {me && !isConnected ?
Reconnecting...
: null}
32 | {!me ?
: null}
39 |
{_.get(me, 'name')}
40 |
{
41 |
42 | this.setState({
43 | showUserMenu: true,
44 | })
45 |
46 | }}>

47 |
48 | {!me && this.state.showUserForm ?
{
49 |
50 |
51 | this.setState({
52 | showUserForm: false,
53 | })
54 |
55 | }} store={store}/> : null}
56 |
57 |
58 | {this.state.showUserMenu ? {
61 |
62 | this.setState({
63 | showUserMenu: false,
64 | })
65 | }}
66 |
67 | /> : null}
68 |
69 |
70 | );
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/components/user-form.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import _ from 'lodash'
3 | import classNames from 'classnames'
4 |
5 |
6 | export default class UserForm extends Component {
7 |
8 | constructor(props) {
9 |
10 | super(props);
11 |
12 |
13 | this.state = {
14 | message: null,
15 | isLogin: true,
16 | user: {
17 | name: '',
18 | email: '',
19 | password: ''
20 | }
21 | }
22 |
23 | this.onSubmit = this.onSubmit.bind(this);
24 | this.onTextFieldChange = this.onTextFieldChange.bind(this)
25 |
26 | this.onClickOutside = this.onClickOutside.bind(this);
27 | }
28 |
29 | onClickOutside(event) {
30 |
31 | if (this.ref && !this.ref.contains(event.target)) {
32 |
33 |
34 | if (this.props.onClose) {
35 | this.props.onClose();
36 | }
37 |
38 | }
39 | }
40 |
41 | componentDidMount() {
42 |
43 | window.addEventListener('mousedown', this.onClickOutside);
44 |
45 | }
46 |
47 | componentWillUnmount() {
48 |
49 | window.removeEventListener('mousedown', this.onClickOutside);
50 |
51 | }
52 |
53 | onSubmit(event) {
54 | const {user, isLogin} = this.state;
55 | const {store} = this.props;
56 |
57 | event.preventDefault();
58 |
59 | this.setState({
60 | message: null,
61 | }, () => {
62 |
63 |
64 | if(isLogin){
65 | store.login(user.email, user.password).then((user) => {
66 |
67 |
68 | if (this.props.onClose) {
69 | this.props.onClose();
70 | }
71 |
72 |
73 | }).catch((err) => {
74 |
75 | console.log("err", err);
76 |
77 | this.setState({
78 | message: {
79 | body: err,
80 | type: 'error',
81 | }
82 | })
83 | });
84 | }else{
85 |
86 | store.register(user).then((_)=> {
87 |
88 | this.setState({
89 | message: {
90 | body: 'User created.',
91 | type: 'success'
92 | }
93 | }, () => {
94 |
95 | // now login this user
96 |
97 | store.login(user.email, user.password).then(() => {
98 |
99 | if (this.props.onClose) {
100 | this.props.onClose();
101 | }
102 | })
103 | })
104 | })
105 | }
106 |
107 |
108 | })
109 |
110 |
111 | }
112 |
113 | onTextFieldChange(event) {
114 |
115 | let {user} = this.state;
116 |
117 |
118 | const field = event.target.name;
119 |
120 | user[field] = event.target.value;
121 |
122 | this.setState({
123 | user: user
124 | });
125 |
126 |
127 | }
128 |
129 | render() {
130 |
131 | const {user, message, isLogin} = this.state;
132 |
133 | return (
134 |
135 | this.ref = ref}>
136 |
137 |
170 |
171 |
172 | );
173 | }
174 | }
--------------------------------------------------------------------------------
/app/src/components/user-menu.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react'
2 |
3 |
4 | export default class UserMenu extends Component{
5 |
6 | constructor(props){
7 | super(props);
8 |
9 |
10 |
11 | this.onClickOutside = this.onClickOutside.bind(this);
12 |
13 |
14 |
15 | }
16 |
17 |
18 | onClickOutside(event){
19 |
20 | if(this.ref && !this.ref.contains(event.target)){
21 |
22 |
23 | if(this.props.onClose){
24 | this.props.onClose();
25 | }
26 |
27 | }
28 | }
29 |
30 | componentDidMount(){
31 |
32 | window.addEventListener('mousedown', this.onClickOutside);
33 |
34 | }
35 | componentWillUnmount(){
36 |
37 | window.removeEventListener('mousedown', this.onClickOutside);
38 |
39 | }
40 |
41 |
42 |
43 | render(){
44 |
45 | const {store} = this.props;
46 |
47 | const user = store.getCurrentUser();
48 |
49 | return this.ref = ref}>
50 | {user ?
51 |
52 |
My menu
53 |
54 |
62 |
63 |
64 |
: null }
65 |
66 |
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/config.js:
--------------------------------------------------------------------------------
1 | export const production = false; // set it to true when deploy to the server
2 |
3 | const domain = production ? '139.59.227.127' : '127.0.0.1:3001'; // if you have domain pointed to digitalOcean Cloud server let use your domain.eg: tabvn.com
4 | export const websocketUrl = `ws://${domain}`
5 | export const apiUrl = `http://${domain}`
--------------------------------------------------------------------------------
/app/src/css/.sass-cache/c6c61f1d3471adfa6b4f36ea934cb8d28f43b0b7/_font.scssc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tabvn/nodejs-reactjs-chatapp/4ff49c67588594195f09ae83b9b03e837813e0d4/app/src/css/.sass-cache/c6c61f1d3471adfa6b4f36ea934cb8d28f43b0b7/_font.scssc
--------------------------------------------------------------------------------
/app/src/css/.sass-cache/c6c61f1d3471adfa6b4f36ea934cb8d28f43b0b7/_variable.scssc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tabvn/nodejs-reactjs-chatapp/4ff49c67588594195f09ae83b9b03e837813e0d4/app/src/css/.sass-cache/c6c61f1d3471adfa6b4f36ea934cb8d28f43b0b7/_variable.scssc
--------------------------------------------------------------------------------
/app/src/css/.sass-cache/c6c61f1d3471adfa6b4f36ea934cb8d28f43b0b7/app.scssc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tabvn/nodejs-reactjs-chatapp/4ff49c67588594195f09ae83b9b03e837813e0d4/app/src/css/.sass-cache/c6c61f1d3471adfa6b4f36ea934cb8d28f43b0b7/app.scssc
--------------------------------------------------------------------------------
/app/src/css/_font.scss:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | @font-face {
4 | font-family: "chatapp";
5 | src:url("./fonts/chatapp.eot");
6 | src:url("./fonts/chatapp.eot?#iefix") format("embedded-opentype"),
7 | url("./fonts/chatapp.woff") format("woff"),
8 | url("./fonts/chatapp.ttf") format("truetype"),
9 | url("./fonts/chatapp.svg#chatapp") format("svg");
10 | font-weight: normal;
11 | font-style: normal;
12 |
13 | }
14 |
15 | [data-icon]:before {
16 | font-family: "chatapp" !important;
17 | content: attr(data-icon);
18 | font-style: normal !important;
19 | font-weight: normal !important;
20 | font-variant: normal !important;
21 | text-transform: none !important;
22 | speak: none;
23 | line-height: 1;
24 | -webkit-font-smoothing: antialiased;
25 | -moz-osx-font-smoothing: grayscale;
26 | }
27 |
28 | [class^="icon-"]:before,
29 | [class*=" icon-"]:before {
30 | font-family: "chatapp" !important;
31 | font-style: normal !important;
32 | font-weight: normal !important;
33 | font-variant: normal !important;
34 | text-transform: none !important;
35 | speak: none;
36 | line-height: 1;
37 | -webkit-font-smoothing: antialiased;
38 | -moz-osx-font-smoothing: grayscale;
39 | }
40 |
41 | .icon-edit-modify-streamline:before {
42 | content: "\61";
43 | }
44 | .icon-settings-streamline-1:before {
45 | content: "\63";
46 | }
47 | .icon-paperplane:before {
48 | content: "\62";
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/css/_variable.scss:
--------------------------------------------------------------------------------
1 | $header-height: 50px;
2 | $left-sidebar-width: 200px;
3 | $right-sidebar-width: 300px;
4 | $border-color: rgba(0, 0, 0, 0.05);
5 | $primary-color: #2ecc71;
6 | $danger-color: #e74c3c;
7 | $body-color: #2c3e50;
--------------------------------------------------------------------------------
/app/src/css/app.css:
--------------------------------------------------------------------------------
1 | @import "https://fonts.googleapis.com/css?family=Open+Sans:400,600";
2 | @font-face {
3 | font-family: "chatapp";
4 | src: url("./fonts/chatapp.eot");
5 | src: url("./fonts/chatapp.eot?#iefix") format("embedded-opentype"), url("./fonts/chatapp.woff") format("woff"), url("./fonts/chatapp.ttf") format("truetype"), url("./fonts/chatapp.svg#chatapp") format("svg");
6 | font-weight: normal;
7 | font-style: normal; }
8 | [data-icon]:before {
9 | font-family: "chatapp" !important;
10 | content: attr(data-icon);
11 | font-style: normal !important;
12 | font-weight: normal !important;
13 | font-variant: normal !important;
14 | text-transform: none !important;
15 | speak: none;
16 | line-height: 1;
17 | -webkit-font-smoothing: antialiased;
18 | -moz-osx-font-smoothing: grayscale; }
19 |
20 | [class^="icon-"]:before,
21 | [class*=" icon-"]:before {
22 | font-family: "chatapp" !important;
23 | font-style: normal !important;
24 | font-weight: normal !important;
25 | font-variant: normal !important;
26 | text-transform: none !important;
27 | speak: none;
28 | line-height: 1;
29 | -webkit-font-smoothing: antialiased;
30 | -moz-osx-font-smoothing: grayscale; }
31 |
32 | .icon-edit-modify-streamline:before {
33 | content: "\61"; }
34 |
35 | .icon-settings-streamline-1:before {
36 | content: "\63"; }
37 |
38 | .icon-paperplane:before {
39 | content: "\62"; }
40 |
41 | body, html {
42 | margin: 0;
43 | padding: 0;
44 | height: 100%; }
45 |
46 | body {
47 | color: #2c3e50;
48 | font-size: 13px;
49 | font-family: 'Open Sans', sans-serif; }
50 |
51 | * {
52 | box-sizing: border-box;
53 | padding: 0;
54 | margin: 0; }
55 |
56 | .app-messenger {
57 | display: flex;
58 | flex-direction: column; }
59 | .app-messenger .header {
60 | height: 50px;
61 | display: flex;
62 | flex-direction: row;
63 | border-bottom: 1px solid rgba(0, 0, 0, 0.05); }
64 | .app-messenger .header .left {
65 | width: 200px;
66 | position: relative; }
67 | .app-messenger .header .left .left-action {
68 | position: absolute;
69 | left: 8px;
70 | top: 0; }
71 | .app-messenger .header .left .right-action {
72 | position: absolute;
73 | right: 8px;
74 | top: 0; }
75 | .app-messenger .header .left h2 {
76 | line-height: 50px;
77 | font-size: 14px;
78 | font-weight: 600;
79 | display: block;
80 | text-align: center; }
81 | .app-messenger .header .left button {
82 | background: none;
83 | line-height: 50px;
84 | border: 0 none;
85 | font-size: 20px;
86 | cursor: pointer; }
87 | .app-messenger .header .content {
88 | flex-grow: 1; }
89 | .app-messenger .header .content h2 {
90 | line-height: 50px;
91 | text-align: center; }
92 | .app-messenger .header .right {
93 | width: 300px; }
94 | .app-messenger .header .right .user-bar {
95 | line-height: 50px;
96 | display: flex;
97 | justify-content: flex-end;
98 | padding: 0 10px; }
99 | .app-messenger .header .right .user-bar .profile-name {
100 | padding-right: 10px; }
101 | .app-messenger .header .right .user-bar .profile-image {
102 | line-height: 50px; }
103 | .app-messenger .header .right .user-bar .profile-image img {
104 | width: 30px;
105 | height: 30px;
106 | border-radius: 50%;
107 | margin: 10px 0 0 0; }
108 | .app-messenger .main {
109 | height: 100%;
110 | display: flex;
111 | overflow: hidden; }
112 | .app-messenger .main .sidebar-left {
113 | width: 200px;
114 | border-right: 1px solid rgba(0, 0, 0, 0.05); }
115 | .app-messenger .main .sidebar-right {
116 | border-left: 1px solid rgba(0, 0, 0, 0.05);
117 | width: 300px; }
118 | .app-messenger .main .sidebar-right .title {
119 | padding: 10px; }
120 | .app-messenger .main .content {
121 | flex-grow: 1;
122 | overflow: hidden;
123 | display: flex;
124 | flex-direction: column; }
125 | .app-messenger .main .content .messages {
126 | flex-grow: 1; }
127 | .app-messenger .main .content .messenger-input {
128 | border-top: 1px solid rgba(0, 0, 0, 0.05);
129 | height: 50px;
130 | display: flex;
131 | flex-direction: row; }
132 | .app-messenger .main .content .messenger-input .text-input {
133 | flex-grow: 1; }
134 | .app-messenger .main .content .messenger-input .text-input textarea {
135 | border: 0 none;
136 | width: 100%;
137 | height: 100%;
138 | padding: 8px 15px; }
139 | .app-messenger .main .content .messenger-input .actions button.send {
140 | background: #2ecc71;
141 | color: #FFF;
142 | border: 0 none;
143 | padding: 7px 15px;
144 | line-height: 50px; }
145 |
146 | .messages {
147 | display: flex;
148 | flex-direction: column;
149 | overflow-y: auto;
150 | height: 100%; }
151 | .messages .message {
152 | display: flex;
153 | flex-direction: row;
154 | justify-content: flex-start;
155 | margin: 15px; }
156 | .messages .message .message-user-image img {
157 | width: 20px;
158 | height: 20px;
159 | border-radius: 50%; }
160 | .messages .message .message-body {
161 | padding-left: 10px; }
162 | .messages .message .message-body .message-text {
163 | background: rgba(0, 0, 0, 0.05);
164 | padding: 10px;
165 | border-radius: 10px; }
166 | .messages .message.me {
167 | justify-content: flex-end; }
168 | .messages .message.me .message-body .message-text {
169 | background: #2ecc71;
170 | color: #FFF; }
171 |
172 | .chanels {
173 | overflow-y: auto;
174 | height: 100%; }
175 | .chanels .chanel {
176 | cursor: pointer;
177 | display: flex;
178 | border-bottom: 1px solid rgba(0, 0, 0, 0.05);
179 | padding: 8px; }
180 | .chanels .chanel .user-image {
181 | width: 30px; }
182 | .chanels .chanel .user-image img {
183 | max-width: 100%; }
184 | .chanels .chanel .user-image .channel-avatars {
185 | overflow: hidden;
186 | width: 30px;
187 | height: 30px;
188 | border-radius: 50%;
189 | background-color: #ccc;
190 | position: relative; }
191 | .chanels .chanel .user-image .channel-avatars.channel-avatars-1 img {
192 | width: 100%;
193 | height: 100%;
194 | border-radius: 50%; }
195 | .chanels .chanel .user-image .channel-avatars.channel-avatars-2 img {
196 | width: 50%;
197 | height: 100%;
198 | position: absolute;
199 | right: 0;
200 | top: 0; }
201 | .chanels .chanel .user-image .channel-avatars.channel-avatars-2 img:first-child {
202 | left: 0;
203 | top: 0; }
204 | .chanels .chanel .user-image .channel-avatars.channel-avatars-3 img {
205 | position: absolute;
206 | width: 50%;
207 | height: 50%;
208 | right: 0;
209 | top: 0; }
210 | .chanels .chanel .user-image .channel-avatars.channel-avatars-3 img:first-child {
211 | left: 0;
212 | top: 0;
213 | width: 50%;
214 | height: 100%; }
215 | .chanels .chanel .user-image .channel-avatars.channel-avatars-3 img:last-child {
216 | bottom: 0;
217 | right: 0;
218 | top: 15px;
219 | width: 50%;
220 | height: 50%; }
221 | .chanels .chanel .user-image .channel-avatars.channel-avatars-4 img {
222 | position: absolute;
223 | width: 50%;
224 | height: 50%;
225 | right: 0;
226 | top: 0; }
227 | .chanels .chanel .user-image .channel-avatars.channel-avatars-4 img:first-child {
228 | left: 0;
229 | top: 0;
230 | width: 50%;
231 | height: 100%; }
232 | .chanels .chanel .user-image .channel-avatars.channel-avatars-4 img:nth-child(3n) {
233 | bottom: 0;
234 | right: 0;
235 | top: 15px;
236 | width: 50%;
237 | height: 50%; }
238 | .chanels .chanel .user-image .channel-avatars.channel-avatars-4 img:last-child {
239 | left: 0;
240 | bottom: 0;
241 | top: 15px; }
242 | .chanels .chanel .chanel-info {
243 | flex-grow: 1;
244 | padding-left: 8px;
245 | padding-right: 8px;
246 | overflow: hidden; }
247 | .chanels .chanel .chanel-info h2 {
248 | font-size: 13px;
249 | font-weight: 400;
250 | white-space: nowrap;
251 | text-overflow: ellipsis;
252 | overflow: hidden; }
253 | .chanels .chanel .chanel-info p {
254 | font-size: 12px;
255 | white-space: nowrap;
256 | text-overflow: ellipsis;
257 | overflow: hidden; }
258 | .chanels .chanel.active {
259 | background: rgba(0, 0, 0, 0.05); }
260 | .chanels .chanel.notify .chanel-info p {
261 | color: #2ecc71; }
262 |
263 | .members .member {
264 | display: flex;
265 | border-bottom: 1px solid rgba(0, 0, 0, 0.05);
266 | padding: 8px; }
267 | .members .member .user-image {
268 | width: 30px;
269 | position: relative; }
270 | .members .member .user-image img {
271 | width: 30px;
272 | height: 30px;
273 | border-radius: 50%; }
274 | .members .member .user-image .user-status {
275 | width: 8px;
276 | height: 8px;
277 | display: block;
278 | position: absolute;
279 | right: 0;
280 | bottom: 10px;
281 | border: 1px solid #FFFFFF;
282 | background: #cccccc;
283 | -webkit-border-radius: 50%;
284 | -moz-border-radius: 50%;
285 | border-radius: 50%; }
286 | .members .member .user-image .user-status.online {
287 | background: #2ecc71; }
288 | .members .member .member-info {
289 | padding-left: 8px;
290 | flex-grow: 1; }
291 | .members .member .member-info h2 {
292 | font-size: 14px; }
293 | .members .member .member-info p {
294 | font-size: 12px; }
295 |
296 | h2.title {
297 | font-size: 16px;
298 | font-weight: 600;
299 | color: rgba(0, 0, 0, 0.8); }
300 |
301 | .toolbar {
302 | height: 50px;
303 | display: flex;
304 | flex-direction: row;
305 | position: relative; }
306 | .toolbar span {
307 | line-height: 20px;
308 | height: 30px;
309 | background: #2ecc71;
310 | color: #FFF;
311 | cursor: pointer;
312 | display: block;
313 | border-radius: 3px;
314 | margin: 10px 5px 0 0;
315 | padding: 5px 8px; }
316 | .toolbar label {
317 | line-height: 50px; }
318 | .toolbar input {
319 | height: 30px;
320 | line-height: 30px;
321 | margin-top: 10px;
322 | border: 0 none; }
323 | .toolbar .search-user {
324 | min-width: 180px;
325 | position: absolute;
326 | left: 0;
327 | top: 50px;
328 | z-index: 1;
329 | border: 1px solid rgba(0, 0, 0, 0.05);
330 | border-top: 0 none; }
331 | .toolbar .search-user .user-list {
332 | display: flex;
333 | flex-direction: column; }
334 | .toolbar .search-user .user-list .user {
335 | display: flex;
336 | flex-direction: row;
337 | padding: 5px;
338 | border-bottom: 1px solid rgba(0, 0, 0, 0.05);
339 | cursor: pointer; }
340 | .toolbar .search-user .user-list .user img {
341 | width: 30px;
342 | height: 30px;
343 | border-radius: 50%;
344 | margin-top: 10px; }
345 | .toolbar .search-user .user-list .user h2 {
346 | padding-left: 8px;
347 | flex-grow: 1;
348 | font-size: 14px; }
349 | .toolbar .search-user .user-list .user:last-child {
350 | border-bottom: 0 none; }
351 | .toolbar .search-user .user-list .user:hover {
352 | background: rgba(0, 0, 0, 0.02); }
353 |
354 | .user-bar {
355 | position: relative; }
356 | .user-bar button.login-btn {
357 | height: 50px;
358 | border: 0 none;
359 | background: none;
360 | color: #2ecc71;
361 | font-weight: 600;
362 | font-size: 14px; }
363 | .user-bar .user-form {
364 | background: #FFF;
365 | box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.09);
366 | position: absolute;
367 | top: 50px;
368 | right: 0;
369 | border: 1px solid rgba(0, 0, 0, 0.05);
370 | border-top: 0 none;
371 | padding: 10px; }
372 | .user-bar .user-form .form-item label {
373 | line-height: 30px;
374 | min-width: 75px;
375 | text-align: right;
376 | margin-right: 8px; }
377 | .user-bar .user-form .form-item input[type="email"], .user-bar .user-form .form-item input[type="password"], .user-bar .user-form .form-item input[type="text"] {
378 | height: 30px;
379 | line-height: 30px; }
380 | .user-bar .user-form .form-actions {
381 | display: flex;
382 | flex-direction: row;
383 | justify-content: flex-end; }
384 | .user-bar .user-menu {
385 | background: #FFF;
386 | box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.09);
387 | min-width: 200px;
388 | position: absolute;
389 | right: 0;
390 | top: 50px;
391 | border: 1px solid rgba(0, 0, 0, 0.05);
392 | border-top: 0 none; }
393 | .user-bar .user-menu ul {
394 | padding: 0;
395 | margin: 0;
396 | list-style: none; }
397 | .user-bar .user-menu ul li {
398 | border-top: 1px solid rgba(0, 0, 0, 0.05);
399 | padding: 8px; }
400 | .user-bar .user-menu ul li button {
401 | background: none;
402 | border: 0 none;
403 | display: block;
404 | cursor: pointer;
405 | text-align: center;
406 | width: 100%; }
407 | .user-bar .user-menu ul li:hover {
408 | background: rgba(0, 0, 0, 0.09); }
409 | .user-bar .user-menu h2 {
410 | font-size: 14px;
411 | font-weight: 600;
412 | margin: 0;
413 | display: block;
414 | text-align: center; }
415 |
416 | .form-item {
417 | display: flex;
418 | margin-bottom: 10px; }
419 | .form-item label {
420 | font-weight: 600; }
421 | .form-item input[type="email"], .form-item input[type="password"], .form-item input[type="text"] {
422 | border: 1px solid rgba(0, 0, 0, 0.05);
423 | padding: 3px 8px; }
424 |
425 | .form-actions button {
426 | border: 0 none;
427 | padding: 7px 15px;
428 | text-align: center; }
429 | .form-actions button.primary {
430 | background: #2ecc71;
431 | color: #FFF; }
432 |
433 | .app-message {
434 | line-height: 1.5em;
435 | padding: 10px;
436 | font-size: 12px;
437 | text-align: center;
438 | border: 1px solid #2ecc71;
439 | border-radius: 5px;
440 | margin: 0 0 10px 0; }
441 | .app-message.error {
442 | background: #e74c3c;
443 | color: #FFF;
444 | border-color: #e74c3c; }
445 |
446 | .user-status {
447 | font-size: 10px;
448 | color: #2c3e50; }
449 | .user-status.online {
450 | color: #2ecc71; }
451 |
452 | .app-warning-state {
453 | font-size: 10px;
454 | padding: 0 10px;
455 | color: #e74c3c; }
456 |
457 | /*# sourceMappingURL=app.css.map */
458 |
--------------------------------------------------------------------------------
/app/src/css/app.css.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "mappings": "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",
4 | "sources": ["app.scss","_font.scss","_variable.scss"],
5 | "names": [],
6 | "file": "app.css"
7 | }
--------------------------------------------------------------------------------
/app/src/css/app.scss:
--------------------------------------------------------------------------------
1 | @import "font";
2 | @import "https://fonts.googleapis.com/css?family=Open+Sans:400,600";
3 | @import 'variable';
4 |
5 | body, html {
6 | margin: 0;
7 | padding: 0;
8 | height: 100%;
9 | }
10 |
11 | body {
12 | color: $body-color;
13 | font-size: 13px;
14 | font-family: 'Open Sans', sans-serif;
15 | }
16 |
17 | * {
18 | box-sizing: border-box;
19 | padding: 0;
20 | margin: 0;
21 | }
22 |
23 | .app-messenger {
24 | display: flex;
25 | flex-direction: column;
26 | .header {
27 | height: $header-height;
28 | display: flex;
29 | flex-direction: row;
30 | border-bottom: 1px solid $border-color;
31 | .left {
32 | width: $left-sidebar-width;
33 | position: relative;
34 | .left-action {
35 | position: absolute;
36 | left: 8px;
37 | top: 0;
38 | }
39 | .right-action {
40 | position: absolute;
41 | right: 8px;
42 | top: 0;
43 | }
44 | h2 {
45 | line-height: $header-height;
46 | font-size: 14px;
47 | font-weight: 600;
48 | display: block;
49 | text-align: center;
50 | }
51 | button {
52 | background: none;
53 | line-height: $header-height;
54 | border: 0 none;
55 | font-size: 20px;
56 | cursor: pointer;
57 |
58 | }
59 | }
60 | .content {
61 | flex-grow: 1;
62 |
63 | h2 {
64 | line-height: $header-height;
65 | text-align: center;
66 | }
67 | }
68 | .right {
69 | width: $right-sidebar-width;
70 | .user-bar {
71 | line-height: $header-height;
72 | display: flex;
73 | justify-content: flex-end;
74 | padding: 0 10px;
75 | .profile-name {
76 | padding-right: 10px;
77 | }
78 | .profile-image {
79 | line-height: $header-height;
80 | img {
81 | width: 30px;
82 | height: 30px;
83 | border-radius: 50%;
84 | margin: 10px 0 0 0;
85 | }
86 | }
87 | }
88 | }
89 |
90 | }
91 | .main {
92 | height: 100%;
93 | display: flex;
94 | overflow: hidden;
95 | .sidebar-left {
96 |
97 | width: $left-sidebar-width;
98 | border-right: 1px solid $border-color;
99 |
100 | }
101 | .sidebar-right {
102 | border-left: 1px solid $border-color;
103 | width: $right-sidebar-width;
104 | .title {
105 | padding: 10px;
106 | }
107 | }
108 | .content {
109 | flex-grow: 1;
110 | overflow: hidden;
111 | display: flex;
112 | flex-direction: column;
113 | .messages {
114 | flex-grow: 1;
115 | }
116 | .messenger-input {
117 | border-top: 1px solid $border-color;
118 | height: 50px;
119 | display: flex;
120 | flex-direction: row;
121 | .text-input {
122 | flex-grow: 1;
123 | textarea {
124 | border: 0 none;
125 | width: 100%;
126 | height: 100%;
127 | padding: 8px 15px;
128 |
129 | }
130 | }
131 | .actions {
132 |
133 | button.send {
134 | background: $primary-color;
135 | color: #FFF;
136 | border: 0 none;
137 | padding: 7px 15px;
138 | line-height: 50px;
139 | }
140 | }
141 | }
142 |
143 | }
144 |
145 | }
146 | }
147 |
148 | .messages {
149 | display: flex;
150 | flex-direction: column;
151 | overflow-y: auto;
152 | height: 100%;
153 | .message {
154 | display: flex;
155 | flex-direction: row;
156 | justify-content: flex-start;
157 | margin: 15px;
158 | .message-user-image {
159 | img {
160 | width: 20px;
161 | height: 20px;
162 | border-radius: 50%;
163 | }
164 | }
165 | .message-body {
166 | padding-left: 10px;
167 | .message-author {
168 |
169 | }
170 | .message-text {
171 | background: rgba(0, 0, 0, 0.05);
172 | padding: 10px;
173 | border-radius: 10px;
174 | }
175 | }
176 |
177 | &.me {
178 | justify-content: flex-end;
179 | .message-body {
180 | .message-text {
181 | background: $primary-color;
182 | color: #FFF;
183 | }
184 | }
185 | }
186 | }
187 | }
188 |
189 | .chanels {
190 |
191 | overflow-y: auto;
192 | height: 100%;
193 | .chanel {
194 | cursor: pointer;
195 | display: flex;
196 | border-bottom: 1px solid $border-color;
197 | padding: 8px;
198 | .user-image {
199 | width: 30px;
200 | img {
201 | max-width: 100%;
202 | }
203 | .channel-avatars {
204 | overflow: hidden;
205 | width: 30px;
206 | height: 30px;
207 | border-radius: 50%;
208 | background-color: #ccc;
209 | position: relative;
210 |
211 | &.channel-avatars-1 {
212 | img {
213 | width: 100%;
214 | height: 100%;
215 | border-radius: 50%;
216 | }
217 | }
218 | &.channel-avatars-2 {
219 |
220 | img {
221 | width: 50%;
222 | height: 100%;
223 | position: absolute;
224 | right: 0;
225 | top: 0;
226 | &:first-child {
227 | left: 0;
228 | top: 0;
229 | }
230 | }
231 | }
232 | &.channel-avatars-3 {
233 |
234 | img {
235 | position: absolute;
236 | width: 50%;
237 | height: 50%;
238 | right: 0;
239 | top: 0;
240 | &:first-child {
241 | left: 0;
242 | top: 0;
243 | width: 50%;
244 | height: 100%;
245 | }
246 | &:last-child {
247 | bottom: 0;
248 | right: 0;
249 | top: 15px;
250 | width: 50%;
251 | height: 50%;
252 | }
253 |
254 | }
255 | }
256 | &.channel-avatars-4 {
257 | img {
258 | position: absolute;
259 | width: 50%;
260 | height: 50%;
261 | right: 0;
262 | top: 0;
263 | &:first-child {
264 | left: 0;
265 | top: 0;
266 | width: 50%;
267 | height: 100%;
268 | }
269 | &:nth-child(3n) {
270 | bottom: 0;
271 | right: 0;
272 | top: 15px;
273 | width: 50%;
274 | height: 50%;
275 | }
276 | &:last-child {
277 |
278 | left: 0;
279 | bottom: 0;
280 | top: 15px;
281 | }
282 |
283 | }
284 | }
285 | }
286 | }
287 | .chanel-info {
288 | flex-grow: 1;
289 | padding-left: 8px;
290 | padding-right: 8px;
291 | overflow: hidden;
292 | h2 {
293 | font-size: 13px;
294 | font-weight: 400;
295 | white-space: nowrap;
296 | text-overflow: ellipsis;
297 | overflow: hidden;
298 | }
299 | p {
300 | font-size: 12px;
301 | white-space: nowrap;
302 | text-overflow: ellipsis;
303 | overflow: hidden;
304 | }
305 | }
306 | &.active {
307 | background: rgba(0, 0, 0, 0.05);
308 | }
309 | &.notify {
310 | .chanel-info {
311 | p {
312 | color: $primary-color;
313 | }
314 | }
315 | }
316 | }
317 | }
318 |
319 | .members {
320 |
321 | .member {
322 | display: flex;
323 | border-bottom: 1px solid $border-color;
324 | padding: 8px;
325 | .user-image {
326 | width: 30px;
327 | position: relative;
328 | img {
329 | width: 30px;
330 | height: 30px;
331 | border-radius: 50%;
332 | }
333 | .user-status{
334 | width: 8px;
335 | height: 8px;
336 | display: block;
337 | position: absolute;
338 | right: 0;
339 | bottom: 10px;
340 | border: 1px solid #FFFFFF;
341 | background: #cccccc;
342 | -webkit-border-radius: 50%;
343 | -moz-border-radius: 50%;
344 | border-radius: 50%;
345 | &.online{
346 | background: $primary-color;
347 | }
348 | }
349 | }
350 | .member-info {
351 | padding-left: 8px;
352 | flex-grow: 1;
353 | h2 {
354 | font-size: 14px;
355 | }
356 | p {
357 |
358 | font-size: 12px;
359 | }
360 | }
361 | }
362 | }
363 |
364 | h2.title {
365 | font-size: 16px;
366 | font-weight: 600;
367 | color: rgba(0, 0, 0, 0.8);
368 | }
369 |
370 | .toolbar {
371 | height: $header-height;
372 | display: flex;
373 | flex-direction: row;
374 | position: relative;
375 | span {
376 | line-height: 20px;
377 | height: 30px;
378 | background: $primary-color;
379 | color: #FFF;
380 | cursor: pointer;
381 | display: block;
382 | border-radius: 3px;
383 | margin: 10px 5px 0 0;
384 | padding: 5px 8px;
385 | }
386 | label {
387 | line-height: $header-height;
388 | }
389 | input {
390 | height: 30px;
391 | line-height: 30px;
392 | margin-top: 10px;
393 | border: 0 none;
394 | }
395 | .search-user {
396 | min-width: 180px;
397 | position: absolute;
398 | left: 0;
399 | top: $header-height;
400 | z-index: 1;
401 | border: 1px solid $border-color;
402 | border-top: 0 none;
403 | .user-list {
404 | display: flex;
405 | flex-direction: column;
406 | .user {
407 | display: flex;
408 | flex-direction: row;
409 | padding: 5px;
410 | border-bottom: 1px solid $border-color;
411 | cursor: pointer;
412 | img {
413 |
414 | width: 30px;
415 | height: 30px;
416 | border-radius: 50%;
417 | margin-top: 10px;
418 | }
419 | h2 {
420 | padding-left: 8px;
421 | flex-grow: 1;
422 | font-size: 14px;
423 | }
424 | &:last-child {
425 | border-bottom: 0 none;
426 | }
427 | &:hover {
428 | background: rgba(0, 0, 0, 0.02);
429 | }
430 | }
431 | }
432 | }
433 | }
434 |
435 | .user-bar {
436 | position: relative;
437 | button.login-btn {
438 | height: $header-height;
439 | border: 0 none;
440 | background: none;
441 | color: $primary-color;
442 | font-weight: 600;
443 | font-size: 14px;
444 | }
445 |
446 | .user-form {
447 | background: #FFF;
448 | box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.09);
449 | position: absolute;
450 | top: $header-height;
451 | right: 0;
452 | border: 1px solid $border-color;
453 | border-top: 0 none;
454 | padding: 10px;
455 | .form-item {
456 | label {
457 | line-height: 30px;
458 | min-width: 75px;
459 | text-align: right;
460 | margin-right: 8px;
461 | }
462 | input[type="email"], input[type="password"], input[type="text"] {
463 |
464 | height: 30px;
465 | line-height: 30px;
466 |
467 | }
468 | }
469 | .form-actions {
470 | display: flex;
471 | flex-direction: row;
472 | justify-content: flex-end;
473 | }
474 | }
475 | .user-menu {
476 | background: #FFF;
477 | box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.09);
478 | min-width: 200px;
479 | position: absolute;
480 | right: 0;
481 | top: $header-height;
482 | border: 1px solid $border-color;
483 | border-top: 0 none;
484 | ul {
485 | padding: 0;
486 | margin: 0;
487 | list-style: none;
488 | li {
489 | border-top: 1px solid $border-color;
490 | padding: 8px;
491 | button {
492 | background: none;
493 | border: 0 none;
494 | display: block;
495 | cursor: pointer;
496 | text-align: center;
497 | width: 100%;
498 | }
499 | &:hover {
500 | background: rgba(0, 0, 0, 0.09);
501 |
502 | }
503 | }
504 | }
505 | h2 {
506 | font-size: 14px;
507 | font-weight: 600;
508 | margin: 0;
509 | display: block;
510 | text-align: center;
511 | }
512 | }
513 | }
514 |
515 | .form-item {
516 | display: flex;
517 | margin-bottom: 10px;
518 | label {
519 | font-weight: 600;
520 | }
521 | input[type="email"], input[type="password"], input[type="text"] {
522 | border: 1px solid $border-color;
523 | padding: 3px 8px;
524 | }
525 | }
526 |
527 | .form-actions {
528 | button {
529 | border: 0 none;
530 | padding: 7px 15px;
531 | text-align: center;
532 | &.primary {
533 | background: $primary-color;
534 | color: #FFF;
535 | }
536 | }
537 | }
538 |
539 | .app-message {
540 | line-height: 1.5em;
541 | padding: 10px;
542 | font-size: 12px;
543 | text-align: center;
544 | border: 1px solid $primary-color;
545 | border-radius: 5px;
546 | margin: 0 0 10px 0;
547 | &.error {
548 | background: $danger-color;
549 | color: #FFF;
550 | border-color: $danger-color;
551 | }
552 | }
553 | .user-status{
554 | font-size: 10px;
555 | color: $body-color;
556 | &.online{
557 | color: $primary-color;
558 | }
559 | }
560 | .app-warning-state{
561 | font-size: 10px;
562 | padding: 0 10px;
563 | color: $danger-color;
564 | }
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
--------------------------------------------------------------------------------
/app/src/css/fonts/chatapp.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tabvn/nodejs-reactjs-chatapp/4ff49c67588594195f09ae83b9b03e837813e0d4/app/src/css/fonts/chatapp.eot
--------------------------------------------------------------------------------
/app/src/css/fonts/chatapp.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/app/src/css/fonts/chatapp.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tabvn/nodejs-reactjs-chatapp/4ff49c67588594195f09ae83b9b03e837813e0d4/app/src/css/fonts/chatapp.ttf
--------------------------------------------------------------------------------
/app/src/css/fonts/chatapp.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tabvn/nodejs-reactjs-chatapp/4ff49c67588594195f09ae83b9b03e837813e0d4/app/src/css/fonts/chatapp.woff
--------------------------------------------------------------------------------
/app/src/helpers/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tabvn/nodejs-reactjs-chatapp/4ff49c67588594195f09ae83b9b03e837813e0d4/app/src/helpers/index.js
--------------------------------------------------------------------------------
/app/src/helpers/objectid.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Machine id.
3 | *
4 | * Create a random 3-byte value (i.e. unique for this
5 | * process). Other drivers use a md5 of the machine id here, but
6 | * that would mean an asyc call to gethostname, so we don't bother.
7 | * @ignore
8 | */
9 | var MACHINE_ID = parseInt(Math.random() * 0xffffff, 10);
10 |
11 | // Regular expression that checks for hex value
12 | var checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$');
13 |
14 | // Check if buffer exists
15 | try {
16 | if (Buffer && Buffer.from) var hasBufferType = true;
17 | } catch (err) {
18 | hasBufferType = false;
19 | }
20 |
21 | /**
22 | * Create a new ObjectID instance
23 | *
24 | * @class
25 | * @param {(string|number)} id Can be a 24 byte hex string, 12 byte binary string or a Number.
26 | * @property {number} generationTime The generation time of this ObjectId instance
27 | * @return {ObjectID} instance of ObjectID.
28 | */
29 | var ObjectID = function ObjectID(id) {
30 | // Duck-typing to support ObjectId from different npm packages
31 | if (id instanceof ObjectID) return id;
32 | if (!(this instanceof ObjectID)) return new ObjectID(id);
33 |
34 | this._bsontype = 'ObjectID';
35 |
36 | // The most common usecase (blank id, new objectId instance)
37 | if (id == null || typeof id === 'number') {
38 | // Generate a new id
39 | this.id = this.generate(id);
40 | // If we are caching the hex string
41 | if (ObjectID.cacheHexString) this.__id = this.toString('hex');
42 | // Return the object
43 | return;
44 | }
45 |
46 | // Check if the passed in id is valid
47 | var valid = ObjectID.isValid(id);
48 |
49 | // Throw an error if it's not a valid setup
50 | if (!valid && id != null) {
51 | throw new Error(
52 | 'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
53 | );
54 | } else if (valid && typeof id === 'string' && id.length === 24 && hasBufferType) {
55 | return new ObjectID(new Buffer(id, 'hex'));
56 | } else if (valid && typeof id === 'string' && id.length === 24) {
57 | return ObjectID.createFromHexString(id);
58 | } else if (id != null && id.length === 12) {
59 | // assume 12 byte string
60 | this.id = id;
61 | } else if (id != null && id.toHexString) {
62 | // Duck-typing to support ObjectId from different npm packages
63 | return id;
64 | } else {
65 | throw new Error(
66 | 'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
67 | );
68 | }
69 |
70 | if (ObjectID.cacheHexString) this.__id = this.toString('hex');
71 | };
72 |
73 | // Allow usage of ObjectId as well as ObjectID
74 | // var ObjectId = ObjectID;
75 |
76 | // Precomputed hex table enables speedy hex string conversion
77 | var hexTable = [];
78 | for (var i = 0; i < 256; i++) {
79 | hexTable[i] = (i <= 15 ? '0' : '') + i.toString(16);
80 | }
81 |
82 | /**
83 | * Return the ObjectID id as a 24 byte hex string representation
84 | *
85 | * @method
86 | * @return {string} return the 24 byte hex string representation.
87 | */
88 | ObjectID.prototype.toHexString = function() {
89 | if (ObjectID.cacheHexString && this.__id) return this.__id;
90 |
91 | var hexString = '';
92 | if (!this.id || !this.id.length) {
93 | throw new Error(
94 | 'invalid ObjectId, ObjectId.id must be either a string or a Buffer, but is [' +
95 | JSON.stringify(this.id) +
96 | ']'
97 | );
98 | }
99 |
100 | if (this.id instanceof _Buffer) {
101 | hexString = convertToHex(this.id);
102 | if (ObjectID.cacheHexString) this.__id = hexString;
103 | return hexString;
104 | }
105 |
106 | for (var i = 0; i < this.id.length; i++) {
107 | hexString += hexTable[this.id.charCodeAt(i)];
108 | }
109 |
110 | if (ObjectID.cacheHexString) this.__id = hexString;
111 | return hexString;
112 | };
113 |
114 | /**
115 | * Update the ObjectID index used in generating new ObjectID's on the driver
116 | *
117 | * @method
118 | * @return {number} returns next index value.
119 | * @ignore
120 | */
121 | ObjectID.prototype.get_inc = function() {
122 | return (ObjectID.index = (ObjectID.index + 1) % 0xffffff);
123 | };
124 |
125 | /**
126 | * Update the ObjectID index used in generating new ObjectID's on the driver
127 | *
128 | * @method
129 | * @return {number} returns next index value.
130 | * @ignore
131 | */
132 | ObjectID.prototype.getInc = function() {
133 | return this.get_inc();
134 | };
135 |
136 | /**
137 | * Generate a 12 byte id buffer used in ObjectID's
138 | *
139 | * @method
140 | * @param {number} [time] optional parameter allowing to pass in a second based timestamp.
141 | * @return {Buffer} return the 12 byte id buffer string.
142 | */
143 | ObjectID.prototype.generate = function(time) {
144 | if ('number' !== typeof time) {
145 | time = ~~(Date.now() / 1000);
146 | }
147 |
148 | // Use pid
149 | var pid =
150 | (typeof process === 'undefined' || process.pid === 1
151 | ? Math.floor(Math.random() * 100000)
152 | : process.pid) % 0xffff;
153 | var inc = this.get_inc();
154 | // Buffer used
155 | var buffer = new Buffer(12);
156 | // Encode time
157 | buffer[3] = time & 0xff;
158 | buffer[2] = (time >> 8) & 0xff;
159 | buffer[1] = (time >> 16) & 0xff;
160 | buffer[0] = (time >> 24) & 0xff;
161 | // Encode machine
162 | buffer[6] = MACHINE_ID & 0xff;
163 | buffer[5] = (MACHINE_ID >> 8) & 0xff;
164 | buffer[4] = (MACHINE_ID >> 16) & 0xff;
165 | // Encode pid
166 | buffer[8] = pid & 0xff;
167 | buffer[7] = (pid >> 8) & 0xff;
168 | // Encode index
169 | buffer[11] = inc & 0xff;
170 | buffer[10] = (inc >> 8) & 0xff;
171 | buffer[9] = (inc >> 16) & 0xff;
172 | // Return the buffer
173 | return buffer;
174 | };
175 |
176 | /**
177 | * Converts the id into a 24 byte hex string for printing
178 | *
179 | * @param {String} format The Buffer toString format parameter.
180 | * @return {String} return the 24 byte hex string representation.
181 | * @ignore
182 | */
183 | ObjectID.prototype.toString = function(format) {
184 | // Is the id a buffer then use the buffer toString method to return the format
185 | if (this.id && this.id.copy) {
186 | return this.id.toString(typeof format === 'string' ? format : 'hex');
187 | }
188 |
189 | // if(this.buffer )
190 | return this.toHexString();
191 | };
192 |
193 | /**
194 | * Converts to a string representation of this Id.
195 | *
196 | * @return {String} return the 24 byte hex string representation.
197 | * @ignore
198 | */
199 | ObjectID.prototype.inspect = ObjectID.prototype.toString;
200 |
201 | /**
202 | * Converts to its JSON representation.
203 | *
204 | * @return {String} return the 24 byte hex string representation.
205 | * @ignore
206 | */
207 | ObjectID.prototype.toJSON = function() {
208 | return this.toHexString();
209 | };
210 |
211 | /**
212 | * Compares the equality of this ObjectID with `otherID`.
213 | *
214 | * @method
215 | * @param {object} otherID ObjectID instance to compare against.
216 | * @return {boolean} the result of comparing two ObjectID's
217 | */
218 | ObjectID.prototype.equals = function equals(otherId) {
219 | // var id;
220 |
221 | if (otherId instanceof ObjectID) {
222 | return this.toString() === otherId.toString();
223 | } else if (
224 | typeof otherId === 'string' &&
225 | ObjectID.isValid(otherId) &&
226 | otherId.length === 12 &&
227 | this.id instanceof _Buffer
228 | ) {
229 | return otherId === this.id.toString('binary');
230 | } else if (typeof otherId === 'string' && ObjectID.isValid(otherId) && otherId.length === 24) {
231 | return otherId.toLowerCase() === this.toHexString();
232 | } else if (typeof otherId === 'string' && ObjectID.isValid(otherId) && otherId.length === 12) {
233 | return otherId === this.id;
234 | } else if (otherId != null && (otherId instanceof ObjectID || otherId.toHexString)) {
235 | return otherId.toHexString() === this.toHexString();
236 | } else {
237 | return false;
238 | }
239 | };
240 |
241 | /**
242 | * Returns the generation date (accurate up to the second) that this ID was generated.
243 | *
244 | * @method
245 | * @return {date} the generation date
246 | */
247 | ObjectID.prototype.getTimestamp = function() {
248 | var timestamp = new Date();
249 | var time = this.id[3] | (this.id[2] << 8) | (this.id[1] << 16) | (this.id[0] << 24);
250 | timestamp.setTime(Math.floor(time) * 1000);
251 | return timestamp;
252 | };
253 |
254 | /**
255 | * @ignore
256 | */
257 | ObjectID.index = ~~(Math.random() * 0xffffff);
258 |
259 | /**
260 | * @ignore
261 | */
262 | ObjectID.createPk = function createPk() {
263 | return new ObjectID();
264 | };
265 |
266 | /**
267 | * Creates an ObjectID from a second based number, with the rest of the ObjectID zeroed out. Used for comparisons or sorting the ObjectID.
268 | *
269 | * @method
270 | * @param {number} time an integer number representing a number of seconds.
271 | * @return {ObjectID} return the created ObjectID
272 | */
273 | ObjectID.createFromTime = function createFromTime(time) {
274 | var buffer = new Buffer([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
275 | // Encode time into first 4 bytes
276 | buffer[3] = time & 0xff;
277 | buffer[2] = (time >> 8) & 0xff;
278 | buffer[1] = (time >> 16) & 0xff;
279 | buffer[0] = (time >> 24) & 0xff;
280 | // Return the new objectId
281 | return new ObjectID(buffer);
282 | };
283 |
284 | // Lookup tables
285 | //var encodeLookup = '0123456789abcdef'.split('');
286 | var decodeLookup = [];
287 | i = 0;
288 | while (i < 10) decodeLookup[0x30 + i] = i++;
289 | while (i < 16) decodeLookup[0x41 - 10 + i] = decodeLookup[0x61 - 10 + i] = i++;
290 |
291 | var _Buffer = Buffer;
292 | var convertToHex = function(bytes) {
293 | return bytes.toString('hex');
294 | };
295 |
296 | /**
297 | * Creates an ObjectID from a hex string representation of an ObjectID.
298 | *
299 | * @method
300 | * @param {string} hexString create a ObjectID from a passed in 24 byte hexstring.
301 | * @return {ObjectID} return the created ObjectID
302 | */
303 | ObjectID.createFromHexString = function createFromHexString(string) {
304 | // Throw an error if it's not a valid setup
305 | if (typeof string === 'undefined' || (string != null && string.length !== 24)) {
306 | throw new Error(
307 | 'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
308 | );
309 | }
310 |
311 | // Use Buffer.from method if available
312 | if (hasBufferType) return new ObjectID(new Buffer(string, 'hex'));
313 |
314 | // Calculate lengths
315 | var array = new _Buffer(12);
316 | var n = 0;
317 | var i = 0;
318 |
319 | while (i < 24) {
320 | array[n++] = (decodeLookup[string.charCodeAt(i++)] << 4) | decodeLookup[string.charCodeAt(i++)];
321 | }
322 |
323 | return new ObjectID(array);
324 | };
325 |
326 | /**
327 | * Checks if a value is a valid bson ObjectId
328 | *
329 | * @method
330 | * @return {boolean} return true if the value is a valid bson ObjectId, return false otherwise.
331 | */
332 | ObjectID.isValid = function isValid(id) {
333 | if (id == null) return false;
334 |
335 | if (typeof id === 'number') {
336 | return true;
337 | }
338 |
339 | if (typeof id === 'string') {
340 | return id.length === 12 || (id.length === 24 && checkForHexRegExp.test(id));
341 | }
342 |
343 | if (id instanceof ObjectID) {
344 | return true;
345 | }
346 |
347 | if (id instanceof _Buffer) {
348 | return true;
349 | }
350 |
351 | // Duck-Typing detection of ObjectId like objects
352 | if (id.toHexString) {
353 | return id.id.length === 12 || (id.id.length === 24 && checkForHexRegExp.test(id.id));
354 | }
355 |
356 | return false;
357 | };
358 |
359 | /**
360 | * @ignore
361 | */
362 | Object.defineProperty(ObjectID.prototype, 'generationTime', {
363 | enumerable: true,
364 | get: function() {
365 | return this.id[3] | (this.id[2] << 8) | (this.id[1] << 16) | (this.id[0] << 24);
366 | },
367 | set: function(value) {
368 | // Encode time into first 4 bytes
369 | this.id[3] = value & 0xff;
370 | this.id[2] = (value >> 8) & 0xff;
371 | this.id[1] = (value >> 16) & 0xff;
372 | this.id[0] = (value >> 24) & 0xff;
373 | }
374 | });
375 |
376 | /**
377 | * Expose.
378 | */
379 | module.exports = ObjectID;
380 | module.exports.ObjectID = ObjectID;
381 | module.exports.ObjectId = ObjectID;
--------------------------------------------------------------------------------
/app/src/images/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tabvn/nodejs-reactjs-chatapp/4ff49c67588594195f09ae83b9b03e837813e0d4/app/src/images/avatar.png
--------------------------------------------------------------------------------
/app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './components/app';
4 | import './css/app.css'
5 |
6 |
7 | //import registerServiceWorker from './registerServiceWorker';
8 |
9 | ReactDOM.render(, document.getElementById('root'));
10 | //registerServiceWorker();
11 |
12 |
--------------------------------------------------------------------------------
/app/src/realtime.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import {OrderedMap} from 'immutable'
3 | import {websocketUrl} from './config'
4 |
5 | export default class Realtime {
6 |
7 |
8 | constructor(store) {
9 |
10 | this.store = store;
11 | this.ws = null;
12 | this.isConnected = false;
13 |
14 | this.connect();
15 | this.reconnect();
16 | }
17 |
18 |
19 | reconnect(){
20 |
21 | const store = this.store;
22 |
23 | window.setInterval(()=>{
24 |
25 | const user = store.getCurrentUser();
26 | if(user && !this.isConnected){
27 |
28 | console.log("try reconnecting...");
29 |
30 | this.connect();
31 | }
32 |
33 | }, 3000)
34 | }
35 |
36 | decodeMessage(msg) {
37 |
38 | let message = {};
39 |
40 | try {
41 |
42 | message = JSON.parse(msg);
43 |
44 | }
45 | catch (err) {
46 |
47 | console.log(err);
48 | }
49 |
50 | return message;
51 | }
52 |
53 | readMessage(msg) {
54 |
55 | const store = this.store;
56 | const currentUser = store.getCurrentUser();
57 | const currentUserId = _.toString(_.get(currentUser, '_id'));
58 | const message = this.decodeMessage(msg);
59 |
60 | const action = _.get(message, 'action', '');
61 |
62 | const payload = _.get(message, 'payload');
63 |
64 | switch (action) {
65 |
66 | case 'user_offline':
67 |
68 | this.onUpdateUserStatus(payload, false);
69 | break;
70 | case 'user_online':
71 |
72 | const isOnline = true;
73 | this.onUpdateUserStatus(payload, isOnline);
74 |
75 | break;
76 | case 'message_added':
77 |
78 | const activeChannel = store.getActiveChannel();
79 |
80 | let notify = _.get(activeChannel, '_id') !== _.get(payload, 'channelId') && currentUserId !== _.get(payload, 'userId');
81 | this.onAddMessage(payload, notify);
82 |
83 | break;
84 |
85 | case 'channel_added':
86 |
87 | // to do check payload object and insert new channel to store.
88 | this.onAddChannel(payload);
89 |
90 | break;
91 |
92 | default:
93 |
94 | break;
95 | }
96 |
97 |
98 | }
99 |
100 | onUpdateUserStatus(userId, isOnline = false){
101 |
102 | const store = this.store;
103 |
104 |
105 | store.users = store.users.update(userId, (user) => {
106 |
107 |
108 | if(user){
109 | user.online = isOnline;
110 |
111 | }
112 |
113 | return user;
114 |
115 | });
116 |
117 | store.update()
118 |
119 | }
120 | onAddMessage(payload, notify = false){
121 |
122 | const store = this.store;
123 | const currentUser = store.getCurrentUser();
124 | const currentUserId = _.toString(_.get(currentUser, '_id'));
125 |
126 | let user = _.get(payload, 'user');
127 |
128 |
129 | // add user to cache
130 | user = store.addUserToCache(user);
131 |
132 | const messageObject = {
133 | _id: payload._id,
134 | body: _.get(payload, 'body', ''),
135 | userId: _.get(payload, 'userId'),
136 | channelId: _.get(payload, 'channelId'),
137 | created: _.get(payload, 'created', new Date()),
138 | me: currentUserId === _.toString(_.get(payload, 'userId')),
139 | user: user,
140 |
141 | };
142 |
143 |
144 |
145 |
146 |
147 | store.setMessage(messageObject, notify);
148 |
149 | }
150 |
151 | onAddChannel(payload) {
152 |
153 | const store = this.store;
154 |
155 | const channelId = _.toString(_.get(payload, '_id'));
156 | const userId = `${payload.userId}`;
157 |
158 | const users = _.get(payload, 'users', []);
159 |
160 |
161 | let channel = {
162 | _id: channelId,
163 | title: _.get(payload, 'title', ''),
164 | isNew: false,
165 | lastMessage: _.get(payload, 'lastMessage'),
166 | members: new OrderedMap(),
167 | messages: new OrderedMap(),
168 | userId: userId,
169 | created: new Date(),
170 |
171 |
172 | };
173 |
174 | _.each(users, (user) => {
175 |
176 | // add this user to store.users collection
177 |
178 | const memberId = `${user._id}`;
179 |
180 | this.store.addUserToCache(user);
181 |
182 | channel.members = channel.members.set(memberId, true);
183 |
184 |
185 | });
186 |
187 |
188 |
189 | const channelMessages = store.messages.filter((m) => _.toString(m.channelId)=== channelId);
190 |
191 | channelMessages.forEach((msg) => {
192 |
193 | const msgId = _.toString(_.get(msg, '_id'));
194 | channel.messages = channel.messages.set(msgId, true);
195 |
196 | })
197 |
198 |
199 | store.addChannel(channelId, channel);
200 |
201 | }
202 |
203 | send(msg = {}) {
204 |
205 | const isConnected = this.isConnected;
206 |
207 | if (this.ws && isConnected) {
208 |
209 | const msgString = JSON.stringify(msg);
210 |
211 | this.ws.send(msgString);
212 | }
213 |
214 | }
215 |
216 | authentication() {
217 | const store = this.store;
218 |
219 | const tokenId = store.getUserTokenId();
220 |
221 | if (tokenId) {
222 |
223 | const message = {
224 | action: 'auth',
225 | payload: `${tokenId}`
226 | }
227 |
228 | this.send(message);
229 | }
230 |
231 | }
232 |
233 |
234 | connect() {
235 |
236 | //console.log("Begin connecting to server via websocket.");
237 |
238 | const ws = new WebSocket(websocketUrl);
239 | this.ws = ws;
240 |
241 |
242 | ws.onopen = () => {
243 |
244 |
245 | //console.log("You are connected");
246 |
247 | // let tell to the server who are you ?
248 |
249 | this.isConnected = true;
250 |
251 | this.authentication();
252 |
253 |
254 | ws.onmessage = (event) => {
255 |
256 | this.readMessage(_.get(event, 'data'));
257 |
258 |
259 | console.log("Mesage from the server: ", event.data);
260 | }
261 |
262 |
263 | }
264 |
265 | ws.onclose = () => {
266 |
267 | //console.log("You disconnected!!!");
268 | this.isConnected = false;
269 | //this.store.update();
270 |
271 | }
272 |
273 | ws.onerror = () => {
274 |
275 | this.isConnected = false;
276 | this.store.update();
277 | }
278 |
279 |
280 | }
281 | }
--------------------------------------------------------------------------------
/app/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 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 | } else {
39 | // Is not local host. Just register service worker
40 | registerValidSW(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 |
--------------------------------------------------------------------------------
/app/src/service.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {apiUrl} from './config'
3 |
4 | const apiURL = apiUrl;
5 |
6 | export default class Service{
7 |
8 | get(endpoint, options = null){
9 |
10 | const url = `${apiURL}/${endpoint}`;
11 |
12 | return axios.get(url, options);
13 | }
14 |
15 | post(endpoint = "", data = {}, options = {headers: {'Content-Type': 'application/json'}}){
16 |
17 | const url = `${apiURL}/${endpoint}`;
18 |
19 | return axios.post(url, data, options);
20 | }
21 |
22 | }
--------------------------------------------------------------------------------
/app/src/store.js:
--------------------------------------------------------------------------------
1 | import {OrderedMap} from 'immutable'
2 | import _ from 'lodash'
3 | import Service from './service'
4 | import Realtime from './realtime'
5 |
6 | export default class Store {
7 | constructor(appComponent) {
8 |
9 | this.app = appComponent;
10 | this.service = new Service();
11 | this.messages = new OrderedMap();
12 | this.channels = new OrderedMap();
13 | this.activeChannelId = null;
14 |
15 |
16 | this.token = this.getTokenFromLocalStore();
17 |
18 | this.user = this.getUserFromLocalStorage();
19 | this.users = new OrderedMap();
20 |
21 | this.search = {
22 | users: new OrderedMap(),
23 | }
24 |
25 |
26 | this.realtime = new Realtime(this);
27 |
28 | this.fetchUserChannels();
29 |
30 |
31 | }
32 |
33 | isConnected(){
34 |
35 | return this.realtime.isConnected;
36 | }
37 | fetchUserChannels(){
38 |
39 | const userToken = this.getUserTokenId();
40 |
41 | if(userToken){
42 |
43 |
44 | const options = {
45 | headers: {
46 | authorization: userToken,
47 | }
48 | }
49 |
50 | this.service.get(`api/me/channels`, options).then((response) => {
51 |
52 | const channels = response.data;
53 |
54 | _.each(channels, (c) => {
55 |
56 | this.realtime.onAddChannel(c);
57 | });
58 |
59 |
60 | const firstChannelId = _.get(channels, '[0]._id', null);
61 |
62 | this.fetchChannelMessages(firstChannelId);
63 |
64 |
65 | }).catch((err) => {
66 |
67 | console.log("An error fetching user channels", err);
68 | })
69 | }
70 | }
71 |
72 | addUserToCache(user) {
73 |
74 | user.avatar = this.loadUserAvatar(user);
75 | const id = _.toString(user._id);
76 | this.users = this.users.set(id, user);
77 |
78 |
79 | return user;
80 |
81 |
82 | }
83 |
84 | getUserTokenId() {
85 | return _.get(this.token, '_id', null);
86 | }
87 |
88 | loadUserAvatar(user) {
89 |
90 | return `https://api.adorable.io/avatars/100/${user._id}.png`
91 | }
92 |
93 | startSearchUsers(q = "") {
94 |
95 | // query to backend servr and get list of users.
96 | const data = {search: q};
97 |
98 | this.search.users = this.search.users.clear();
99 |
100 | this.service.post('api/users/search', data).then((response) => {
101 |
102 | // list of users matched.
103 | const users = _.get(response, 'data', []);
104 |
105 | _.each(users, (user) => {
106 |
107 | // cache to this.users
108 | // add user to this.search.users
109 |
110 | user.avatar = this.loadUserAvatar(user);
111 | const userId = `${user._id}`;
112 |
113 | this.users = this.users.set(userId, user);
114 | this.search.users = this.search.users.set(userId, user);
115 |
116 |
117 | });
118 |
119 |
120 | // update component
121 | this.update();
122 |
123 |
124 | }).catch((err) => {
125 |
126 |
127 | //console.log("searching errror", err);
128 | })
129 |
130 | }
131 |
132 | setUserToken(accessToken) {
133 |
134 | if (!accessToken) {
135 |
136 | this.localStorage.removeItem('token');
137 | this.token = null;
138 |
139 | return;
140 | }
141 |
142 | this.token = accessToken;
143 | localStorage.setItem('token', JSON.stringify(accessToken));
144 |
145 | }
146 |
147 | getTokenFromLocalStore() {
148 |
149 |
150 | if (this.token) {
151 | return this.token;
152 | }
153 |
154 | let token = null;
155 |
156 | const data = localStorage.getItem('token');
157 | if (data) {
158 |
159 | try {
160 |
161 | token = JSON.parse(data);
162 | }
163 | catch (err) {
164 |
165 | console.log(err);
166 | }
167 | }
168 |
169 | return token;
170 | }
171 |
172 | getUserFromLocalStorage() {
173 |
174 | let user = null;
175 | const data = localStorage.getItem('me');
176 | try {
177 |
178 | user = JSON.parse(data);
179 | }
180 | catch (err) {
181 |
182 | console.log(err);
183 | }
184 |
185 |
186 | if (user) {
187 |
188 | // try to connect to backend server and verify this user is exist.
189 | const token = this.getTokenFromLocalStore();
190 | const tokenId = _.get(token, '_id');
191 |
192 | const options = {
193 | headers: {
194 | authorization: tokenId,
195 | }
196 | }
197 | this.service.get('api/users/me', options).then((response) => {
198 |
199 | // this mean user is logged with this token id.
200 |
201 | const accessToken = response.data;
202 | const user = _.get(accessToken, 'user');
203 |
204 | this.setCurrentUser(user);
205 | this.setUserToken(accessToken);
206 |
207 | }).catch(err => {
208 |
209 | this.signOut();
210 |
211 | });
212 |
213 | }
214 | return user;
215 | }
216 |
217 | setCurrentUser(user) {
218 |
219 |
220 | // set temporary user avatar image url
221 | user.avatar = this.loadUserAvatar(user);
222 | this.user = user;
223 |
224 |
225 | if (user) {
226 | localStorage.setItem('me', JSON.stringify(user));
227 |
228 | // save this user to our users collections in local
229 | const userId = `${user._id}`;
230 | this.users = this.users.set(userId, user);
231 | }
232 |
233 | this.update();
234 |
235 | }
236 |
237 | clearCacheData(){
238 |
239 | this.channels = this.channels.clear();
240 | this.messages = this.messages.clear();
241 | this.users = this.users.clear();
242 | }
243 | signOut() {
244 |
245 | const userId = _.toString(_.get(this.user, '_id', null));
246 | const tokenId = _.get(this.token, '_id', null); //this.token._id;
247 | // request to backend and loggout this user
248 |
249 | const options = {
250 | headers: {
251 | authorization: tokenId,
252 | }
253 | };
254 |
255 | this.service.get('api/me/logout', options);
256 |
257 | this.user = null;
258 | localStorage.removeItem('me');
259 | localStorage.removeItem('token');
260 |
261 | this.clearCacheData();
262 |
263 | if (userId) {
264 | this.users = this.users.remove(userId);
265 | }
266 |
267 | this.update();
268 | }
269 |
270 | register(user){
271 |
272 | return new Promise((resolve, reject) => {
273 |
274 | this.service.post('api/users', user).then((response) => {
275 |
276 | console.log("use created", response.data);
277 |
278 | return resolve(response.data);
279 | }).catch(err => {
280 |
281 | return reject("An error create your account");
282 | })
283 |
284 |
285 | });
286 | }
287 | login(email = null, password = null) {
288 |
289 | const userEmail = _.toLower(email);
290 |
291 |
292 | const user = {
293 | email: userEmail,
294 | password: password,
295 | }
296 | //console.log("Ttrying to login with user info", user);
297 |
298 |
299 | return new Promise((resolve, reject) => {
300 |
301 |
302 | // we call to backend service and login with user data
303 |
304 | this.service.post('api/users/login', user).then((response) => {
305 |
306 | // that mean successful user logged in
307 |
308 | const accessToken = _.get(response, 'data');
309 | const user = _.get(accessToken, 'user');
310 |
311 | this.setCurrentUser(user);
312 | this.setUserToken(accessToken);
313 |
314 | // call to realtime and connect again to socket server with this user
315 |
316 | this.realtime.connect();
317 |
318 | // begin fetching user's channels
319 |
320 | this.fetchUserChannels();
321 |
322 | //console.log("Got user login callback from the server: ", accessToken);
323 |
324 |
325 | }).catch((err) => {
326 |
327 | console.log("Got an error login from server", err);
328 | // login error
329 |
330 | const message = _.get(err, 'response.data.error.message', "Login Error!");
331 |
332 | return reject(message);
333 | })
334 |
335 | });
336 |
337 |
338 | }
339 |
340 | removeMemberFromChannel(channel = null, user = null) {
341 |
342 | if (!channel || !user) {
343 | return;
344 | }
345 |
346 | const userId = _.get(user, '_id');
347 | const channelId = _.get(channel, '_id');
348 |
349 | channel.members = channel.members.remove(userId);
350 |
351 | this.channels = this.channels.set(channelId, channel);
352 |
353 | this.update();
354 |
355 | }
356 |
357 | addUserToChannel(channelId, userId) {
358 |
359 |
360 | const channel = this.channels.get(channelId);
361 |
362 | if (channel) {
363 |
364 | // now add this member id to channels members.
365 | channel.members = channel.members.set(userId, true);
366 | this.channels = this.channels.set(channelId, channel);
367 | this.update();
368 | }
369 |
370 | }
371 |
372 | getSearchUsers() {
373 |
374 | return this.search.users.valueSeq();
375 | }
376 |
377 | onCreateNewChannel(channel = {}) {
378 |
379 | const channelId = _.get(channel, '_id');
380 | this.addChannel(channelId, channel);
381 | this.setActiveChannelId(channelId);
382 |
383 | //console.log(JSON.stringify(this.channels.toJS()));
384 |
385 | }
386 |
387 | getCurrentUser() {
388 |
389 | return this.user;
390 | }
391 |
392 |
393 | fetchChannelMessages(channelId){
394 |
395 |
396 | let channel = this.channels.get(channelId);
397 |
398 | if (channel && !_.get(channel, 'isFetchedMessages')){
399 |
400 | const token = _.get(this.token, '_id');//this.token._id;
401 | const options = {
402 | headers: {
403 | authorization: token,
404 | }
405 | }
406 |
407 | this.service.get(`api/channels/${channelId}/messages`, options).then((response) => {
408 |
409 |
410 |
411 | channel.isFetchedMessages = true;
412 |
413 | const messages = response.data;
414 |
415 | _.each(messages, (message) => {
416 |
417 | this.realtime.onAddMessage(message);
418 |
419 | });
420 |
421 |
422 | this.channels = this.channels.set(channelId, channel);
423 |
424 |
425 |
426 |
427 | }).catch((err) => {
428 |
429 | console.log("An error fetching channel 's messages", err);
430 | })
431 |
432 |
433 | }
434 |
435 | }
436 | setActiveChannelId(id) {
437 |
438 | this.activeChannelId = id;
439 |
440 | this.fetchChannelMessages(id);
441 |
442 | this.update();
443 |
444 | }
445 |
446 |
447 | getActiveChannel() {
448 |
449 | const channel = this.activeChannelId ? this.channels.get(this.activeChannelId) : this.channels.first();
450 | return channel;
451 |
452 | }
453 |
454 | setMessage(message, notify = false) {
455 |
456 | const id = _.toString(_.get(message, '_id'));
457 | this.messages = this.messages.set(id, message);
458 | const channelId = _.toString(message.channelId);
459 | const channel = this.channels.get(channelId);
460 |
461 | if (channel) {
462 | channel.messages = channel.messages.set(id, true);
463 | channel.lastMessage = _.get(message, 'body', '');
464 | channel.notify = notify;
465 |
466 | this.channels = this.channels.set(channelId, channel);
467 | } else {
468 |
469 | // fetch to the server with channel info
470 | this.service.get(`api/channels/${channelId}`).then((response) => {
471 |
472 |
473 | const channel = _.get(response, 'data');
474 |
475 | /*const users = _.get(channel, 'users');
476 | _.each(users, (user) => {
477 |
478 | this.addUserToCache(user);
479 | });*/
480 |
481 | this.realtime.onAddChannel(channel);
482 |
483 |
484 | })
485 | }
486 | this.update();
487 | }
488 |
489 | addMessage(id, message = {}) {
490 |
491 | // we need add user object who is author of this message
492 |
493 |
494 | const user = this.getCurrentUser();
495 | message.user = user;
496 |
497 | this.messages = this.messages.set(id, message);
498 |
499 | // let's add new message id to current channel->messages.
500 |
501 | const channelId = _.get(message, 'channelId');
502 | if (channelId) {
503 |
504 | let channel = this.channels.get(channelId);
505 |
506 |
507 | channel.lastMessage = _.get(message, 'body', '');
508 |
509 | // now send this channel info to the server
510 | const obj = {
511 |
512 | action: 'create_channel',
513 | payload: channel,
514 | };
515 | this.realtime.send(obj);
516 |
517 |
518 | //console.log("channel:", channel);
519 |
520 | // send to the server via websocket to creawte new message and notify to other members.
521 |
522 | this.realtime.send(
523 | {
524 | action: 'create_message',
525 | payload: message,
526 | }
527 | );
528 |
529 | channel.messages = channel.messages.set(id, true);
530 |
531 |
532 | channel.isNew = false;
533 | this.channels = this.channels.set(channelId, channel);
534 |
535 |
536 | }
537 | this.update();
538 |
539 | // console.log(JSON.stringify(this.messages.toJS()));
540 |
541 | }
542 |
543 | getMessages() {
544 |
545 | return this.messages.valueSeq();
546 | }
547 |
548 | getMessagesFromChannel(channel) {
549 |
550 | let messages = new OrderedMap();
551 |
552 |
553 | if (channel) {
554 |
555 |
556 | channel.messages.forEach((value, key) => {
557 |
558 |
559 | const message = this.messages.get(key);
560 |
561 | messages = messages.set(key, message);
562 |
563 | });
564 |
565 | }
566 |
567 | return messages.valueSeq();
568 | }
569 |
570 | getMembersFromChannel(channel) {
571 |
572 | let members = new OrderedMap();
573 |
574 | if (channel) {
575 |
576 |
577 | channel.members.forEach((value, key) => {
578 |
579 |
580 | const userId = `${key}`;
581 | const user = this.users.get(userId);
582 |
583 | const loggedUser = this.getCurrentUser();
584 |
585 | if (_.get(loggedUser, '_id') !== _.get(user, '_id')) {
586 | members = members.set(key, user);
587 | }
588 |
589 |
590 | });
591 | }
592 |
593 | return members.valueSeq();
594 | }
595 |
596 | addChannel(index, channel = {}) {
597 | this.channels = this.channels.set(`${index}`, channel);
598 |
599 | this.update();
600 | }
601 |
602 | getChannels() {
603 |
604 | //return this.channels.valueSeq();
605 |
606 | // we need to sort channel by date , the last one will list on top.
607 |
608 |
609 | this.channels = this.channels.sort((a, b) => a.updated < b.updated);
610 |
611 | return this.channels.valueSeq();
612 | }
613 |
614 | update() {
615 |
616 | this.app.forceUpdate()
617 | }
618 | }
--------------------------------------------------------------------------------
/deployment-to-digitalocean-hosting.md:
--------------------------------------------------------------------------------
1 | # Deploy Reactjs, Nodejs Chat app to DigitalOcean hosting (Ubuntu VPS)
2 |
3 | ## Get DigitalOcean account
4 |
5 | I have been using DigitalOcean for me and setup for my customers, so I recommend to use it for your project as well. Just pick the VPS best suited for the size of your project, starting at 5$, 10$ or 20$. The price is very flexible. DO provides SSD cloud hosting at a good price - I don't think you can get same price on other providers with same quality.
6 | Besides, their support is very fast and they have good documentation, and a friendly UI for end-users.
7 | So lets get started by registering an account and deploy your app at Digitalocean.com
8 |
9 | ## Setup Ubuntu on DigitalOcean Cloud VPS.
10 |
11 | * In this tutorial I will use Ubuntu 16.04, and I also recommend this OS for your VPS. I recommend you choose a suitable VPN, depending on how much traffic you expect. I will start at 20$ a month, and upgrade if necessary.
12 | * Choose a data center region: DigitalOcean has many data centers, which means you can pick the data center near where you expect most of your visitors to live. For example, if I expect all the visitors to come from Vietnam, I will choose the data center closest to Vietnam, which is Singapore.
13 | * Select additional options if you want to have additional backup service, or private network.
14 | * Add your SSH keys: you can generate your SSH key on your computer and copy it to your VPS, which means when you login from SSH, you're not required to enter a username and password. This is more secure and will save you time. If you would like to know how to generate SSH key and use it on DigitalOcean hosting, I recommend this article
15 | * By default, you create one droplet at a time. If you want to, you can set up multiple droplets at once.
16 | * Name your droplet and click submit, just get a cup of coffee and wait a moment for DigitalOcean to set everything up for you. When you see "Happy coding!" your cloud VPS is ready for use.
17 | * Check your email that you registered with on DigitalOcean. You should receive an email notifying you about your VPS IP, root username and password.
18 | This is the format of the email.
19 | Droplet Name: [Name of your Droplet]
20 | IP Address: [your-VPS-IP]
21 | Username: root
22 | Password: [your-root-password-generated-by-robot]
23 | * Login to your Cloud via terminal by writing
24 | ```
25 | ssh root@YOUR-IP-ADDREESS
26 | ```
27 | Now, enter the root password given in the email. You will be asked for a new password the first time logging in.
28 | + The server will ask you for your password once more (the password given in the email).
29 | + Enter a new password
30 | + Confirm the password, and remember it for later
31 | + The setup is complete.
32 |
33 | ## Configuring the Firewall on your Cloud
34 |
35 | This is a very important step we need to do. We need to reconfigure our firewall software to allow access to the service
36 | * I recommend open port only for 80, 443, SSH (port 22), but it depends on your project, and it may need more ports open for any other services. In this project we will open port 80 for http access, 443 https (ssl), and port 22 (for SSH login). This will suffice.
37 | * By default Firewall is inactive, which you can check by running
38 | ``` sudo ufw status ```
39 |
40 | ```
41 | sudo ufw app list
42 | ```
43 | * So let us config the Firewall and allow those ports by
44 | ```
45 | sudo ufw allow 'Nginx Full'
46 | ```
47 |
48 | ```
49 | sudo ufw allow 'OpenSSH'
50 | ```
51 |
52 | ```
53 | sudo ufw enable
54 | ```
55 |
56 | ## Setup Nodejs on DigitalOcean Ubuntu 16.04
57 | We are using Nodejs for backend and we will serve the static files of the react application build. So Nodejs is required
58 | * Visit https://nodejs.org/en/download/package-manager/ to see the documentation
59 | * We use package management to install, here is command to install Node.js v9
60 |
61 | ```
62 | curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
63 | ```
64 | ```
65 | sudo apt-get install -y nodejs
66 | ```
67 | * After successfully installing Node.js, we can check the version by typing in the command ``` node -v ``` and you should see the current version (v9.3.0 at the time of this writing).
68 |
69 | ## Setup MongoDB v3.6 on DigitalOcean Ubuntu 16.04 Cloud VPS
70 |
71 | We are using MongoDB as a database, and so we can install it my following the documentation https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/
72 |
73 | * Import the public key used by the package management system
74 | ```
75 | sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5
76 | ```
77 | * Create a list file for MongoDB (Ubuntu 16.04)
78 | ```
79 | echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.6 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.6.list
80 | ```
81 | * Reload local package database
82 | ```
83 | sudo apt-get update
84 | ```
85 | * Install the latest stable version of MongoDB
86 | ```
87 | sudo apt-get install -y mongodb-org
88 | ```
89 | * Start MongoDB by running (default port: 27017)
90 | ```
91 | sudo service mongod start
92 | ```
93 | * Stop MongoDB by running
94 | ```
95 | sudo service mongod stop
96 | ```
97 | * Restart MongoDB by running
98 | ```
99 | sudo service mongod restart
100 | ```
101 |
102 | ## Install Nginx - Http Proxy Server
103 | Let me explain simply why we use Nginx for this Nodejs web application.
104 | When we run our chat app, it will run on port 3000, which is the default for running a Nodejs application. We can change the port to 3001, 3002 or 8080, and so on... However, if you point your domain to DigitalOcean cloud VPS, you can access your app throught the domain. For example, you can reach a Nodejs app on the VPS with a port 3000 by vising https://tabvn.com:3000.
105 | In order to set a nodejs web app on the default port of 80, which can be visited by simply going to http://tabvn.com/, we use Nginx.
106 |
107 | * To install Nginx, visit the official documentation at http://nginx.org/en/linux_packages.html
108 | * So we will run following command on Ubuntu cloud VPS 16.04
109 | ```
110 | apt-get update
111 | ```
112 |
113 | ```
114 | sudo apt-get install nginx
115 | ```
116 | * Start Nginx: open your IP-address, for example: http://123.456.789. You should see "Welcome to nginx!". All the Nginx configurations is in our cloud at the location /etc/nginx/nginx.conf
117 | ```
118 | nginx
119 | ```
120 | * Stop Nginx
121 | ```
122 | nginx -s stop
123 | ```
124 | * Reload Nginx
125 | ```
126 | nginx -s reload
127 | ```
128 | * Close your Cloud command line by ``` exit ``` or cloud command line tab in terminal
129 |
130 | ## Time to Deployment
131 |
132 | * Download the chat app project at https://github.com/tabvn/nodejs-reactjs-chatapp.
133 | ```
134 | git clone https://github.com/tabvn/nodejs-reactjs-chatapp.git chatApp
135 | ```
136 | ```
137 | cd chatApp
138 | ```
139 | ```
140 | cd server
141 | ```
142 | ```
143 | npm install
144 | ```
145 | ```
146 | cd ../app
147 | ```
148 | ```
149 | npm install
150 | ```
151 |
152 | * Fixed issue of bcrypt on Ubuntu 16.04
153 |
154 | ```
155 | sudo apt-get install build-essential
156 |
157 | ```
158 |
159 |
160 | ## Nginx config sample:
161 | Nginx Websocket document: http://nginx.org/en/docs/http/websocket.html
162 |
163 | ```
164 | server {
165 | listen 80;
166 | root /var/www/html;
167 | location / {
168 |
169 |
170 | proxy_pass http://127.0.0.1:3001;
171 | proxy_http_version 1.1;
172 | proxy_set_header Upgrade $http_upgrade;
173 | proxy_set_header Connection "upgrade";
174 | }
175 |
176 | }
177 | ```
178 | See Video: https://www.youtube.com/watch?v=wJsH45eWNBo
179 |
180 |
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:18.04
2 | #RUN apk add --update \ libc6-compat
3 | # Install a bunch of node modules that are commonly used.
4 | #ADD package.json /usr/app/
5 | RUN apt-get update && apt-get -qq -y install curl
6 |
7 |
8 | ENV NODE_VERSION=9.9.0
9 | RUN apt-get install -y curl
10 | RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
11 | ENV NVM_DIR=/root/.nvm
12 | RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
13 | RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
14 | RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
15 | ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}"
16 | RUN node --version
17 | RUN npm --version
18 | ADD . /usr/app/
19 |
20 | WORKDIR /usr/app
21 | #RUN npm install -g n
22 | #RUN n 8.4.0
23 |
24 |
25 | #RUN npm install uNetworking/uWebSockets.js#v15.11.0
26 | RUN npm install
27 | #RUN npm rebuild uws
28 |
--------------------------------------------------------------------------------
/server/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | server:
4 | build: .
5 | ports:
6 | - "3001:3001"
7 | depends_on:
8 | - mongo
9 | command: npm run dev
10 | mongo:
11 | image: mongo
12 | ports:
13 | - "27017:27017"
14 |
15 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chatapp",
3 | "version": "1.0.0",
4 | "description": "Use websocket in application.",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "nodemon -w src --exec \"babel-node src --presets env,stage-0\"",
8 | "build": "babel src -s -D -d dist --presets env,stage-0",
9 | "start": "node dist",
10 | "prestart": "npm run -s build",
11 | "test": "eslint src"
12 | },
13 | "eslintConfig": {
14 | "extends": "eslint:recommended",
15 | "parserOptions": {
16 | "ecmaVersion": 7,
17 | "sourceType": "module"
18 | },
19 | "env": {
20 | "node": true
21 | },
22 | "rules": {
23 | "no-console": 0,
24 | "no-unused-vars": 1
25 | }
26 | },
27 | "dependencies": {
28 | "bcrypt": "^2.0.0",
29 | "body-parser": "^1.18.2",
30 | "cors": "^2.8.4",
31 | "express": "^4.16.2",
32 | "immutable": "^3.8.2",
33 | "lodash": "^4.17.4",
34 | "moment": "^2.19.3",
35 | "mongodb": "^2.2.33",
36 | "morgan": "^1.9.0",
37 | "uws": "^9.14.0"
38 | },
39 | "devDependencies": {
40 | "babel-cli": "^6.26.0",
41 | "babel-core": "^6.26.0",
42 | "babel-preset-env": "^1.6.1",
43 | "babel-preset-stage-0": "^6.24.1",
44 | "eslint": "^4.9.0",
45 | "nodemon": "^1.12.1"
46 | },
47 | "author": "toan@tabvn.com",
48 | "license": "ISC"
49 | }
50 |
--------------------------------------------------------------------------------
/server/src/app-router.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import _ from 'lodash'
3 |
4 |
5 | export const START_TIME = new Date();
6 |
7 | export default class AppRouter {
8 |
9 |
10 | constructor(app) {
11 |
12 | this.app = app;
13 |
14 |
15 | this.setupRouter = this.setupRouter.bind(this);
16 |
17 |
18 | this.setupRouter();
19 | }
20 |
21 | setupRouter() {
22 |
23 | const app = this.app;
24 |
25 | console.log("APp ROuter works!");
26 |
27 |
28 |
29 | /**
30 | * @endpoint: /api/users
31 | * @method: POST
32 | **/
33 | app.post('/api/users', (req, res, next) => {
34 |
35 | const body = req.body;
36 |
37 | app.models.user.create(body).then((user) => {
38 |
39 | _.unset(user, 'password');
40 |
41 | return res.status(200).json(user);
42 |
43 | }).catch(err => {
44 |
45 |
46 | return res.status(503).json({error: err});
47 | })
48 |
49 |
50 | });
51 |
52 |
53 | /**
54 | * @endpoint: /api/users/me
55 | * @method: GET
56 | **/
57 |
58 | app.get('/api/users/me', (req, res, next) => {
59 |
60 | let tokenId = req.get('authorization');
61 |
62 | if (!tokenId) {
63 | // get token from query
64 |
65 | tokenId = _.get(req, 'query.auth');
66 | }
67 |
68 |
69 | app.models.token.loadTokenAndUser(tokenId).then((token) => {
70 | _.unset(token, 'user.password');
71 |
72 | return res.json(token);
73 |
74 | }).catch(err => {
75 |
76 | return res.status(401).json({
77 | error: err
78 | })
79 | });
80 |
81 |
82 | });
83 |
84 |
85 | /**
86 | * @endpoint: /api/users/search
87 | * @method: POST
88 | **/
89 |
90 | app.post('/api/users/search', (req, res, next) => {
91 |
92 |
93 | const keyword = _.get(req, 'body.search', '');
94 |
95 | app.models.user.search(keyword).then((results) => {
96 |
97 |
98 | return res.status(200).json(results);
99 | }).catch((err) => {
100 |
101 | return res.status(404).json({
102 | error: 'Not found.'
103 | })
104 | })
105 |
106 | });
107 |
108 |
109 | /**
110 | * @endpoint: /api/users/:id
111 | * @method: GET
112 | **/
113 |
114 | app.get('/api/users/:id', (req, res, next) => {
115 |
116 | const userId = _.get(req, 'params.id');
117 |
118 |
119 | app.models.user.load(userId).then((user) => {
120 |
121 | _.unset(user, 'password');
122 |
123 | return res.status(200).json(user);
124 | }).catch(err => {
125 |
126 | return res.status(404).json({
127 | error: err,
128 | })
129 | })
130 |
131 |
132 | });
133 |
134 |
135 | /**
136 | * @endpoint: /api/users/login
137 | * @method: POST
138 | **/
139 |
140 | app.post('/api/users/login', (req, res, next) => {
141 |
142 | const body = _.get(req, 'body');
143 |
144 |
145 | app.models.user.login(body).then((token) => {
146 |
147 |
148 | _.unset(token, 'user.password');
149 |
150 | return res.status(200).json(token);
151 |
152 |
153 | }).catch(err => {
154 |
155 | return res.status(401).json({
156 | error: err
157 | })
158 | })
159 |
160 | })
161 |
162 |
163 | /**
164 | * @endpoint: /api/channels/:id
165 | * @method: GET
166 | **/
167 |
168 |
169 | app.get('/api/channels/:id', (req, res, next) => {
170 |
171 | const channelId = _.get(req, 'params.id');
172 |
173 | console.log(channelId);
174 |
175 | if (!channelId) {
176 |
177 | return res.status(404).json({error: {message: "Not found."}});
178 | }
179 |
180 |
181 | app.models.channel.load(channelId).then((channel) => {
182 |
183 | // fetch all uses belong to memberId
184 |
185 | const members = channel.members;
186 | const query = {
187 | _id: {$in: members}
188 | };
189 | const options = {_id: 1, name: 1, created: 1};
190 |
191 | app.models.user.find(query, options).then((users) => {
192 | channel.users = users;
193 |
194 | return res.status(200).json(channel);
195 | }).catch(err => {
196 |
197 | return res.status(404).json({error: {message: "Not found."}});
198 |
199 | });
200 |
201 |
202 | }).catch((err) => {
203 |
204 | return res.status(404).json({error: {message: "Not found."}});
205 | })
206 |
207 |
208 | });
209 |
210 |
211 | /**
212 | * @endpoint: /api/channels/:id/messages
213 | * @method: GET
214 | **/
215 |
216 | app.get('/api/channels/:id/messages', (req, res, next) => {
217 |
218 |
219 | let tokenId = req.get('authorization');
220 |
221 | if (!tokenId) {
222 | // get token from query
223 |
224 | tokenId = _.get(req, 'query.auth');
225 | }
226 |
227 |
228 | app.models.token.loadTokenAndUser(tokenId).then((token) => {
229 |
230 |
231 | const userId = token.userId;
232 |
233 |
234 | // make sure user are logged in
235 | // check if this user is inside of channel members. other retun 401.
236 |
237 | let filter = _.get(req, 'query.filter', null);
238 | if (filter) {
239 |
240 | filter = JSON.parse(filter);
241 | console.log(filter);
242 | }
243 |
244 | const channelId = _.toString(_.get(req, 'params.id'));
245 | const limit = _.get(filter, 'limit', 50);
246 | const offset = _.get(filter, 'offset', 0);
247 |
248 |
249 | // load channel
250 |
251 | this.app.models.channel.load(channelId).then((c) => {
252 |
253 |
254 | const memberIds = _.get(c, 'members');
255 |
256 | const members = [];
257 |
258 | _.each(memberIds, (id) => {
259 | members.push(_.toString(id));
260 | })
261 |
262 |
263 | if (!_.includes(members, _.toString(userId))) {
264 |
265 | return res.status(401).json({error: {message: "Access denied"}});
266 | }
267 |
268 | this.app.models.message.getChannelMessages(channelId, limit, offset).then((messages) => {
269 |
270 |
271 | return res.status(200).json(messages);
272 |
273 | }).catch((err) => {
274 |
275 | return res.status(404).json({error: {message: "Not found."}});
276 | })
277 |
278 |
279 | }).catch((err) => {
280 |
281 | return res.status(404).json({error: {message: "Not found."}});
282 |
283 | })
284 |
285 |
286 | }).catch((err) => {
287 |
288 |
289 | return res.status(401).json({error: {message: "Access denied"}});
290 |
291 |
292 | });
293 |
294 |
295 | });
296 |
297 |
298 | /**
299 | * @endpoint: /api/me/channels
300 | * @method: GET
301 | **/
302 |
303 | app.get('/api/me/channels', (req, res, next) => {
304 |
305 |
306 | let tokenId = req.get('authorization');
307 |
308 | if (!tokenId) {
309 | // get token from query
310 |
311 | tokenId = _.get(req, 'query.auth');
312 | }
313 |
314 |
315 | app.models.token.loadTokenAndUser(tokenId).then((token) => {
316 |
317 |
318 | const userId = token.userId;
319 |
320 |
321 | const query = [
322 |
323 | {
324 | $lookup: {
325 | from: 'users',
326 | localField: 'members',
327 | foreignField: '_id',
328 | as: 'users',
329 | }
330 | },
331 | {
332 | $match: {
333 | members: {$all: [userId]}
334 | }
335 | },
336 | {
337 | $project: {
338 | _id: true,
339 | title: true,
340 | lastMessage: true,
341 | created: true,
342 | updated: true,
343 | userId: true,
344 | users: {
345 | _id: true,
346 | name: true,
347 | created: true,
348 | online: true
349 | },
350 | members: true,
351 | }
352 | },
353 | {
354 | $sort: {updated: -1, created: -1}
355 | },
356 | {
357 | $limit: 50,
358 | }
359 | ];
360 |
361 | app.models.channel.aggregate(query).then((channels) => {
362 |
363 |
364 | return res.status(200).json(channels);
365 |
366 |
367 | }).catch((err) => {
368 |
369 | return res.status(404).json({error: {message: "Not found."}});
370 | })
371 |
372 |
373 | }).catch(err => {
374 |
375 | return res.status(401).json({
376 | error: "Access denied."
377 | })
378 | });
379 |
380 |
381 | });
382 |
383 |
384 |
385 |
386 | /**
387 | * @endpoint: /api/me/logout
388 | * @method: GET
389 | **/
390 |
391 | app.get('/api/me/logout', (req, res, next) => {
392 |
393 | let tokenId = req.get('authorization');
394 |
395 | if (!tokenId) {
396 | // get token from query
397 |
398 | tokenId = _.get(req, 'query.auth');
399 | }
400 |
401 |
402 | app.models.token.loadTokenAndUser(tokenId).then((token) => {
403 |
404 |
405 | app.models.token.logout(token);
406 |
407 | return res.status(200).json({
408 | message: 'Successful.'
409 | });
410 |
411 | }).catch(err => {
412 |
413 |
414 | return res.status(401).json({error: {message: 'Access denied'}});
415 | })
416 |
417 |
418 |
419 | })
420 |
421 |
422 | }
423 | }
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
--------------------------------------------------------------------------------
/server/src/database.js:
--------------------------------------------------------------------------------
1 | import {MongoClient} from 'mongodb'
2 |
3 | const URL = 'mongodb://mongo:27017';
4 |
5 |
6 | export default class Database{
7 |
8 | connect(){
9 |
10 |
11 | return new Promise((resolve, reject) => {
12 |
13 | MongoClient.connect(URL, (err, db) => {
14 |
15 | return err ? reject(err) : resolve(db);
16 |
17 | });
18 |
19 |
20 | });
21 |
22 |
23 |
24 | }
25 | }
--------------------------------------------------------------------------------
/server/src/helper.js:
--------------------------------------------------------------------------------
1 | export const isEmail = (emaill) => {
2 |
3 |
4 | const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
5 |
6 | return regex.test(emaill);
7 | }
8 |
9 | export const toString = (id = "") => {
10 |
11 |
12 | return `${id}`;
13 |
14 |
15 | }
--------------------------------------------------------------------------------
/server/src/index.js:
--------------------------------------------------------------------------------
1 | import http from 'http';
2 | import express from 'express';
3 | import cors from 'cors';
4 | import bodyParser from 'body-parser';
5 | import {version} from '../package.json'
6 | import WebSocketServer, {Server} from 'uws';
7 | import AppRouter from './app-router'
8 | import Model from './models'
9 | import Database from './database'
10 | import path from 'path'
11 |
12 | const PORT = 3001;
13 | const app = express();
14 | app.server = http.createServer(app);
15 |
16 |
17 | //app.use(morgan('dev'));
18 |
19 |
20 | app.use(cors({
21 | exposedHeaders: "*"
22 | }));
23 |
24 | app.use(bodyParser.json({
25 | limit: '50mb'
26 | }));
27 |
28 |
29 |
30 | app.wss = new Server({
31 | server: app.server
32 | });
33 |
34 |
35 | // static www files use express
36 | const wwwPath = path.join(__dirname, 'www');
37 |
38 | app.use('/', express.static(wwwPath));
39 |
40 | // Connect to Mongo Database
41 |
42 | new Database().connect().then((db) => {
43 |
44 | console.log("Successful connected to database.")
45 |
46 | app.db = db;
47 |
48 | }).catch((err) => {
49 |
50 |
51 | throw(err);
52 | });
53 |
54 |
55 | // End connect to Mongodb Database
56 |
57 | app.models = new Model(app);
58 | app.routers = new AppRouter(app);
59 |
60 |
61 |
62 |
63 |
64 | app.server.listen(process.env.PORT || PORT, () => {
65 | console.log(`App is running on port ${app.server.address().port}`);
66 | });
67 |
68 | export default app;
--------------------------------------------------------------------------------
/server/src/models/channel.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import {toString} from '../helper'
3 | import {ObjectID} from 'mongodb'
4 | import {OrderedMap} from 'immutable'
5 |
6 | export default class Channel {
7 |
8 | constructor(app) {
9 |
10 | this.app = app;
11 |
12 | this.channels = new OrderedMap();
13 | }
14 |
15 |
16 | aggregate(q){
17 |
18 | return new Promise((resolve, reject) => {
19 |
20 |
21 | this.app.db.collection('channels').aggregate(q, (err, results) => {
22 |
23 |
24 | return err ? reject(err) : resolve(results);
25 |
26 | });
27 |
28 |
29 | })
30 |
31 | }
32 | find(q, options = {}){
33 |
34 |
35 |
36 | return new Promise((resolve, reject) => {
37 |
38 |
39 | this.app.db.collection('channels').find(q, options).toArray((err, results) => {
40 |
41 |
42 | return err ? reject(err) : resolve(results);
43 | });
44 |
45 |
46 | });
47 | }
48 | load(id) {
49 |
50 | return new Promise((resolve, reject) => {
51 |
52 |
53 | id = _.toString(id);
54 |
55 |
56 | // first find in cache
57 | const channelFromCache = this.channels.get(id);
58 |
59 | if (channelFromCache) {
60 |
61 | return resolve(channelFromCache);
62 | }
63 |
64 |
65 | // let find in db
66 |
67 | this.findById(id).then((c) => {
68 |
69 | this.channels = this.channels.set(id, c);
70 |
71 | return resolve(c);
72 |
73 | }).catch((err) => {
74 |
75 |
76 | return reject(err);
77 | })
78 |
79 |
80 |
81 | })
82 |
83 | }
84 |
85 | findById(id){
86 |
87 | return new Promise((resolve, reject) => {
88 |
89 |
90 | this.app.db.collection('channels').findOne({_id: new ObjectID(id)}, (err, result) => {
91 |
92 | if(err || !result){
93 |
94 | return reject(err ? err : "Not found");
95 | }
96 |
97 | return resolve(result);
98 |
99 | });
100 |
101 |
102 | })
103 | }
104 | create(obj) {
105 |
106 |
107 | return new Promise((resolve, reject) => {
108 |
109 | let id = toString(_.get(obj, '_id'));
110 |
111 |
112 | let idObject = id ? new ObjectID(id) : new ObjectID();
113 |
114 |
115 | let members = [];
116 |
117 | _.each(_.get(obj, 'members', []), (value, key) => {
118 |
119 |
120 | const memberObjectId = new ObjectID(key);
121 | members.push(memberObjectId);
122 | });
123 |
124 |
125 | let userIdObject = null;
126 |
127 | let userId = _.get(obj, 'userId', null);
128 | if (userId) {
129 | userIdObject = new ObjectID(userId);
130 | }
131 |
132 |
133 | const channel = {
134 |
135 | _id: idObject,
136 | title: _.get(obj, 'title', ''),
137 | lastMessage: _.get(obj, 'lastMessage', ''),
138 | created: new Date(),
139 | userId: userIdObject,
140 | members: members,
141 | }
142 |
143 |
144 | this.app.db.collection('channels').insertOne(channel, (err, info) => {
145 |
146 | if (!err) {
147 |
148 | const channelId = channel._id.toString();
149 |
150 | this.channels = this.channels.set(channelId, channel);
151 | }
152 | return err ? reject(err) : resolve(channel);
153 | });
154 |
155 |
156 | });
157 |
158 |
159 | }
160 | }
--------------------------------------------------------------------------------
/server/src/models/connection.js:
--------------------------------------------------------------------------------
1 | import {OrderedMap} from 'immutable'
2 | import {ObjectID} from 'mongodb'
3 | import _ from 'lodash'
4 |
5 | export default class Connection {
6 |
7 | constructor(app) {
8 |
9 | this.app = app;
10 |
11 | this.connections = OrderedMap();
12 |
13 | this.modelDidLoad();
14 | }
15 |
16 |
17 | decodeMesasge(msg) {
18 |
19 |
20 | let messageObject = null;
21 |
22 |
23 | try {
24 |
25 | messageObject = JSON.parse(msg);
26 | }
27 | catch (err) {
28 |
29 | console.log("An error decode the socket mesage", msg);
30 | }
31 |
32 |
33 | return messageObject;
34 |
35 | }
36 |
37 | sendToMembers(userId, obj) {
38 |
39 | const query = [
40 | {
41 | $match: {
42 |
43 | members: {$all: [new ObjectID(userId)]}
44 | }
45 | },
46 | {
47 |
48 | $lookup: {
49 |
50 | from: 'users',
51 | localField: 'members',
52 | foreignField: '_id',
53 | as: 'users'
54 | }
55 | },
56 | {
57 | $unwind: {
58 |
59 | path: '$users'
60 | }
61 | },
62 | {
63 | $match: {'users.online': {$eq: true}}
64 | },
65 | {
66 | $group: {
67 |
68 | _id: "$users._id"
69 | }
70 | }
71 |
72 |
73 | ];
74 |
75 |
76 | const users = [];
77 |
78 |
79 | this.app.db.collection('channels').aggregate(query, (err, results) => {
80 |
81 |
82 | // console.log("found members array who is chattting with current user", results);
83 | if (err === null && results) {
84 |
85 | _.each(results, (result) => {
86 |
87 |
88 | const uid = _.toString(_.get(result, '_id'));
89 | if (uid) {
90 | users.push(uid);
91 | }
92 | });
93 |
94 |
95 | // this is list of all connections is chatting with current user
96 | const memberConnections = this.connections.filter((con) => _.includes(users, _.toString(_.get(con, 'userId'))));
97 | if (memberConnections.size) {
98 |
99 | memberConnections.forEach((connection, key) => {
100 |
101 | const ws = connection.ws;
102 | this.send(ws, obj);
103 | });
104 | }
105 |
106 |
107 | }
108 | })
109 | }
110 |
111 | sendAll(obj) {
112 |
113 |
114 | // send socket messages to all clients.
115 |
116 | this.connections.forEach((con, key) => {
117 | const ws = con.ws;
118 |
119 | this.send(ws, obj);
120 | });
121 | }
122 |
123 | send(ws, obj) {
124 |
125 | const message = JSON.stringify(obj);
126 |
127 | ws.send(message);
128 | }
129 |
130 | doTheJob(socketId, msg) {
131 |
132 |
133 | const action = _.get(msg, 'action');
134 | const payload = _.get(msg, 'payload');
135 | const userConnection = this.connections.get(socketId);
136 |
137 | switch (action) {
138 |
139 |
140 | case 'create_message':
141 |
142 | if (userConnection.isAuthenticated) {
143 | let messageObject = payload;
144 |
145 | messageObject.userId = _.get(userConnection, 'userId');
146 | //console.log("Got message from client about creating new message", payload);
147 |
148 | this.app.models.message.create(messageObject).then((message) => {
149 |
150 |
151 | // console.log("Mesage crewated", message);
152 |
153 | const channelId = _.toString(_.get(message, 'channelId'));
154 | this.app.models.channel.load(channelId).then((channel) => {
155 |
156 | // console.log("got channel of the message created", channel);
157 |
158 | const memberIds = _.get(channel, 'members', []);
159 |
160 | _.each(memberIds, (memberId) => {
161 |
162 | memberId = _.toString(memberId);
163 |
164 | const memberConnections = this.connections.filter((c) => _.toString(c.userId) === memberId);
165 |
166 |
167 | memberConnections.forEach((connection) => {
168 |
169 |
170 | const ws = connection.ws;
171 |
172 | this.send(ws, {
173 |
174 | action: 'message_added',
175 | payload: message,
176 | })
177 |
178 |
179 | })
180 |
181 |
182 | });
183 | })
184 |
185 | // message created successful.
186 |
187 |
188 | }).catch(err => {
189 |
190 |
191 | // send back to the socket client who sent this messagse with error
192 | const ws = userConnection.ws;
193 | this.send(ws, {
194 | action: 'create_message_error',
195 | payload: payload,
196 | })
197 | })
198 | }
199 |
200 |
201 | break;
202 | case 'create_channel':
203 |
204 | let channel = payload;
205 |
206 |
207 | const userId = userConnection.userId;
208 | channel.userId = userId;
209 |
210 | this.app.models.channel.create(channel).then((chanelObject) => {
211 |
212 | // successful created channel ,
213 |
214 | //console.log("Succesful created new channel", typeof userId, chanelObject);
215 |
216 | // let send back to all members in this channel with new channel created
217 | let memberConnections = [];
218 |
219 | const memberIds = _.get(chanelObject, 'members', []);
220 |
221 | // fetch all users has memberId
222 |
223 | const query = {
224 | _id: {$in: memberIds}
225 | };
226 |
227 | const queryOptions = {
228 | _id: 1,
229 | name: 1,
230 | created: 1,
231 | }
232 |
233 | this.app.models.user.find(query, queryOptions).then((users) => {
234 | chanelObject.users = users;
235 |
236 |
237 | _.each(memberIds, (id) => {
238 |
239 | const userId = id.toString();
240 | const memberConnection = this.connections.filter((con) => `${con.userId}` === userId);
241 |
242 | if (memberConnection.size) {
243 | memberConnection.forEach((con) => {
244 |
245 | const ws = con.ws;
246 | const obj = {
247 | action: 'channel_added',
248 | payload: chanelObject,
249 | }
250 |
251 | // send to socket client matching userId in channel members.
252 | this.send(ws, obj);
253 |
254 | })
255 |
256 |
257 | }
258 |
259 |
260 | });
261 |
262 | });
263 |
264 |
265 | //const memberConnections = this.connections.filter((con) => `${con.userId}` = )
266 |
267 |
268 | });
269 |
270 | //console.log("Got new channel need to be created form client", channel);
271 |
272 |
273 | break;
274 |
275 | case 'auth':
276 |
277 | const userTokenId = payload;
278 | let connection = this.connections.get(socketId);
279 |
280 | if (connection) {
281 |
282 |
283 |
284 | // let find user with this token and verify it.
285 |
286 | this.app.models.token.loadTokenAndUser(userTokenId).then((token) => {
287 |
288 | const userId = token.userId;
289 |
290 | connection.isAuthenticated = true;
291 | connection.userId = `${userId}`;
292 |
293 | this.connections = this.connections.set(socketId, connection);
294 |
295 | // now send back to the client you are verified.
296 | const obj = {
297 | action: 'auth_success',
298 | payload: 'You are verified',
299 | }
300 | this.send(connection.ws, obj);
301 |
302 | //send to all socket clients connection
303 |
304 | const userIdString = _.toString(userId);
305 | this.sendToMembers(userIdString, {
306 | action: 'user_online',
307 | payload: userIdString,
308 | });
309 |
310 | this.app.models.user.updateUserStatus(userIdString, true);
311 |
312 |
313 | }).catch((err) => {
314 |
315 |
316 | // send back to socket client you are not logged.
317 | const obj = {
318 | action: 'auth_error',
319 | payload: "An error authentication your account: " + userTokenId
320 | };
321 |
322 | this.send(connection.ws, obj);
323 |
324 | })
325 |
326 |
327 | }
328 |
329 |
330 | break;
331 |
332 | default:
333 |
334 | break;
335 | }
336 | }
337 |
338 | modelDidLoad() {
339 |
340 | this.app.wss.on('connection', (ws) => {
341 |
342 | const socketId = new ObjectID().toString();
343 |
344 | //console.log("Somone connected to the server via socket.", socketId)
345 |
346 | const clientConnection = {
347 | _id: `${socketId}`,
348 | ws: ws,
349 | userId: null,
350 | isAuthenticated: false,
351 | }
352 |
353 | // save this connection client to cache.
354 | this.connections = this.connections.set(socketId, clientConnection);
355 |
356 |
357 | // listen any message from websocket client.
358 |
359 | ws.on('message', (msg) => {
360 |
361 | //console.log("SERVER: message from a client", msg);
362 |
363 | const message = this.decodeMesasge(msg);
364 | this.doTheJob(socketId, message);
365 |
366 | //console.log("SERVER: message from a client", msg);
367 |
368 | });
369 |
370 |
371 | ws.on('close', () => {
372 |
373 | //console.log("Someone disconnected to the server", socketId);
374 |
375 |
376 | const closeConnection = this.connections.get(socketId);
377 | const userId = _.toString(_.get(closeConnection, 'userId', null));
378 |
379 | // let remove this socket client from the cache collection.
380 | this.connections = this.connections.remove(socketId);
381 |
382 | if (userId) {
383 | // now find all socket clients matching with userId
384 |
385 | const userConnections = this.connections.filter((con) => _.toString(_.get(con, 'userId')) === userId);
386 | if (userConnections.size === 0) {
387 |
388 | // this mean no more socket clients is online with this userId. now user is offline.
389 |
390 | this.sendToMembers(userId, {
391 | action: 'user_offline',
392 | payload: userId
393 | });
394 |
395 | // update user status into database
396 |
397 | this.app.models.user.updateUserStatus(userId, false);
398 | }
399 | }
400 |
401 |
402 | });
403 | });
404 | }
405 | }
--------------------------------------------------------------------------------
/server/src/models/index.js:
--------------------------------------------------------------------------------
1 | import User from './user'
2 | import Token from './token'
3 | import Connection from './connection'
4 | import Channel from './channel'
5 | import Message from "./message";
6 |
7 | export default class Model{
8 |
9 | constructor(app){
10 |
11 | this.app = app;
12 |
13 | this.user = new User(app);
14 | this.token = new Token(app);
15 | this.channel = new Channel(app);
16 | this.message = new Message(app);
17 | this.connection = new Connection(app);
18 |
19 | }
20 | }
--------------------------------------------------------------------------------
/server/src/models/message.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import {OrderedMap} from 'immutable'
3 | import {ObjectID} from 'mongodb'
4 |
5 | export default class Message {
6 |
7 | constructor(app) {
8 | this.app = app;
9 | this.messages = new OrderedMap();
10 | }
11 |
12 | getChannelMessages(channelId, limit = 50, offset = 0){
13 |
14 | return new Promise((resolve, reject) => {
15 |
16 | channelId = new ObjectID(channelId);
17 |
18 | const query = [
19 | {
20 |
21 | $lookup: {
22 | from: 'users',
23 | localField: 'userId',
24 | foreignField: '_id',
25 | as: 'user'
26 | }
27 | },
28 | {
29 | $match: {
30 | 'channelId': {$eq: channelId},
31 | },
32 | },
33 | {
34 |
35 | $project: {
36 | _id: true,
37 | channelId: true,
38 | user: {$arrayElemAt: ['$user', 0]},
39 | userId: true,
40 | body: true,
41 | created: true,
42 | }
43 | },
44 | {
45 | $project: {
46 | _id: true,
47 | channelId: true,
48 | user: {_id: true, name: true, created: true, online: true},
49 | userId: true,
50 | body: true,
51 | created: true,
52 | }
53 | },
54 | {
55 | $limit: limit
56 | },
57 | {
58 | $skip: offset,
59 | },
60 | {
61 | $sort: {created: -1}
62 | }
63 |
64 | ];
65 |
66 |
67 | this.app.db.collection('messages').aggregate(query, (err, results) => {
68 |
69 |
70 |
71 | return err ? reject(err): resolve(results)
72 |
73 | });
74 |
75 |
76 | })
77 | }
78 | create(obj) {
79 |
80 |
81 | return new Promise((resolve, reject) => {
82 |
83 |
84 | let id = _.get(obj, '_id', null);
85 | id = _.toString(id);
86 |
87 | const userId = new ObjectID(_.get(obj, 'userId'));
88 | const channelId = new ObjectID(_.get(obj, 'channelId'));
89 |
90 | const message = {
91 | _id: new ObjectID(id),
92 | body: _.get(obj, 'body', ''),
93 | userId: userId,
94 | channelId: channelId,
95 | created: new Date(),
96 | };
97 |
98 |
99 | this.app.db.collection('messages').insertOne(message, (err, info) => {
100 |
101 | if(err){
102 | return reject(err);
103 | }
104 |
105 |
106 | // let update lastMessgage field to channel
107 | this.app.db.collection('channels').findOneAndUpdate({_id: channelId}, {
108 | $set: {
109 | lastMessage: _.get(message, 'body', ''),
110 | updated: new Date(),
111 | }
112 | })
113 |
114 | this.app.models.user.load(_.toString(userId)).then((user) => {
115 |
116 | _.unset(user, 'password');
117 | _.unset(user, 'email');
118 | message.user = user;
119 |
120 |
121 | return resolve(message);
122 |
123 | }).catch((err) => {
124 |
125 | return reject(err);
126 | });
127 | });
128 |
129 |
130 | });
131 | }
132 |
133 | }
--------------------------------------------------------------------------------
/server/src/models/token.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import {ObjectID} from 'mongodb'
3 | import {OrderedMap} from 'immutable'
4 |
5 | export default class Token{
6 |
7 | constructor(app){
8 |
9 | this.app = app;
10 |
11 |
12 | this.tokens = new OrderedMap();
13 |
14 | }
15 |
16 | logout(token){
17 |
18 | return new Promise((resolve, reject) => {
19 |
20 | const tokenId = _.toString(token._id);
21 | // to remove token from cache
22 | this.tokens = this.tokens.remove(tokenId);
23 | // we have to delete this token id from tokens collection
24 |
25 | this.app.db.collection('tokens').remove({_id: new ObjectID(tokenId)}, (err, info) => {
26 |
27 | return err ? reject(err) : resolve(info);
28 | });
29 |
30 | })
31 |
32 | }
33 | loadTokenAndUser(id){
34 |
35 | return new Promise((resolve, reject) => {
36 |
37 | this.load(id).then((token) => {
38 |
39 |
40 | const userId = `${token.userId}`
41 |
42 | this.app.models.user.load(userId).then((user) => {
43 |
44 | token.user = user;
45 | return resolve(token);
46 |
47 | }).catch(err => {
48 |
49 | return reject(err);
50 |
51 | });
52 |
53 |
54 | }).catch((err) => {
55 | return reject(err);
56 | });
57 |
58 |
59 | })
60 | }
61 |
62 | load(id = null){
63 |
64 |
65 | id = `${id}`;
66 |
67 |
68 |
69 |
70 | return new Promise((resolve, reject) => {
71 |
72 |
73 | // first we check in cache if found dont need to query to database.
74 |
75 | const tokenFromCache = this.tokens.get(id);
76 | if(tokenFromCache){
77 |
78 | return resolve(tokenFromCache);
79 | }
80 |
81 | this.findTokenById(id, (err, token) => {
82 |
83 | if(!err && token){
84 |
85 | const tokenId = token._id.toString();
86 |
87 | this.tokens = this.tokens.set(tokenId, token);
88 |
89 | }
90 | return err ? reject(err) : resolve(token);
91 |
92 | });
93 | })
94 | }
95 |
96 | findTokenById(id, cb = () => {}){
97 |
98 |
99 | //console.log("Begin query into database!!!!!!");
100 |
101 |
102 | const idObject = new ObjectID(id);
103 |
104 | const query = {_id: idObject}
105 | this.app.db.collection('tokens').findOne(query, (err, result) => {
106 |
107 | if(err || !result){
108 |
109 | return cb({message: "Not found"}, null);
110 | }
111 |
112 |
113 | return cb(null, result);
114 |
115 | })
116 | }
117 |
118 | create(userId){
119 |
120 | const token = {
121 | userId: userId,
122 | created: new Date(),
123 | }
124 |
125 |
126 | return new Promise((resolve, reject) => {
127 |
128 |
129 | this.app.db.collection('tokens').insertOne(token, (err, info) => {
130 | return err ? reject(err) : resolve(token);
131 | })
132 |
133 |
134 |
135 | })
136 | }
137 |
138 |
139 |
140 | }
--------------------------------------------------------------------------------
/server/src/models/user.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import {isEmail} from '../helper'
3 | import bcrypt from 'bcrypt'
4 | import {ObjectID} from 'mongodb'
5 | import {OrderedMap} from 'immutable'
6 |
7 | const saltRound = 10;
8 |
9 | export default class User {
10 |
11 | constructor(app) {
12 |
13 | this.app = app;
14 |
15 | this.users = new OrderedMap();
16 |
17 | }
18 |
19 | updateUserStatus(userId, isOnline = false) {
20 |
21 | return new Promise((resolve, reject) => {
22 |
23 | // first update status of cache this.users
24 |
25 |
26 | this.users = this.users.update(userId, (user) => {
27 |
28 | if (user) {
29 | user.online = isOnline;
30 | }
31 |
32 | return user;
33 | });
34 |
35 | const query = {_id: new ObjectID(userId)};
36 | const updater = {$set: {online: isOnline}};
37 | this.app.db.collection('users').update(query, updater, (err, info) => {
38 | return err ? reject(err) : resolve(info);
39 | });
40 |
41 |
42 | })
43 | }
44 |
45 | find(query = {}, options = {}) {
46 |
47 |
48 | return new Promise((resolve, reject) => {
49 |
50 | this.app.db.collection('users').find(query, options).toArray((err, users) => {
51 |
52 | return err ? reject(err) : resolve(users);
53 | })
54 |
55 | });
56 | }
57 |
58 | search(q = "") {
59 |
60 | return new Promise((resolve, reject) => {
61 |
62 |
63 | const regex = new RegExp(q, 'i');
64 |
65 | const query = {
66 | $or: [
67 | {name: {$regex: regex}},
68 | {email: {$regex: regex}},
69 | ],
70 | };
71 |
72 | this.app.db.collection('users').find(query, {
73 | _id: true,
74 | name: true,
75 | created: true
76 | }).toArray((err, results) => {
77 |
78 |
79 | if (err || !results || !results.length) {
80 |
81 | return reject({message: "User not found."})
82 | }
83 |
84 | return resolve(results);
85 | });
86 |
87 |
88 | });
89 | }
90 |
91 | login(user) {
92 |
93 | const email = _.get(user, 'email', '');
94 | const password = _.get(user, 'password', '');
95 |
96 |
97 | return new Promise((resolve, reject) => {
98 |
99 |
100 | if (!password || !email || !isEmail(email)) {
101 | return reject({message: "An error login."})
102 | }
103 |
104 |
105 | // find in database with email
106 |
107 | this.findUserByEmail(email, (err, result) => {
108 |
109 |
110 | if (err) {
111 |
112 | return reject({message: "Login Error."});
113 | }
114 |
115 |
116 | // if found user we have to compare the password hash and plain text.
117 |
118 |
119 | const hashPassword = _.get(result, 'password');
120 |
121 | const isMatch = bcrypt.compareSync(password, hashPassword);
122 |
123 |
124 | if (!isMatch) {
125 |
126 | return reject({message: "Login Error."});
127 | }
128 |
129 | // user login successful let creat new token save to token collection.
130 |
131 | const userId = result._id;
132 |
133 | this.app.models.token.create(userId).then((token) => {
134 |
135 | token.user = result;
136 |
137 | return resolve(token);
138 |
139 | }).catch(err => {
140 |
141 | return reject({message: "Login error"});
142 | })
143 |
144 |
145 | });
146 |
147 |
148 | })
149 |
150 |
151 | }
152 |
153 | findUserByEmail(email, callback = () => {
154 | }) {
155 |
156 |
157 | this.app.db.collection('users').findOne({email: email}, (err, result) => {
158 |
159 | if (err || !result) {
160 |
161 | return callback({message: "User not found."})
162 | }
163 |
164 | return callback(null, result);
165 |
166 | });
167 |
168 |
169 | }
170 |
171 | load(id) {
172 |
173 |
174 | id = `${id}`;
175 |
176 | return new Promise((resolve, reject) => {
177 |
178 | // find in cache if found we return and dont nee to query db
179 |
180 | const userInCache = this.users.get(id);
181 |
182 |
183 | if (userInCache) {
184 | return resolve(userInCache);
185 | }
186 |
187 | // if not found then we start query db
188 | this.findUserById(id, (err, user) => {
189 |
190 | if (!err && user) {
191 |
192 |
193 | this.users = this.users.set(id, user);
194 | }
195 |
196 | return err ? reject(err) : resolve(user);
197 |
198 | })
199 |
200 |
201 | })
202 | }
203 |
204 | findUserById(id, callback = () => {
205 | }) {
206 |
207 | //console.log("Begin query in database");
208 |
209 | if (!id) {
210 | return callback({message: "User not found"}, null);
211 | }
212 |
213 |
214 | const userId = new ObjectID(id);
215 |
216 | this.app.db.collection('users').findOne({_id: userId}, (err, result) => {
217 |
218 |
219 | if (err || !result) {
220 |
221 | return callback({message: "User not found"});
222 | }
223 | return callback(null, result);
224 |
225 | });
226 | }
227 |
228 | beforeSave(user, callback = () => {
229 | }) {
230 |
231 |
232 | // first is validate user object before save to user collection.
233 |
234 | let errors = [];
235 |
236 |
237 | const fields = ['name', 'email', 'password'];
238 | const validations = {
239 | name: {
240 | errorMesage: 'Name is required',
241 | do: () => {
242 |
243 | const name = _.get(user, 'name', '');
244 |
245 | return name.length;
246 | }
247 | },
248 | email: {
249 | errorMesage: 'Email is not correct',
250 | do: () => {
251 |
252 | const email = _.get(user, 'email', '');
253 |
254 | if (!email.length || !isEmail(email)) {
255 | return false;
256 | }
257 |
258 |
259 | return true;
260 | }
261 | },
262 | password: {
263 | errorMesage: 'Password is required and more than 3 characters',
264 | do: () => {
265 | const password = _.get(user, 'password', '');
266 |
267 | if (!password.length || password.length < 3) {
268 |
269 | return false;
270 | }
271 |
272 | return true;
273 | }
274 | }
275 | }
276 |
277 |
278 | // loop all fields to check if valid or not.
279 | fields.forEach((field) => {
280 |
281 |
282 | const fieldValidation = _.get(validations, field);
283 |
284 | if (fieldValidation) {
285 |
286 | // do check/
287 |
288 | const isValid = fieldValidation.do();
289 | const msg = fieldValidation.errorMesage;
290 |
291 | if (!isValid) {
292 | errors.push(msg);
293 | }
294 | }
295 |
296 |
297 | });
298 |
299 | if (errors.length) {
300 |
301 | // this is not pass of the validation.
302 | const err = _.join(errors, ',');
303 | return callback(err, null);
304 | }
305 |
306 | // check email is exist in db or not
307 | const email = _.toLower(_.trim(_.get(user, 'email', '')));
308 |
309 | this.app.db.collection('users').findOne({email: email}, (err, result) => {
310 |
311 | if (err || result) {
312 | return callback({message: "Email is already exist"}, null);
313 | }
314 |
315 |
316 | // return callback with succes checked.
317 | const password = _.get(user, 'password');
318 | const hashPassword = bcrypt.hashSync(password, saltRound);
319 |
320 | const userFormatted = {
321 | name: `${_.trim(_.get(user, 'name'))}`,
322 | email: email,
323 | password: hashPassword,
324 | created: new Date(),
325 | };
326 |
327 |
328 | return callback(null, userFormatted);
329 |
330 |
331 | });
332 |
333 |
334 | }
335 |
336 | create(user) {
337 |
338 | const db = this.app.db;
339 |
340 | console.log("User:", user)
341 |
342 | return new Promise((resolve, reject) => {
343 |
344 |
345 | this.beforeSave(user, (err, user) => {
346 |
347 |
348 | console.log("After validation: ", err, user);
349 |
350 |
351 | if (err) {
352 | return reject(err);
353 | }
354 |
355 |
356 | // insert new user object to users collections
357 |
358 | db.collection('users').insertOne(user, (err, info) => {
359 |
360 |
361 | // check if error return error to user
362 | if (err) {
363 | return reject({message: "An error saving user."});
364 | }
365 |
366 | // otherwise return user object to user.
367 |
368 | const userId = _.get(user, '_id').toString(); // this is OBJET ID
369 |
370 |
371 | this.users = this.users.set(userId, user);
372 |
373 | return resolve(user);
374 |
375 | });
376 |
377 | });
378 |
379 |
380 | });
381 | }
382 | }
--------------------------------------------------------------------------------
/server/src/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Welcome to my website.
6 |
7 |
--------------------------------------------------------------------------------