├── src
├── client
│ ├── components
│ │ ├── MessageStatus
│ │ │ ├── index.js
│ │ │ └── style.css
│ │ ├── NewConversation
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── TypingIndicator
│ │ │ ├── index.js
│ │ │ └── style.css
│ │ ├── ViewConversation
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── RecipientsListItem
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── Conversation
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── Conversations
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── ConversationsListItem
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── ConversationsList
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── ConversationHeader
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── ConversationsListHeader
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── Message
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── ConversationThread
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── MessageInput
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── MessageRecipients
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ └── RecipientsList
│ │ │ ├── style.css
│ │ │ └── index.js
│ ├── containers
│ │ ├── RecipientsListContainer
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── ConversationThreadContainer
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ ├── ConversationsListContainer
│ │ │ ├── style.css
│ │ │ └── index.js
│ │ └── App
│ │ │ ├── index.js
│ │ │ └── style.css
│ ├── middleware
│ │ ├── index.js
│ │ └── logger.js
│ ├── actions
│ │ ├── profile.js
│ │ ├── recipientsList.js
│ │ ├── newConversation.js
│ │ ├── selectedConversation.js
│ │ ├── users.js
│ │ ├── conversations.js
│ │ └── draft.js
│ ├── constants
│ │ └── filters.js
│ ├── reducers
│ │ ├── newConversation.js
│ │ ├── recipientsList.js
│ │ ├── selectedConversation.js
│ │ ├── profile.js
│ │ ├── users.js
│ │ ├── index.js
│ │ ├── draft.js
│ │ └── conversations.js
│ ├── index.js
│ ├── store
│ │ └── index.js
│ └── index.html
├── utils.js
└── server
│ ├── models
│ └── user.js
│ ├── views
│ ├── login.ejs
│ └── signup.ejs
│ ├── routes.js
│ ├── messaging.js
│ └── auth.js
├── .gitignore
├── .babelrc
├── server.js
├── config
├── database.js
├── auth.js
└── passport.js
├── TODO.md
├── webpack.config.js
├── package.json
└── README.md
/src/client/components/MessageStatus/index.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/components/MessageStatus/style.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/components/NewConversation/style.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/components/TypingIndicator/index.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/components/TypingIndicator/style.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/components/ViewConversation/style.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/containers/RecipientsListContainer/style.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_STORE
2 | node_modules
3 | static
4 | .module-cache
5 | *.log*
6 |
7 |
--------------------------------------------------------------------------------
/src/client/middleware/index.js:
--------------------------------------------------------------------------------
1 |
2 | import logger from './logger'
3 |
4 | export {
5 | logger
6 | }
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "react"],
3 | "plugins": ["transform-runtime"]
4 | }
5 |
--------------------------------------------------------------------------------
/src/client/components/RecipientsListItem/style.css:
--------------------------------------------------------------------------------
1 | .recipientsListItem {
2 | background-color: #FFF;
3 | height: 2.5em;
4 | }
5 |
--------------------------------------------------------------------------------
/src/client/middleware/logger.js:
--------------------------------------------------------------------------------
1 |
2 | export default store => next => action => {
3 | console.log(action)
4 | return next(action)
5 | }
--------------------------------------------------------------------------------
/src/client/actions/profile.js:
--------------------------------------------------------------------------------
1 |
2 | import { createAction } from 'redux-actions'
3 |
4 | export const setUsername = createAction('set username')
5 |
--------------------------------------------------------------------------------
/src/client/actions/recipientsList.js:
--------------------------------------------------------------------------------
1 |
2 | import { createAction } from 'redux-actions'
3 |
4 | export const toggle = createAction('toggle recipients list')
5 |
--------------------------------------------------------------------------------
/src/client/actions/newConversation.js:
--------------------------------------------------------------------------------
1 |
2 | import { createAction } from 'redux-actions'
3 |
4 | export const newConversation = createAction('new conversation')
5 |
--------------------------------------------------------------------------------
/src/client/actions/selectedConversation.js:
--------------------------------------------------------------------------------
1 |
2 | import { createAction } from 'redux-actions'
3 |
4 | export const viewConversation = createAction('select conversation')
5 |
--------------------------------------------------------------------------------
/src/client/constants/filters.js:
--------------------------------------------------------------------------------
1 |
2 | export const SHOW_ALL = 'show_all'
3 | export const SHOW_COMPLETED = 'show_completed'
4 | export const SHOW_ACTIVE = 'show_active'
5 |
--------------------------------------------------------------------------------
/src/client/components/Conversation/style.css:
--------------------------------------------------------------------------------
1 | .conversation {
2 | position: absolute;
3 | top: 0;
4 | left: 340px;
5 | right: 0;
6 | bottom: 0;
7 | overflow: hidden;
8 | }
9 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var authServer = require('./src/server/auth');
4 | var messagingServer = require('./src/server/messaging');
5 |
6 | authServer.start();
7 | messagingServer.start();
8 |
--------------------------------------------------------------------------------
/src/client/actions/users.js:
--------------------------------------------------------------------------------
1 |
2 | import { createAction } from 'redux-actions'
3 |
4 | export const addUser = createAction('add user')
5 | export const updateUserList = createAction('update user list')
6 |
--------------------------------------------------------------------------------
/config/database.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: (process.env.NODE_ENV === 'test' ? 'iMsgTest' : 'iMsg'),
3 | host: 'localhost:27017',
4 | user: process.env.DATABASE_USER,
5 | password: process.env.DATABASE_PASSWORD
6 | };
7 |
--------------------------------------------------------------------------------
/src/client/components/Conversations/style.css:
--------------------------------------------------------------------------------
1 | .conversations {
2 | width: 340px;
3 | position: fixed;
4 | top: 0;
5 | left: 0;
6 | bottom: 0;
7 | border-right: 1px solid #ddd;
8 | box-sizing: border-box;
9 | overflow: hidden;
10 | }
11 |
--------------------------------------------------------------------------------
/src/client/reducers/newConversation.js:
--------------------------------------------------------------------------------
1 |
2 | import { handleActions } from 'redux-actions'
3 |
4 | const initialState = false
5 |
6 | export default handleActions({
7 | 'new conversation' (state, action) {
8 | return action.payload
9 | }
10 | }, initialState)
11 |
--------------------------------------------------------------------------------
/src/client/reducers/recipientsList.js:
--------------------------------------------------------------------------------
1 |
2 | import { handleActions } from 'redux-actions'
3 |
4 | const initialState = false
5 |
6 | export default handleActions({
7 | 'toggle recipients list' (state, action) {
8 | return !state
9 | }
10 | }, initialState)
11 |
--------------------------------------------------------------------------------
/src/client/reducers/selectedConversation.js:
--------------------------------------------------------------------------------
1 |
2 | import { handleActions } from 'redux-actions'
3 |
4 | const initialState = 0
5 |
6 | export default handleActions({
7 | 'select conversation' (state, action) {
8 | return action.payload
9 | }
10 | }, initialState)
11 |
--------------------------------------------------------------------------------
/src/client/containers/ConversationThreadContainer/style.css:
--------------------------------------------------------------------------------
1 | .threadView {
2 | position: absolute;
3 | margin: 0;
4 | padding: 0;
5 | top: 65px;
6 | bottom: 0;
7 | right: 0;
8 | left: 0;
9 | background: #FFF;
10 | overflow-x: hidden;
11 | }
12 |
--------------------------------------------------------------------------------
/src/client/containers/ConversationsListContainer/style.css:
--------------------------------------------------------------------------------
1 | .listView {
2 | position: absolute;
3 | top: 65px;
4 | left: 0;
5 | bottom: 0;
6 | background-color: #FFF;
7 | padding: 0;
8 | width: 339px;
9 | margin: 0;
10 | overflow-x: hidden;
11 | }
12 |
--------------------------------------------------------------------------------
/src/client/reducers/profile.js:
--------------------------------------------------------------------------------
1 |
2 | import { handleActions } from 'redux-actions'
3 |
4 | const initialState = {
5 | username: ''
6 | }
7 |
8 | export default handleActions({
9 | 'set username' (state, action) {
10 | return {
11 | username: action.payload
12 | }
13 | }
14 | }, initialState)
15 |
--------------------------------------------------------------------------------
/config/auth.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'cookieSecret': 'ch4t5evar',
3 | 'facebookAuth' : {
4 | 'clientID' : '1557410067920133', // your App ID
5 | 'clientSecret': '5e1d09351c1c70c36e6576be644cdd86', // your App Secret
6 | 'callbackURL' : 'http://localhost:8001/auth/facebook/callback'
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | var utils = {};
2 |
3 | utils.getUserIdFromSession = function (sessionString) {
4 | if (!sessionString) {
5 | return
6 | }
7 |
8 | var sessionBody = new Buffer(sessionString, 'base64').toString('utf8');
9 | var jsonBody = JSON.parse(sessionBody);
10 | return jsonBody.passport.user;
11 | };
12 |
13 | module.exports = utils;
14 |
--------------------------------------------------------------------------------
/src/client/reducers/users.js:
--------------------------------------------------------------------------------
1 |
2 | import { handleActions } from 'redux-actions'
3 |
4 | const initialState = []
5 |
6 | export default handleActions({
7 | 'add user' (state, action) {
8 | return [
9 | action.payload,
10 | ...state
11 | ]
12 | },
13 |
14 | 'update user list' (state, action) {
15 | return action.payload
16 | }
17 | }, initialState)
18 |
--------------------------------------------------------------------------------
/src/client/components/ConversationsListItem/style.css:
--------------------------------------------------------------------------------
1 | .listItem {
2 | display: block;
3 | overflow: hidden;
4 | }
5 |
6 | a {
7 | text-decoration: none;
8 | }
9 |
10 | h3 {
11 | color: #111;
12 | margin-bottom: 0;
13 | }
14 |
15 | .listDescription {
16 | color: #333;
17 | text-decoration: none;
18 | display: inline-block;
19 | text-overflow:ellipsis;
20 | overflow: hidden;
21 | white-space: nowrap;
22 | }
23 |
--------------------------------------------------------------------------------
/src/client/components/ConversationsList/style.css:
--------------------------------------------------------------------------------
1 | ul {
2 | margin: 0;
3 | padding: 0;
4 | list-style-type: none;
5 |
6 | }
7 |
8 | .conversationsList {
9 | position: absolute;
10 | top: 65px;
11 | left: 0;
12 | bottom: 0;
13 | right: 0;
14 | overflow-y: auto;
15 | }
16 |
17 | li {
18 | border-bottom: 1px solid #ccc;
19 | height: 5em;
20 | padding: 0 1em;
21 | }
22 |
23 | .selectedListItem {
24 | background-color: #D9D9D9;
25 | }
26 |
--------------------------------------------------------------------------------
/src/client/components/ConversationHeader/style.css:
--------------------------------------------------------------------------------
1 | .header {
2 | box-sizing: border-box;
3 | position: relative;
4 | z-index: 10;
5 | height: 65px;
6 | background-color: rgba(246,246,246, 0.97);
7 | border-bottom: 1px solid rgb(207,207,207);
8 | }
9 |
10 | .title {
11 | color: #111;
12 | line-height: 65px;
13 | text-align: center;
14 | font-size: 1.3em;
15 | }
16 |
17 | .titleLabel {
18 | display: inline-block;
19 | font-weight: bold;
20 | text-align: center;
21 | }
22 |
--------------------------------------------------------------------------------
/src/client/actions/conversations.js:
--------------------------------------------------------------------------------
1 |
2 | import { createAction } from 'redux-actions'
3 |
4 | export const addConversation = createAction('add conversation')
5 | export const deleteConversation = createAction('delete converstaion')
6 | export const addMessage = createAction('add message')
7 | export const receiveMessage = createAction('receive message')
8 | export const readMessage = createAction('read message')
9 | export const startTyping = createAction('start typing')
10 | export const updateConversationList = createAction('update conversation list')
11 |
--------------------------------------------------------------------------------
/src/client/components/Conversation/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import ConversationThreadContainer from '../../containers/ConversationThreadContainer'
4 | import style from './style.css'
5 |
6 | class Conversation extends Component {
7 | render() {
8 | const { conversation } = this.props
9 | console.log('~~~ a');
10 | return (
11 |
14 | )
15 | }
16 | }
17 |
18 | export default Conversation
19 |
--------------------------------------------------------------------------------
/src/client/components/Conversations/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import ConversationsListHeader from '../ConversationsListHeader'
4 | import ConversationsListContainer from '../../containers/ConversationsListContainer'
5 | import style from './style.css'
6 |
7 | class Conversations extends Component {
8 | render() {
9 | const { conversations } = this.props
10 | return (
11 |
15 | )
16 | }
17 | }
18 |
19 | export default Conversations
20 |
--------------------------------------------------------------------------------
/src/client/reducers/index.js:
--------------------------------------------------------------------------------
1 |
2 | import { routeReducer as routing } from 'react-router-redux'
3 | import { combineReducers } from 'redux'
4 | import conversations from './conversations'
5 | import users from './users'
6 | import recipientsList from './recipientsList'
7 | import draft from './draft'
8 | import profile from './profile'
9 | import selectedConversation from './selectedConversation'
10 | import newConversation from './newConversation'
11 |
12 | export default combineReducers({
13 | routing,
14 | conversations,
15 | users,
16 | recipientsList,
17 | draft,
18 | profile,
19 | selectedConversation,
20 | newConversation
21 | })
22 |
--------------------------------------------------------------------------------
/src/client/components/ConversationsListHeader/style.css:
--------------------------------------------------------------------------------
1 | .header {
2 | box-sizing: border-box;
3 | position: relative;
4 | height: 65px;
5 | background-color: rgb(246,246,246);
6 | border-bottom: 1px solid rgb(207,207,207);
7 | }
8 |
9 | .title {
10 | color: #111;
11 | line-height: 65px;
12 | text-align: center;
13 | font-size: 1.3em;
14 | }
15 |
16 | .titleLabel {
17 | display: inline-block;
18 | font-weight: bold;
19 | text-align: center;
20 | }
21 |
22 | .titleRightAction {
23 | display: inline-block;
24 | position: absolute;
25 | color: rgb(20,111,246);
26 | text-decoration: none;
27 | right: 0;
28 | width: 65px;
29 | line-height: 65px;
30 | height: 65px;
31 | font-size: 1.7em;
32 | }
33 |
--------------------------------------------------------------------------------
/src/server/models/user.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 | var bcrypt = require('bcrypt-nodejs');
3 |
4 | // define the schema for our user model
5 | var userSchema = mongoose.Schema({
6 | local : {
7 | username : String,
8 | password : String
9 | }
10 | });
11 |
12 | // generating a hash
13 | userSchema.methods.generateHash = function(password) {
14 | return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
15 | };
16 |
17 | // checking if password is valid
18 | userSchema.methods.validPassword = function(password) {
19 | return bcrypt.compareSync(password, this.local.password);
20 | };
21 |
22 | // create the model for users and expose it to our app
23 | module.exports = mongoose.model('User', userSchema);
24 |
--------------------------------------------------------------------------------
/src/client/components/Message/style.css:
--------------------------------------------------------------------------------
1 | .messageWrapper {
2 | display: block;
3 | width: 100%;
4 | height: initial;
5 | }
6 |
7 | .message {
8 | display: inline-block;
9 | position: relative;
10 | float: left;
11 | clear: both;
12 | background-color: #E5E5EA;
13 | color: #111;
14 | border-radius: 13px;
15 | height: initial;
16 | padding: .3em .75em;
17 | margin-top: 3px;
18 | border: none;
19 | }
20 |
21 | .messageRight {
22 | display: inline-block;
23 | position: relative;
24 | float: right;
25 | clear: both;
26 | background-color: rgb(20,111,246);
27 | color: #FFF;
28 | border-radius: 13px;
29 | height: initial;
30 | padding: .3em .75em;
31 | margin-top: 3px;
32 | border: none;
33 | }
34 |
--------------------------------------------------------------------------------
/src/client/components/ConversationHeader/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { connect } from 'react-redux'
4 | import style from './style.css'
5 |
6 | class ConversationHeader extends Component {
7 | render() {
8 | const { newConversation, titleLabel } = this.props
9 | return (
10 |
15 | )
16 | }
17 | }
18 |
19 | function mapStateToProps(state) {
20 | var conversationsState = state.conversations
21 | return {
22 | newConversation: state.newConversation
23 | }
24 | }
25 | export default connect(mapStateToProps)(ConversationHeader)
26 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # TODO
2 |
3 | * Deploy to AWS via Elastic Beanstalk and a Mongo instance
4 |
5 | ## Tooling
6 | * Test suite
7 | * Linting
8 | * Continuous Integration
9 |
10 | ## Client side
11 | * Mobile friendly/responsive web application
12 | * Typing indicator
13 | * Message status (delivered / read)
14 | * Time stamp indicators
15 | * Consecutive messages within minute are tightly bunched.
16 | * Groups within hour have timestamp heading
17 | * Last message in bunch has tick
18 | * Support Unicode 8.0 to allow emojis
19 | * Handle delivered acknowledgement
20 | * Handle read receipt
21 | * Direct manipulation pan to reveal timestamps
22 |
23 | ## Server side
24 | * paginated history via minimal rest api
25 | * Persist to data store and add caching (e.g. Redis integration to Deepstream.io) instead of default in-memory storage
26 |
--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
1 |
2 | import { Router, Route, browserHistory, IndexRoute } from 'react-router'
3 | import { Provider } from 'react-redux'
4 | import ReactDOM from 'react-dom'
5 | import React from 'react'
6 |
7 | import App from './containers/App'
8 | import NewConversation from './components/NewConversation'
9 | import ViewConversation from './components/ViewConversation'
10 | import configure from './store'
11 |
12 | var store = configure()
13 | ReactDOM.render(
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ,
22 | document.getElementById('root')
23 | )
24 |
--------------------------------------------------------------------------------
/src/client/components/ConversationThread/style.css:
--------------------------------------------------------------------------------
1 | .threadContainer {
2 | position: absolute;
3 | top: 0;
4 | bottom: 55px;
5 | left: 0;
6 | right: 0;
7 | border-bottom: 1px solid rgb(207,207,207);
8 | background-color: #FFF;
9 | overflow-y: auto;
10 | -webkit-overflow-scrolling: touch;
11 | padding: 10px;
12 | display: flex;
13 | flex-direction: column;
14 | }
15 |
16 | .threadList {
17 | position: absolute;
18 | top: 0;
19 | bottom: 0;
20 | left: 0;
21 | right: 0;
22 | }
23 |
24 | .threadContainer ul li {
25 | display: block;
26 | border: none;
27 | overflow: hidden;
28 | margin: 0;
29 | padding: 0;
30 | }
31 |
32 | .listPadder {
33 | flex-grow: 1;
34 | position: relative;
35 | top: 0;
36 | bottom: 0;
37 | left: 0;
38 | right: 0;
39 | min-height: 120px;
40 | }
41 |
--------------------------------------------------------------------------------
/src/client/store/index.js:
--------------------------------------------------------------------------------
1 |
2 | import { createStore, applyMiddleware } from 'redux'
3 | import { syncHistory } from 'react-router-redux'
4 | import { browserHistory } from 'react-router'
5 | import thunk from 'redux-thunk'
6 |
7 | import { logger } from '../middleware'
8 | import rootReducer from '../reducers'
9 |
10 | export default function configure(initialState) {
11 | const create = window.devToolsExtension
12 | ? window.devToolsExtension()(createStore)
13 | : createStore
14 |
15 | const createStoreWithMiddleware = applyMiddleware(
16 | logger,
17 | syncHistory(browserHistory),
18 | thunk
19 | )(create)
20 |
21 | const store = createStoreWithMiddleware(rootReducer, initialState)
22 |
23 | if (module.hot) {
24 | module.hot.accept('../reducers', () => {
25 | const nextReducer = require('../reducers')
26 | store.replaceReducer(nextReducer)
27 | })
28 | }
29 |
30 | return store
31 | }
32 |
--------------------------------------------------------------------------------
/src/client/reducers/draft.js:
--------------------------------------------------------------------------------
1 |
2 | import { handleActions } from 'redux-actions'
3 |
4 | const initialState = {
5 | recipients: [],
6 | availableRecipients: [],
7 | body: ''
8 | }
9 |
10 | export default handleActions({
11 | 'new draft' (state, action) {
12 | return {
13 | recipients: [],
14 | body: ''
15 | }
16 | },
17 |
18 | 'add recipient' (state, action) {
19 | var u = {}
20 | var recipients = []
21 | for (let recipient of [...state.recipients, 'users/'+action.payload]) {
22 | if (!u.hasOwnProperty(recipient) ) {
23 | u[recipient] = recipient
24 | recipients.push(recipient)
25 | }
26 | }
27 | return {
28 | recipients: recipients,
29 | body: state.body
30 | }
31 | },
32 |
33 | 'remove recipient' (state, action) {
34 | },
35 |
36 | 'add message body' (state, action) {
37 | return {
38 | recipients: [...state.recipients],
39 | body: action.payload
40 | }
41 | }
42 | }, initialState)
43 |
--------------------------------------------------------------------------------
/src/client/components/NewConversation/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { bindActionCreators } from 'redux'
4 | import { connect } from 'react-redux'
5 |
6 | import Conversations from '../../components/Conversations'
7 | import Conversation from '../../components/Conversation'
8 | import * as ConversationActions from '../../actions/conversations'
9 | import * as NewConversationActions from '../../actions/newConversation'
10 | import * as DraftActions from '../../actions/draft'
11 | import App from '../../containers/App'
12 | import style from './style.css'
13 |
14 | class NewConversation extends Component {
15 | componentWillMount() {
16 | const { dispatch } = this.props
17 | dispatch(NewConversationActions.newConversation(true))
18 | dispatch(DraftActions.clearDraft())
19 | }
20 |
21 | render() {
22 | return (
23 |
24 |
25 |
26 |
27 | )
28 | }
29 | }
30 |
31 | export default connect()(NewConversation)
32 |
--------------------------------------------------------------------------------
/src/client/components/ConversationsListHeader/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { connect } from 'react-redux'
3 | import { browserHistory } from 'react-router'
4 |
5 | import style from './style.css'
6 | import * as NewConversationActions from '../../actions/newConversation'
7 | import * as DraftActions from '../../actions/draft'
8 |
9 | class ConversationsListHeader extends Component {
10 | onAddConversationClick(e) {
11 | e.preventDefault()
12 | const { dispatch } = this.props
13 | browserHistory.push('/')
14 | }
15 |
16 | render() {
17 | const users = this.props.users
18 | return (
19 |
20 |
21 |
Messages
22 |
+
23 |
24 |
25 | )
26 | }
27 | }
28 |
29 | export default connect()(ConversationsListHeader)
30 |
--------------------------------------------------------------------------------
/src/client/components/ConversationsList/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import ConversationsListItem from '../ConversationsListItem'
4 | import style from './style.css'
5 |
6 | class ConversationsList extends Component {
7 | render() {
8 | const { newConversation, selectedConversation, conversations, conversationsById, onConversationClick } = this.props
9 | var items = []
10 | if (newConversation) {
11 | items.push()
12 | }
13 | for (let index in conversations) {
14 | var conversationId = conversations[index]
15 | items.push()
16 | }
17 | return (
18 |
19 | )
20 | }
21 | }
22 |
23 | export default ConversationsList
24 |
--------------------------------------------------------------------------------
/src/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | iMsg
7 |
8 |
9 |
10 |
11 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/client/components/MessageInput/style.css:
--------------------------------------------------------------------------------
1 | .messageInput {
2 | position: absolute;
3 | display: flex;
4 | height: 55px;
5 | bottom: 0;
6 | left: 0;
7 | right: 0;
8 | background-color: #F5F5F3;
9 | }
10 |
11 | .messageInput textarea {
12 | -webkit-box-flex: 1;
13 | flex-grow: 1;
14 | font-size: 14px;
15 | text-indent: 8px;
16 | resize: none;
17 | color: rgba(0, 0, 0, 0.870588);
18 | line-height: 30px;
19 | overflow-x: hidden;
20 | overflow-y: hidden;
21 | background-color: rgb(250, 251, 252);
22 | border-image-source: initial;
23 | border-image-slice: initial;
24 | border-image-width: initial;
25 | border-image-outset: initial;
26 | border-image-repeat: initial;
27 | outline: none;
28 | margin: 10px;
29 | border: 1px solid rgb(228, 233, 236);
30 | border-radius: 4px;
31 | }
32 |
33 | .button {
34 | display: inline-block;
35 | color: rgb(20,111,246);
36 | text-decoration: none;
37 | width: 55px;
38 | height: 55px;
39 | line-height: 55px;
40 | font-size: 1.3em;
41 | font-weight: 700;
42 | }
43 |
--------------------------------------------------------------------------------
/src/client/components/Message/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { connect } from 'react-redux'
4 |
5 | import App from '../../containers/App'
6 | import style from './style.css'
7 |
8 | class Message extends Component {
9 | componentDidMount() {
10 | const { message } = this.props
11 | console.log('<<< MESSAGE ID: ', message);
12 | this.record = App.ds.record.getRecord(message);
13 | this.record.whenReady( function() {
14 | setTimeout(function () {
15 | this.setState(this.record.get() );
16 | }.bind(this), 250);
17 | }.bind( this ));
18 | }
19 |
20 | render() {
21 | const { profile } = this.props
22 | return (
23 |
24 |
25 | { this.state ? this.state.body : '' }
26 |
27 |
28 | )
29 | }
30 | }
31 |
32 | function mapStateToProps(state) {
33 | return {
34 | profile: state.profile
35 | }
36 | }
37 |
38 | export default connect(mapStateToProps)(Message)
39 |
--------------------------------------------------------------------------------
/src/client/components/RecipientsListItem/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { connect } from 'react-redux'
4 |
5 | import style from './style.css'
6 | import deepstream from 'deepstream.io-client-js'
7 | import App from '../../containers/App'
8 | import * as DraftActions from '../../actions/draft'
9 | import * as RecipientsListActions from '../../actions/recipientsList'
10 |
11 | class RecipientsListItem extends Component {
12 | onClick(e) {
13 | const { dispatch } = this.props
14 | e.preventDefault()
15 | dispatch(DraftActions.addRecipient(this.state.username))
16 | dispatch(RecipientsListActions.toggle())
17 | }
18 |
19 | componentDidMount() {
20 | const { user } = this.props
21 | this.record = App.ds.record.getRecord(user);
22 | this.record.whenReady( function() {
23 | this.setState(this.record.get() );
24 | }.bind( this ));
25 | }
26 |
27 | render() {
28 | return (
29 |
30 | {this.state ? this.state.username : 'wut'}
31 |
32 | )
33 | }
34 | }
35 |
36 | export default connect()(RecipientsListItem)
37 |
--------------------------------------------------------------------------------
/src/client/containers/ConversationsListContainer/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { bindActionCreators } from 'redux'
4 | import { connect } from 'react-redux'
5 |
6 | import ConversationsList from '../../components/ConversationsList'
7 | import style from './style.css'
8 |
9 | class ConversationsListContainer extends Component {
10 | render() {
11 | const { newConversation, selectedConversation, conversations, conversationsById } = this.props
12 | return (
13 |
18 | )
19 | }
20 | }
21 |
22 | function mapStateToProps(state) {
23 | var conversationsState = state.conversations
24 | return {
25 | newConversation: state.newConversation,
26 | selectedConversation: state.selectedConversation,
27 | conversations: conversationsState.conversations,
28 | conversationsById: conversationsState.conversationsById
29 | }
30 | }
31 |
32 | export default connect(
33 | mapStateToProps
34 | )(ConversationsListContainer)
35 |
--------------------------------------------------------------------------------
/src/client/components/MessageRecipients/style.css:
--------------------------------------------------------------------------------
1 | .messageRecipients {
2 | position: relative;
3 | z-index: 10;
4 | display: flex;
5 | height: 55px;
6 | top: 0;
7 | left: 0;
8 | right: 0;
9 | background-color: rgba(246,246,246, 0.95);
10 | border-bottom: 1px solid rgb(207,207,207);
11 | }
12 |
13 | .toLabel {
14 | display: inline-block;
15 | height: 55px;
16 | line-height: 55px;
17 | width: 30px;
18 | padding-left: 20px;
19 | }
20 |
21 | textarea {
22 | -webkit-box-flex: 1;
23 | flex-grow: 1;
24 | font-size: 14px;
25 | text-indent: 8px;
26 | resize: none;
27 | color: rgba(0, 0, 0, 0.870588);
28 | line-height: 30px;
29 | overflow-x: hidden;
30 | overflow-y: hidden;
31 | background-color: transparent;
32 | outline: none;
33 | margin: 10px;
34 | border: none;
35 | border-radius: 4px;
36 | }
37 |
38 | .titleRightAction {
39 | display: inline-block;
40 | position: absolute;
41 | color: rgb(20,111,246);
42 | text-decoration: none;
43 | right: 0;
44 | line-height: 20px;
45 | width: 25px;
46 | height: 25px;
47 | font-size: 1.7em;
48 | text-align: center;
49 | border: 1px solid rgb(20,111,246);
50 | border-radius: 25px;
51 | margin: 15px;
52 | }
53 |
--------------------------------------------------------------------------------
/src/client/containers/RecipientsListContainer/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { bindActionCreators } from 'redux'
4 | import { connect } from 'react-redux'
5 |
6 | import RecipientsList from '../../components/RecipientsList'
7 | import * as UserActions from '../../actions/users'
8 | import style from './style.css'
9 |
10 | class RecipientsListContainer extends Component {
11 | render() {
12 | const { users, selectedRecipients, showRecipientsList } = this.props
13 | var availableRecipients = users.filter(function (userRecordName) {
14 | if (userRecordName && selectedRecipients.indexOf(userRecordName) === -1) {
15 | return userRecordName
16 | }
17 | })
18 | return (
19 |
20 | )
21 | }
22 | }
23 |
24 | function mapStateToProps(state) {
25 | return {
26 | users: state.users,
27 | selectedRecipients: state.draft.recipients,
28 | showRecipientsList: state.recipientsList
29 | }
30 | }
31 |
32 | function mapDispatchToProps(dispatch) {
33 | return {
34 | actions: bindActionCreators(UserActions, dispatch)
35 | }
36 | }
37 |
38 |
39 | export default connect(
40 | mapStateToProps,
41 | mapDispatchToProps
42 | )(RecipientsListContainer)
43 |
--------------------------------------------------------------------------------
/src/client/components/RecipientsList/style.css:
--------------------------------------------------------------------------------
1 | .animate {
2 | -webkit-transition: top 0.3s ease-in;
3 | -moz-transition: top 0.3s ease-in;
4 | -ms-transition: top 0.3s ease-in;
5 | -o-transition: top 0.3s ease-in;
6 | transition: top 0.3s ease-in;
7 | }
8 | .recipientsList {
9 | position: absolute;
10 | z-index: 10;
11 | display: block;
12 | top: 100%;
13 | left: 0;
14 | right: 0;
15 | bottom: 0;
16 | background-color: #FFF;
17 | }
18 |
19 | .recipientsListVisible {
20 | position: absolute;
21 | z-index: 10;
22 | display: block;
23 | top: 0px;
24 | left: 0;
25 | right: 0;
26 | bottom: 0;
27 | background-color: #FFF;
28 | }
29 |
30 | .header {
31 | box-sizing: border-box;
32 | position: relative;
33 | z-index: 10;
34 | height: 65px;
35 | background-color: rgba(246,246,246, 0.95);
36 | border-bottom: 1px solid rgb(207,207,207);
37 | }
38 |
39 | .recipientsList .title,
40 | .recipientsListVisible .title{
41 | color: #111;
42 | line-height: 65px;
43 | text-align: center;
44 | font-size: 1.3em;
45 | }
46 |
47 | .titleLabel {
48 | display: inline-block;
49 | font-weight: bold;
50 | text-align: center;
51 | }
52 |
53 | .titleRightAction {
54 | display: inline-block;
55 | position: absolute;
56 | color: rgb(20,111,246);
57 | text-decoration: none;
58 | right: 10px;
59 | width: 65px;
60 | line-height: 65px;
61 | height: 65px;
62 | font-size: 1em;
63 | }
64 |
--------------------------------------------------------------------------------
/src/server/views/login.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Node Authentication
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
Login
17 |
18 |
19 | <% if (message.length > 0) { %>
20 |
<%= message %>
21 | <% } %>
22 |
23 |
24 |
36 |
37 |
38 |
39 |
Need an account? Signup
40 |
Or go home.
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/server/views/signup.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Node Authentication
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
Signup
17 |
18 |
19 | <% if (message.length > 0) { %>
20 |
<%= message %>
21 | <% } %>
22 |
23 |
24 |
36 |
37 |
38 |
39 |
Already have an account? Login
40 |
Or go home.
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/client/components/ConversationsListItem/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { bindActionCreators } from 'redux'
4 | import { connect } from 'react-redux'
5 | import { Link } from 'react-router'
6 | import App from '../../containers/App'
7 |
8 | import style from './style.css'
9 |
10 | class ConversationListItem extends Component {
11 | componentWillReceiveProps(nextProps) {
12 | const { conversation } = nextProps
13 | if (conversation) {
14 | this.conversationRecord = App.ds.record.getRecord(conversation.id)
15 | this.conversationRecord.whenReady(() => {
16 | var lastMessage = this.conversationRecord.get('lastMessage')
17 | this.setState({ lastMessage: lastMessage })
18 |
19 | this.conversationRecord.subscribe('lastMessage', (lastMessage) => {
20 | this.setState({ lastMessage: lastMessage })
21 | })
22 | })
23 | }
24 | }
25 |
26 | componentWillUnmount() {
27 | if (this.conversationRecord) {
28 | this.conversationRecord.unsubscribe('lastMessage')
29 | }
30 | }
31 |
32 | render() {
33 | const { conversation } = this.props
34 | return (
35 |
36 | {conversation ? conversation.title : ''}
37 | {this.state ? this.state.lastMessage : ''}
38 |
39 | )
40 | }
41 | }
42 |
43 | export default connect()(ConversationListItem)
44 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var rucksack = require('rucksack-css')
2 | var webpack = require('webpack')
3 | var path = require('path')
4 |
5 | module.exports = {
6 | context: path.join(__dirname, './src/client'),
7 | entry: {
8 | jsx: './index.js',
9 | html: './index.html',
10 | vendor: ['react', 'deepstream.io-client-js']
11 | },
12 | output: {
13 | path: path.join(__dirname, './static'),
14 | filename: 'bundle.js',
15 | },
16 | module: {
17 | loaders: [
18 | {
19 | test: /\.html$/,
20 | loader: 'file?name=[name].[ext]'
21 | },
22 | {
23 | test: /\.css$/,
24 | loaders: [
25 | 'style-loader',
26 | 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]',
27 | 'postcss-loader'
28 | ]
29 | },
30 | {
31 | test: /\.(js|jsx)$/,
32 | exclude: /node_modules/,
33 | loaders: [
34 | 'react-hot',
35 | 'babel-loader'
36 | ]
37 | },
38 | ],
39 | },
40 | resolve: {
41 | extensions: ['', '.js', '.jsx']
42 | },
43 | postcss: [
44 | rucksack({
45 | autoprefixer: true
46 | })
47 | ],
48 | plugins: [
49 | new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js'),
50 | new webpack.DefinePlugin({
51 | 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development') }
52 | })
53 | ],
54 | devServer: {
55 | contentBase: './src/client',
56 | hot: true
57 | },
58 | node: {
59 | fs: 'empty',
60 | net: 'empty',
61 | tls: 'empty'
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/client/components/RecipientsList/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { connect } from 'react-redux'
4 |
5 | import RecipientsListItem from '../RecipientsListItem'
6 | import * as RecipientsListActions from '../../actions/recipientsList'
7 | import style from './style.css'
8 |
9 | class RecipientsList extends Component {
10 | onCancelRecipientsList(e) {
11 | e.preventDefault()
12 | const { dispatch } = this.props
13 | dispatch(RecipientsListActions.toggle())
14 | }
15 |
16 | render() {
17 | const { users, profile, visible } = this.props
18 | var items = []
19 | var filtered = users.filter(function (user) {
20 | if (user == 'users/' + profile.username) {
21 | return false
22 | }
23 | return true
24 | })
25 | console.log(filtered, profile.username)
26 | for (let index in filtered) {
27 | items.push()
28 | }
29 | return (
30 |
39 | )
40 | }
41 | }
42 |
43 | function mapStateToProps(state) {
44 | return {
45 | profile: state.profile
46 | }
47 | }
48 |
49 | export default connect(mapStateToProps)(RecipientsList)
50 |
--------------------------------------------------------------------------------
/src/client/components/MessageRecipients/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { bindActionCreators } from 'redux'
4 | import { connect } from 'react-redux'
5 |
6 | import * as RecipientsListActions from '../../actions/recipientsList'
7 | import style from './style.css'
8 |
9 | class MessageRecipients extends Component {
10 | onClickAddRecipient(e) {
11 | e.preventDefault()
12 | const { actions } = this.props
13 | actions.toggle()
14 | }
15 |
16 | render() {
17 | const { newConversation } = this.props
18 | var addRecipientButton
19 | if (newConversation) {
20 | addRecipientButton =
21 | +
22 | }
23 | const { recipients } = this.props
24 | var recipientsPretty = recipients.map(function (recipient) {
25 | return recipient.split('/')[1]
26 | })
27 | return (
28 |
29 | To:
30 |
31 | {addRecipientButton}
32 |
33 | )
34 | }
35 | }
36 |
37 | function mapStateToProps(state) {
38 | var draftState = state.draft
39 | return {
40 | newConversation: state.newConversation,
41 | recipients: draftState.recipients,
42 | availableRecipients: draftState.availableRecipients,
43 | body: draftState.body
44 | }
45 | }
46 |
47 | function mapDispatchToProps(dispatch) {
48 | return {
49 | actions: bindActionCreators(RecipientsListActions, dispatch)
50 | }
51 | }
52 |
53 | export default connect(
54 | mapStateToProps,
55 | mapDispatchToProps
56 | )(MessageRecipients)
57 |
--------------------------------------------------------------------------------
/src/client/reducers/conversations.js:
--------------------------------------------------------------------------------
1 |
2 | import { handleActions } from 'redux-actions'
3 |
4 | const initialState = {
5 | conversations: [],
6 | conversationsById: {}
7 | }
8 |
9 | export default handleActions({
10 | 'view conversation' (state, action) {
11 | return Object.assign({}, state, {
12 | selectedConversation: action.payload
13 | })
14 | },
15 |
16 | 'add conversation' (state, action) {
17 | var conversation = action.payload
18 | conversation.title = action.payload.title || (conversation.participants ? conversation.participants.map(function (participant) {
19 | return participant.split('/')[1]
20 | }).join(', ') : 'New Message')
21 | var conversations = [...state.conversations]
22 | if (conversations.indexOf(conversation.id) == -1) {
23 | conversations.unshift(conversation.id)
24 | }
25 | var conversationsById = Object.assign({}, state.conversationsById)
26 | conversationsById[conversation.id] = conversation
27 |
28 | return Object.assign({}, state, {
29 | conversations: conversations,
30 | conversationsById: conversationsById
31 | })
32 | },
33 |
34 | 'add message' (state, action) {
35 | var conversationsById = Object.assign({}, state.conversationsById)
36 | conversationsById[action.payload.conversationId].messages = [action.payload, ...state.conversationsById[action.payload.conversationId].messages]
37 | conversationsById[action.payload.conversationId].lastMessage = action.payload.body
38 | return Object.assign({}, state, {
39 | conversationsById: conversationsById
40 | })
41 | },
42 |
43 | 'update conversation list' (state, action) {
44 | return Object.assign({}, state, {
45 | conversations: [...action.payload]
46 | })
47 | }
48 | }, initialState)
49 |
--------------------------------------------------------------------------------
/src/server/routes.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | module.exports = function (app, passport) {
4 |
5 | // route for chat app
6 | app.get('/', isLoggedIn, function (req, res) {
7 | res.redirect('localhost:3000/');
8 | });
9 |
10 | // login page
11 | app.get('/login', function (req, res) {
12 | res.render(path.join(__dirname, './views/login.ejs'), { message: req.flash('loginMessage') });
13 | });
14 |
15 | // process the login form
16 | app.post('/login', passport.authenticate('local-login', {
17 | successRedirect : 'http://localhost:3000/', // redirect the secure app
18 | failureRedirect : '/login', // redirect back to the signup page if there is an error
19 | failureFlash : true // allow flash messages
20 | }));
21 |
22 | // signup page
23 | app.get('/signup', function (req, res) {
24 | res.render(path.join(__dirname, './views/signup.ejs'), { message: req.flash('signupMessage') });
25 | });
26 |
27 | app.post('/signup', passport.authenticate('local-signup', {
28 | successRedirect : '/login', // redirect to the secure app
29 | failureRedirect : '/signup', // redirect back to the signup page if there is an error
30 | failureFlash : true // allow flash messages
31 | }));
32 |
33 | // route for logging out
34 | app.get('/logout', function(req, res) {
35 | req.logout();
36 | res.redirect('/login');
37 | });
38 | };
39 |
40 | // route middleware to make sure a user is logged in
41 | function isLoggedIn(req, res, next) {
42 |
43 | // if user is authenticated in the session, carry on
44 | if (req.isAuthenticated()) {
45 | return next();
46 | }
47 |
48 | // if they aren't redirect the login
49 | res.redirect('/login');
50 | }
51 |
--------------------------------------------------------------------------------
/src/client/components/MessageInput/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { bindActionCreators } from 'redux'
4 | import { connect } from 'react-redux'
5 | import style from './style.css'
6 | import App from '../../containers/App'
7 | import * as DraftActions from '../../actions/draft'
8 |
9 | function getTitleFromRecipients(recipients) {
10 | return title
11 | }
12 |
13 | class MessageInput extends Component {
14 | onKeyPress(e) {
15 | if (e.which != 13 || e.shiftKey) {
16 | return
17 | }
18 | e.preventDefault()
19 | const { dispatch, draft } = this.props
20 |
21 | // If no recipients, no-op
22 | if (draft.recipients.length == 0) {
23 | console.log('Message has no recipients!')
24 | return
25 | }
26 |
27 | var messageBody = this.textarea.value
28 | if (messageBody) {
29 | dispatch(DraftActions.sendDraft(messageBody))
30 | }
31 | this.textarea.value = ''
32 | }
33 |
34 | onSendClick(e) {
35 | e.preventDefault()
36 | const { dispatch, draft } = this.props
37 |
38 | // If no recipients, no-op
39 | if (draft.recipients.length == 0) {
40 | console.log('Message has no recipients!')
41 | return
42 | }
43 |
44 | var messageBody = this.textarea.value
45 | if (messageBody) {
46 | dispatch(DraftActions.sendDraft(messageBody))
47 | }
48 | this.textarea.value = ''
49 | }
50 |
51 | render() {
52 | return (
53 |
57 | )
58 | }
59 | }
60 |
61 | function mapStateToProps(state) {
62 | var draftState = state.draft
63 | return {
64 | draft: draftState
65 | }
66 | }
67 |
68 | export default connect(
69 | mapStateToProps
70 | )(MessageInput)
71 |
--------------------------------------------------------------------------------
/src/server/messaging.js:
--------------------------------------------------------------------------------
1 | //var RedisCacheConnector = require( 'deepstream.io-cache-redis' );
2 | //var RedisMessageConnector = require( 'deepstream.io-msg-redis' );
3 | var DeepstreamServer = require('deepstream.io');
4 | var server = new DeepstreamServer();
5 | var C = server.constants;
6 | var cookieParser = require('cookie-parser');
7 | var cookieSecret = require('../../config/auth').cookieSecret;
8 | var User = require('./models/user');
9 | var utils = require('../utils');
10 |
11 | var authServer = {
12 | start: function () {
13 | // Optionally you can specify some settings, a full list of which
14 | // can be found here //deepstream.io/docs/deepstream.html
15 | server.set( 'host', 'localhost' );
16 | server.set( 'port', 6020 );
17 |
18 | /*
19 | server.set( 'cache', new RedisCacheConnector({
20 | port: 6379,
21 | host: 'localhost'
22 | }));
23 |
24 | server.set( 'messageConnector', new RedisMessageConnector({
25 | port: 6379,
26 | host: 'localhost'
27 | }));
28 | */
29 |
30 | server.set('permissionHandler', {
31 | isValidUser: function( connectionData, authData, callback ) {
32 | if (authData.sid) {
33 | var userId = utils.getUserIdFromSession(authData.sid);
34 | User.findById(userId, function (err, foundUser) {
35 | callback(err, userId, foundUser.local.toObject());
36 | });
37 | } else {
38 | callback('Invalid credentials');
39 | }
40 | },
41 |
42 | canPerformAction: function( username, message, callback ) {
43 | callback( null, true );
44 | },
45 |
46 | onClientDisconnect: function( username ){} // this one is optional
47 | });
48 |
49 | // start the server
50 | server.start();
51 | }
52 | }
53 |
54 | module.exports = authServer;
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend-boilerplate",
3 | "version": "1.0.0",
4 | "private": true,
5 | "description": "A boilerplate of things that shouldn't exist",
6 | "main": "server.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "start": "npm run start:express & npm run start:webpack",
10 | "start:express": "node server.js",
11 | "start:webpack": "webpack-dev-server --history-api-fallback --hot --inline --progress --colors --port 3000",
12 | "build": "NODE_ENV=production webpack --progress --colors"
13 | },
14 | "devDependencies": {
15 | "babel-core": "^6.3.26",
16 | "babel-loader": "^6.2.0",
17 | "babel-plugin-transform-runtime": "^6.3.13",
18 | "babel-preset-es2015": "^6.3.13",
19 | "babel-preset-react": "^6.3.13",
20 | "babel-preset-stage-0": "^6.3.13",
21 | "babel-runtime": "^6.3.19",
22 | "css-loader": "^0.23.1",
23 | "deepstream.io-client-js": "0.4.0",
24 | "file-loader": "^0.8.4",
25 | "js-cookie": "2.1.0",
26 | "postcss-loader": "^0.8.0",
27 | "react": "^0.14.0",
28 | "react-dom": "^0.14.0",
29 | "react-hot-loader": "^1.3.0",
30 | "react-redux": "^4.0.6",
31 | "react-router": "^2.0.0-rc5",
32 | "react-router-redux": "^2.1.0",
33 | "redux": "^3.0.2",
34 | "redux-actions": "^0.9.0",
35 | "redux-thunk": "1.0.3",
36 | "rucksack-css": "^0.8.5",
37 | "style-loader": "^0.12.4",
38 | "webpack": "^1.12.2",
39 | "webpack-dev-server": "^1.12.0",
40 | "webpack-hot-middleware": "^2.2.0"
41 | },
42 | "dependencies": {
43 | "bcrypt-nodejs": "0.0.3",
44 | "body-parser": "1.15.0",
45 | "connect-ensure-login": "0.1.1",
46 | "connect-flash": "0.1.1",
47 | "connect-mongo": "0.8.1",
48 | "cookie-parser": "1.4.1",
49 | "cookie-session": "1.2.0",
50 | "deepstream.io": "0.8.0",
51 | "ejs": "^2.5.5",
52 | "express": "4.13.4",
53 | "express-session": "1.13.0",
54 | "mongoose": "4.4.4",
55 | "morgan": "1.7.0",
56 | "passport": "0.3.2",
57 | "passport-local": "1.0.0",
58 | "session-file-store": "0.0.24"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # iMsg
3 |
4 | > An iMessage-like web-based messaging client built with:
5 | * React
6 | * Redux
7 | * Deepstream.io
8 | * Express + Passport.js
9 |
10 | ## TODO
11 | See [TODO.md](TODO.md)
12 |
13 | ## Component Tree
14 | ```
15 | App
16 | |_ NewConversation
17 | |_ ViewConversation
18 | |_ Conversations
19 | |_ ConversationListHeader
20 | |_ ConversationListContainer (Container)
21 | | ConversationList
22 | |_ ConversationListItem
23 | |_ Conversation
24 | |_ ConversationHeader
25 | |_ ConversationThreadContainer (Container)
26 | |_ ConversationThread
27 | |_ HourMessageGroup?
28 | |_ MinuteMessageGroup?
29 | |_ Message
30 | |_ Avatar
31 | |_ AuthorLabel
32 | |_ MessageBody
33 | |_ MessageStatus
34 | |_ TypingIndicator
35 | |_ ConversationInput
36 | ```
37 |
38 | ## Records
39 | * User - `users/`
40 | * Conversation - `conversations/`
41 | * Message - `messages/`
42 | * Users (Online + Previously Connected) - `users`
43 | * User's Conversations - `users//conversations`
44 | * Conversation Messages - `conversations//messages`
45 |
46 | ## UI State
47 | * Selected Conversation
48 | * User's Conversations
49 | * Conversation Messages
50 | * Recipients List Visibility
51 | * Message Draft
52 | * Selected Recipients
53 | * Available Recipients
54 | * Body
55 |
56 |
57 | ## Dependencies
58 | ### Client-side
59 | As seen in [frontend-boilerplate](https://github.com/tj/frontend-boilerplate):
60 | - [x] [Webpack](https://webpack.github.io)
61 | - [x] [React](https://facebook.github.io/react/)
62 | - [x] [Redux](https://github.com/rackt/redux)
63 | - [x] [Babel](https://babeljs.io/)
64 | - [x] [Autoprefixer](https://github.com/postcss/autoprefixer)
65 | - [x] [PostCSS](https://github.com/postcss/postcss)
66 | - [x] [CSS modules](https://github.com/outpunk/postcss-modules)
67 | - [x] [Rucksack](http://simplaio.github.io/rucksack/docs)
68 | - [x] [React Router Redux](https://github.com/rackt/react-router-redux)
69 | - [x] [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension)
70 |
71 | ### Server-side
72 | - [x] [Deepstream.io](https://deepstream.io)
73 | - [x] [Express](http://expressjs.com/)
74 | - [x] [Passport](http://passportjs.org/)
75 |
76 | ## Setup
77 | ```
78 | $ npm install
79 | ```
80 |
81 | ## Running
82 | ```
83 | $ npm start
84 | ```
85 |
86 | # License
87 | MIT
88 |
--------------------------------------------------------------------------------
/src/server/auth.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var express = require('express');
3 | var app = express();
4 | var port = process.env.PORT || 8001;
5 | var mongoose = require('mongoose');
6 | var passport = require('passport');
7 | var cookieParser = require('cookie-parser');
8 | var cookieSession = require('cookie-session');
9 | var flash = require('connect-flash');
10 |
11 | var morgan = require('morgan');
12 | var bodyParser = require('body-parser');
13 | var session = require('express-session');
14 | var mongoStore = require('connect-mongo')(session);
15 | var passportConfigurer = require('../../config/passport');
16 | var cookieSecret = require('../../config/auth').cookieSecret;
17 |
18 | var authServer = {
19 | start: function () {
20 | var configDB = require('../../config/database.js');
21 |
22 | // configuration ===============================================================
23 | // Get Mongo credentials
24 | var databaseUrl = 'mongodb://' + configDB.host + '/' + configDB.name;
25 | mongoose.connect(databaseUrl); // connect to our database
26 |
27 | passportConfigurer(passport);
28 |
29 | // set up our express application
30 | app.use(morgan('dev')); // log every request to the console
31 | app.use(bodyParser()); // get information from html forms
32 |
33 | app.set('view engine', 'ejs'); // set up jade for templating
34 |
35 | // required for passport
36 | app.use(cookieParser());
37 | app.use(cookieSession({
38 | key: 'imsg-sess',
39 | secret: cookieSecret,
40 | httpOnly: false
41 | }));
42 | app.use(function (req, res, next) {
43 | console.log(req.session);
44 | console.log(req.cookies);
45 | console.log(req.signedCookies);
46 | next();
47 | });
48 |
49 | // use passport session
50 | app.use(passport.initialize());
51 | app.use(passport.session());
52 | app.use(flash()); // use connect-flash for flash messages stored in session
53 |
54 | // routes ======================================================================
55 | require('./routes.js')(app, passport); // load our routes and pass in our app and fully configured passport
56 |
57 | app.use(express.static(path.join(__dirname, '../../static')));
58 |
59 | // launch ======================================================================
60 | app.listen(port);
61 | console.log('Serving app on port ' + port);
62 | }
63 | };
64 |
65 | module.exports = authServer;
66 |
--------------------------------------------------------------------------------
/src/client/components/ConversationThread/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { connect } from 'react-redux'
4 | import Message from '../Message'
5 | import App from '../../containers/App'
6 | //import MessageStatus from '../MessageStatus'
7 | //import TypingIndicator from '../TypingIndicator'
8 | import style from './style.css'
9 |
10 | class ConversationThread extends Component {
11 | componentWillMount() {
12 | setTimeout(() => {
13 | this.threadContainer.scrollTop = this.threadList.scrollHeight;
14 | }, 1500)
15 | }
16 |
17 | componentWillReceiveProps(nextProps) {
18 | const { profile, selectedConversation } = nextProps
19 | if (profile.username && selectedConversation) {
20 | console.log("SUBSCRIBING TO: ", selectedConversation)
21 | this.conversationRecord = App.ds.record.getRecord(selectedConversation)
22 | this.conversationRecord.whenReady(() => {
23 | var messages = this.conversationRecord.get('messages')
24 | this.setState({ messages: messages })
25 | this.threadContainer.scrollTop = this.threadList.scrollHeight;
26 |
27 | this.conversationRecord.subscribe('messages', (messages) => {
28 | this.setState({ messages: messages })
29 | setTimeout(() => {
30 | this.threadContainer.scrollTop = this.threadList.scrollHeight;
31 | }, 100)
32 | })
33 | })
34 | }
35 | }
36 |
37 | componentWillUnmount() {
38 | if (this.conversationRecord) {
39 | this.conversationRecord.unsubscribe('messages')
40 | }
41 | }
42 |
43 | render() {
44 | const { newConversation, conversation, onSendMessage, onReceivedMessage, onReadMessage, onStartTyping } = this.props
45 |
46 | var messages = this.state ? this.state.messages : []
47 | if (!messages.length && conversation) {
48 | messages = conversation.messages
49 | }
50 | console.log('~~~ b', messages.length);
51 | var messageItems = []
52 | if (conversation && !newConversation) {
53 | for (let index in messages) {
54 | var message = messages[index]
55 | messageItems.push()
56 | }
57 | }
58 | return (
59 | this.threadContainer = c} className={style.threadContainer}>
60 |
this.paddingItem = c} className={style.listPadder}>
61 |
this.threadList = c}>{messageItems}
62 |
63 | )
64 | }
65 | }
66 |
67 | function mapStateToProps(state) {
68 | return {
69 | newConversation: state.newConversation,
70 | profile: state.profile,
71 | selectedConversation: state.selectedConversation
72 | }
73 | }
74 |
75 | export default connect(
76 | mapStateToProps
77 | )(ConversationThread)
78 |
--------------------------------------------------------------------------------
/src/client/containers/App/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { bindActionCreators } from 'redux'
4 | import { connect } from 'react-redux'
5 | import * as UsersActions from '../../actions/users'
6 | import style from './style.css'
7 | import deepstream from 'deepstream.io-client-js'
8 | import Cookie from 'js-cookie'
9 | import * as ProfileActions from '../../actions/profile'
10 | import * as ConversationActions from '../../actions/conversations'
11 |
12 | function containsObjectId(objId, list) {
13 | var i;
14 | for (i = 0; i < list.length; i++) {
15 | if (list[i] === objId) {
16 | return true;
17 | }
18 | }
19 |
20 | return false;
21 | }
22 |
23 | class App extends Component {
24 | componentWillMount() {
25 | const { dispatch } = this.props
26 | const sid = Cookie.get('imsg-sess')
27 | if (sid == undefined) {
28 | window.location = 'http://localhost:8001'
29 | return
30 | }
31 | App.ds.login({ sid: sid }, function (success, errorCode, userObj) {
32 | var userId = 'users/' + userObj.username
33 | console.log('<<< userId', userId)
34 | var userRecord = App.ds.record.getRecord(userId)
35 | userRecord.set(userObj)
36 | dispatch(ProfileActions.setUsername(userObj.username))
37 |
38 | var usersList = App.ds.record.getList('users')
39 | usersList.subscribe(function onUsersChange(updatedUsersList) {
40 | console.log('List of users is now', updatedUsersList)
41 |
42 | if (!containsObjectId(userId, updatedUsersList)) {
43 | usersList.addEntry(userId)
44 | }
45 | dispatch(UsersActions.updateUserList(updatedUsersList))
46 | })
47 | usersList.addEntry(userId)
48 |
49 | var conversationListId = userId + '/conversations'
50 | console.log('<<< conversationListId', conversationListId)
51 | var conversations = App.ds.record.getList(conversationListId)
52 | conversations.subscribe(function onConversationsChange(updatedConversations) {
53 | console.log( 'List of my conversations is now', updatedConversations )
54 | dispatch(ConversationActions.updateConversationList(updatedConversations))
55 |
56 | updatedConversations.forEach( function( id ) {
57 | var conversationRecord = App.ds.record.getRecord( id );
58 | conversationRecord.whenReady( function () {
59 | var conversationObj = conversationRecord.get()
60 | dispatch(ConversationActions.addConversation(conversationObj))
61 | });
62 | })
63 | })
64 | })
65 | }
66 |
67 | render() {
68 | const { children } = this.props
69 | return (
70 |
71 | {this.props.children}
72 |
73 | )
74 | }
75 | }
76 |
77 | if (!App.ds) {
78 | App.ds = deepstream('localhost:6020')
79 | }
80 |
81 | export default connect()(App)
82 |
--------------------------------------------------------------------------------
/src/client/components/ViewConversation/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { bindActionCreators } from 'redux'
4 | import { connect } from 'react-redux'
5 | import { browserHistory } from 'react-router'
6 |
7 | import Conversations from '../../components/Conversations'
8 | import Conversation from '../../components/Conversation'
9 | import * as SelectedConversationActions from '../../actions/selectedConversation'
10 | import * as NewConversationActions from '../../actions/newConversation'
11 | import * as DraftActions from '../../actions/draft'
12 | import App from '../../containers/App'
13 | import style from './style.css'
14 |
15 | class ViewConversation extends Component {
16 | componentWillMount() {
17 | const { params, conversationsById, newConversationActions} = this.props
18 | const selectedConversation = 'conversations/'+params.conversationId
19 | this.setState({
20 | selectedConversation: selectedConversation
21 | });
22 | newConversationActions.newConversation(false)
23 | }
24 |
25 | componentDidMount() {
26 | const { params, conversationsById} = this.props
27 | const selectedConversation = 'conversations/'+params.conversationId
28 | this.setState({
29 | selectedConversation: selectedConversation
30 | });
31 | }
32 |
33 | componentWillReceiveProps(nextProps) {
34 | const { actions, draftActions, conversationsById } = this.props
35 | const selectedConversation = 'conversations/'+nextProps.params.conversationId
36 | console.log('<<< SWITCHING TO: ', selectedConversation);
37 | actions.viewConversation(selectedConversation)
38 | this.setState({
39 | selectedConversation: selectedConversation
40 | });
41 | }
42 |
43 | render() {
44 | const { params, conversationsById } = this.props
45 | console.log('state', this.state.selectedConversation);
46 | const selectedConversation = this.state.selectedConversation
47 | const conversation = selectedConversation? conversationsById[selectedConversation] : undefined
48 | console.log('<<< RENDERING CONV: ', conversation);
49 | return (
50 |
51 |
52 |
53 |
54 | )
55 | }
56 | }
57 |
58 | function mapStateToProps(state) {
59 | var conversationsState = state.conversations
60 | return {
61 | selectedConversation: state.selectedConversation,
62 | conversationsById: conversationsState.conversationsById,
63 | conversations: conversationsState.conversations
64 | }
65 | }
66 |
67 | function mapDispatchToProps(dispatch) {
68 | return {
69 | actions: bindActionCreators(SelectedConversationActions, dispatch),
70 | draftActions: bindActionCreators(DraftActions, dispatch),
71 | newConversationActions: bindActionCreators(NewConversationActions, dispatch)
72 | }
73 | }
74 |
75 | export default connect(
76 | mapStateToProps,
77 | mapDispatchToProps
78 | )(ViewConversation)
79 |
--------------------------------------------------------------------------------
/src/client/containers/ConversationThreadContainer/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { bindActionCreators } from 'redux'
4 | import { connect } from 'react-redux'
5 |
6 | import ConversationHeader from '../../components/ConversationHeader'
7 | import ConversationThread from '../../components/ConversationThread'
8 | import MessageInput from '../../components/MessageInput'
9 | import MessageRecipients from '../../components/MessageRecipients'
10 | import RecipientsListContainer from '..//RecipientsListContainer'
11 | import * as ConversationActions from '../../actions/conversations'
12 | import * as DraftActions from '../../actions/draft'
13 | import style from './style.css'
14 |
15 | class ConversationThreadContainer extends Component {
16 | componentWillReceiveProps(nextProps) {
17 | const { newConversation, draftActions, profile, conversation, selectedConversation, conversationsById, showRecipientsList, actions } = nextProps
18 | var conv = conversationsById[selectedConversation] || conversation
19 | if (conv && conv.participants) {
20 | var filtered = conv.participants.filter(function (participant) {
21 | if (participant !== 'users/'+profile.username) {
22 | return true
23 | }
24 | return false
25 | })
26 | draftActions.clearDraft()
27 | if (!newConversation) {
28 | for (let userId of filtered) {
29 | console.log('RECIPIENT:', conv.id, userId);
30 | console.log(nextProps);
31 | draftActions.addRecipient(userId.split('/')[1])
32 | }
33 | }
34 | }
35 | }
36 |
37 | render() {
38 | const { conversation, selectedConversation, conversationsById, showRecipientsList, actions } = this.props
39 | console.log('SELECTED CONVERSATION: ', selectedConversation);
40 | var conv = conversationsById[selectedConversation] || conversation
41 | return (
42 |
43 |
44 |
45 |
46 |
52 |
53 |
54 | )
55 | }
56 | }
57 |
58 | function mapStateToProps(state) {
59 | var conversationsState = state.conversations
60 | return {
61 | newConversation: state.newConversation,
62 | profile: state.profile,
63 | selectedConversation: state.selectedConversation,
64 | conversationsById: conversationsState.conversationsById,
65 | showRecipientsList: state.recipientsList
66 | }
67 | }
68 |
69 | function mapDispatchToProps(dispatch) {
70 | return {
71 | actions: bindActionCreators(ConversationActions, dispatch),
72 | draftActions: bindActionCreators(DraftActions, dispatch)
73 | }
74 | }
75 |
76 | export default connect(
77 | mapStateToProps,
78 | mapDispatchToProps
79 | )(ConversationThreadContainer)
80 |
--------------------------------------------------------------------------------
/src/client/actions/draft.js:
--------------------------------------------------------------------------------
1 |
2 | import { createAction } from 'redux-actions'
3 | import { browserHistory } from 'react-router'
4 | import * as ConversationActions from './conversations'
5 | import App from '../containers/App'
6 |
7 | export const addRecipient = createAction('add recipient')
8 | export const removeRecipient = createAction('remove recipient')
9 | export const addMessageBody = createAction('add message body')
10 | export const clearDraft = createAction('new draft')
11 |
12 | export function sendDraft(body) {
13 | return function (dispatch, getState) {
14 | const { draft, profile, conversations } = getState()
15 |
16 | // Create message record
17 | var messageId = 'messages/' + App.ds.getUid()
18 | var messageRecord = App.ds.record.getRecord(messageId)
19 | var authorId = 'users/' + profile.username
20 | var messageObj = {
21 | id: messageId,
22 | author: authorId,
23 | body: body,
24 | time: Date.now()
25 | }
26 | messageRecord.set(messageObj)
27 |
28 | // Create conversation record
29 | var participants = [...draft.recipients, authorId].sort()
30 | var conversationId = 'conversations/' + new Buffer(JSON.stringify(participants)).toString('base64')
31 | var existingConversation = conversations.conversationsById[conversationId]
32 | var conversationRecord = App.ds.record.getRecord(conversationId)
33 | conversationRecord.whenReady(function () {
34 | var conv = conversationRecord.get()
35 | const isNewConv = !conv || !conv.messages || conv.messages.length == 0
36 | if (isNewConv) {
37 | // If new conversation, assign message array
38 | var conversationObj = {
39 | id: conversationId,
40 | participants: participants,
41 | lastMessage: body,
42 | messages: [messageId]
43 | }
44 | conversationRecord.set(conversationObj)
45 | } else {
46 | // Otherwise, append messageId
47 | var messagesList = [...conv.messages, messageId]
48 | conversationRecord.set('messages', messagesList)
49 | conversationRecord.set('lastMessage', body)
50 | }
51 |
52 | // Add conversation to participant conv lists
53 | for (let index in participants) {
54 | var listId = participants[index] + '/conversations'
55 | var participantConversationList = App.ds.record.getList(listId)
56 | participantConversationList.whenReady(function () {
57 | var entries = participantConversationList.getEntries()
58 | if (entries.indexOf(conversationId) == -1) {
59 | participantConversationList.addEntry(conversationId)
60 | }
61 | })
62 | }
63 |
64 | // Add message to store
65 | dispatch(ConversationActions.addMessage(Object.assign({
66 | conversationId: conversationId
67 | }, messageObj )))
68 |
69 | if (isNewConv) {
70 | // If new conversation, navigate to conversation
71 | browserHistory.push('/' + conversationId)
72 | }
73 | })
74 |
75 | dispatch(clearDraft())
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/client/containers/App/style.css:
--------------------------------------------------------------------------------
1 | /** Ultra Light */
2 | @font-face {
3 | font-family: "San Francisco";
4 | font-weight: 100;
5 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-ultralight-webfont.woff");
6 | }
7 |
8 | /** Thin */
9 | @font-face {
10 | font-family: "San Francisco";
11 | font-weight: 200;
12 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-thin-webfont.woff");
13 | }
14 |
15 | /** Regular */
16 | @font-face {
17 | font-family: "San Francisco";
18 | font-weight: 400;
19 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-regular-webfont.woff");
20 | }
21 |
22 | /** Medium */
23 | @font-face {
24 | font-family: "San Francisco";
25 | font-weight: 500;
26 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-medium-webfont.woff");
27 | }
28 |
29 | /** Semi Bold */
30 | @font-face {
31 | font-family: "San Francisco";
32 | font-weight: 600;
33 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-semibold-webfont.woff");
34 | }
35 |
36 | /** Bold */
37 | @font-face {
38 | font-family: "San Francisco";
39 | font-weight: 700;
40 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-bold-webfont.woff");
41 | }
42 |
43 |
44 | html,
45 | body {
46 | margin: 0;
47 | padding: 0;
48 | }
49 |
50 | button {
51 | margin: 0;
52 | padding: 0;
53 | border: 0;
54 | background: none;
55 | font-size: 100%;
56 | vertical-align: baseline;
57 | font-family: inherit;
58 | font-weight: inherit;
59 | color: inherit;
60 | appearance: none;
61 | font-smoothing: antialiased;
62 | }
63 |
64 | body {
65 | font: 14px 'San Francisco', 'Helvetica Neue', Helvetica, Arial, sans-serif;
66 | line-height: 1.4em;
67 | background: #fefefe;
68 | color: #4d4d4d;
69 | position: absolute;
70 | top: 0;
71 | left: 0;
72 | right: 0;
73 | bottom: 0;
74 | margin: 0;
75 | overflow: hidden;
76 | -webkit-font-smoothing: antialiased;
77 | -moz-font-smoothing: antialiased;
78 | -ms-font-smoothing: antialiased;
79 | font-smoothing: antialiased;
80 | }
81 |
82 | button,
83 | input[type="checkbox"] {
84 | outline: none;
85 | }
86 |
87 | .normal {
88 | background: #fff;
89 | margin: 200px 0 40px 0;
90 | position: relative;
91 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
92 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
93 | }
94 |
95 | .normal input::-webkit-input-placeholder {
96 | font-style: italic;
97 | font-weight: 300;
98 | color: #e6e6e6;
99 | }
100 |
101 | .normal input::-moz-placeholder {
102 | font-style: italic;
103 | font-weight: 300;
104 | color: #e6e6e6;
105 | }
106 |
107 | .normal input::input-placeholder {
108 | font-style: italic;
109 | font-weight: 300;
110 | color: #e6e6e6;
111 | }
112 |
113 | .normal h1 {
114 | position: absolute;
115 | top: -155px;
116 | width: 100%;
117 | font-size: 100px;
118 | font-weight: 100;
119 | text-align: center;
120 | color: rgba(175, 47, 47, 0.15);
121 | -webkit-text-rendering: optimizeLegibility;
122 | -moz-text-rendering: optimizeLegibility;
123 | -ms-text-rendering: optimizeLegibility;
124 | text-rendering: optimizeLegibility;
125 | }
126 |
--------------------------------------------------------------------------------
/config/passport.js:
--------------------------------------------------------------------------------
1 | var LocalStrategy = require('passport-local').Strategy;
2 | var User = require('../src/server/models/user');
3 |
4 | // expose this function to our app using module.exports
5 | module.exports = function(passport) {
6 |
7 | // =========================================================================
8 | // passport session setup ==================================================
9 | // =========================================================================
10 | // required for persistent login sessions
11 | // passport needs ability to serialize and unserialize users out of session
12 |
13 | // used to serialize the user for the session
14 | passport.serializeUser(function(user, done) {
15 | done(null, user.id);
16 | });
17 |
18 | // used to deserialize the user
19 | passport.deserializeUser(function(id, done) {
20 | User.findById(id, function(err, user) {
21 | done(err, user);
22 | });
23 | });
24 |
25 | // =========================================================================
26 | // LOCAL SIGNUP ============================================================
27 | // =========================================================================
28 | // we are using named strategies since we have one for login and one for signup
29 | // by default, if there was no name, it would just be called 'local'
30 |
31 | passport.use('local-signup', new LocalStrategy({
32 | // by default, local strategy uses username and password, we will override with email
33 | usernameField : 'username',
34 | passwordField : 'password',
35 | passReqToCallback : true // allows us to pass back the entire request to the callback
36 | },
37 | function(req, username, password, done) {
38 |
39 | // asynchronous
40 | // User.findOne wont fire unless data is sent back
41 | process.nextTick(function() {
42 |
43 | // find a user whose email is the same as the forms email
44 | // we are checking to see if the user trying to login already exists
45 | User.findOne({ 'local.username' : username }, function(err, user) {
46 | // if there are any errors, return the error
47 | if (err) {
48 | return done(err);
49 | }
50 |
51 | // check to see if theres already a user with that email
52 | if (user) {
53 | return done(null, false, req.flash('signupMessage', 'That username is already taken.'));
54 | } else {
55 |
56 | // if there is no user with that email
57 | // create the user
58 | var newUser = new User();
59 |
60 | // set the user's local credentials
61 | newUser.local.username = username;
62 | newUser.local.password = newUser.generateHash(password);
63 |
64 | // save the user
65 | newUser.save(function(err) {
66 | if (err) {
67 | throw err;
68 | }
69 | return done(null, newUser);
70 | });
71 | }
72 |
73 | });
74 |
75 | });
76 |
77 | }));
78 |
79 | // =========================================================================
80 | // LOCAL LOGIN =============================================================
81 | // =========================================================================
82 | // we are using named strategies since we have one for login and one for signup
83 | // by default, if there was no name, it would just be called 'local'
84 |
85 | passport.use('local-login', new LocalStrategy({
86 | usernameField : 'username',
87 | passwordField : 'password',
88 | passReqToCallback : true // allows us to pass back the entire request to the callback
89 | },
90 | function(req, username, password, done) { // callback with email and password from our form
91 | // find a user whose email is the same as the forms email
92 | // we are checking to see if the user trying to login already exists
93 | User.findOne({ 'local.username' : username }, function(err, user) {
94 | // if there are any errors, return the error before anything else
95 | if (err) {
96 | return done(err);
97 | }
98 |
99 | // if no user is found, return the message
100 | if (!user)
101 | return done(null, false, req.flash('loginMessage', 'No user found.')); // req.flash is the way to set flashdata using connect-flash
102 |
103 | // if the user is found but the password is wrong
104 | if (!user.validPassword(password))
105 | return done(null, false, req.flash('loginMessage', 'Oops! Wrong password.')); // create the loginMessage and save it to session as flashdata
106 |
107 | // all is well, return successful user
108 | return done(null, user);
109 | });
110 |
111 | }));
112 |
113 | };
114 |
--------------------------------------------------------------------------------