├── server
├── favicon.ico
├── utilities
│ ├── mysqlConnection.js
│ └── socket.js
├── middlewares
│ └── authentication.js
├── routes
│ ├── users.js
│ ├── index.js
│ ├── todos.js
│ └── authentication.js
├── config
│ ├── config.js
│ └── passport.js
├── package.json
├── readme.md
├── models
│ ├── users.js
│ └── todos.js
├── index.js
├── app.js
└── scripts
│ └── database_creation_script.js
├── .gitignore
├── client
├── index.js
├── components
│ ├── static_pages
│ │ ├── AboutUs.jsx
│ │ ├── SettingsPage.jsx
│ │ └── landing_page
│ │ │ ├── LandingPage.css
│ │ │ └── LandingPage.jsx
│ ├── todo_components
│ │ ├── AddTodo.jsx
│ │ ├── Footer.jsx
│ │ ├── TodoList.jsx
│ │ ├── TodoWidget.jsx
│ │ └── Todo.jsx
│ ├── Navbar.jsx
│ └── authentication
│ │ ├── LoginForm.jsx
│ │ └── SignUpForm.jsx
├── index.html
├── webpack.config.js
├── utilities
│ ├── ServerSocket.js
│ └── RegexValidators.js
├── package.json
├── reducers
│ ├── RootReducer.js
│ ├── ProfileReducer.js
│ ├── TodoReducer.js
│ └── AuthReducer.js
├── readme.md
├── actions
│ ├── ProfileActions.js
│ ├── TodoActions.js
│ └── AuthActions.js
└── containers
│ ├── MainLoginPage.jsx
│ ├── MainSignUpPage.jsx
│ ├── App.jsx
│ ├── Root.jsx
│ ├── UserProfilePage.jsx
│ └── Dashboard.jsx
├── docs
└── settingUpMySql.md
└── readme.md
/server/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aybmab/express-redux-sample/HEAD/server/favicon.ico
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Compiled binary addons (http://nodejs.org/api/addons.html)
6 | **/build
7 |
8 | # Dependency directory
9 | **/node_modules
10 |
11 | **/.DS_STORE
12 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-core/polyfill';
2 | import React from 'react';
3 | import Root from './containers/Root';
4 |
5 | let rootElement = document.body;
6 |
7 | React.render(
8 | ,
9 | rootElement
10 | );
11 |
--------------------------------------------------------------------------------
/client/components/static_pages/AboutUs.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | export default class AboutUsPage extends Component {
4 | render() {
5 | return (
6 |
7 |
About us
8 |
9 | );
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/components/static_pages/SettingsPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | export default class SettingsPage extends Component {
4 | render() {
5 | return (
6 |
7 |
Settings
8 |
not implemented yet
9 |
10 | );
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/server/utilities/mysqlConnection.js:
--------------------------------------------------------------------------------
1 | var mysql = require('mysql');
2 | var config = require('../config/config.js');
3 |
4 | // TODO figure out best way to be handling connections.
5 | var createConnection = function(){
6 | var connection = mysql.createConnection(config.mysqlParams);
7 | return connection;
8 | }
9 |
10 |
11 | module.exports = createConnection;
--------------------------------------------------------------------------------
/server/middlewares/authentication.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | isLoggedIn: function(req, res, next) {
4 | if (req.isAuthenticated()) return next();
5 | res.json({error: "Not signed in"});
6 | },
7 |
8 | isLoggedOut: function(req, res, next) {
9 | if (!req.isAuthenticated()) return next();
10 | res.json({error: "Already signed in"});
11 | }
12 |
13 | };
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/server/routes/users.js:
--------------------------------------------------------------------------------
1 | var authenticationMiddleware = require('../middlewares/authentication.js'); //todo apply when needed
2 | var userModel = require('../models/users.js');
3 |
4 | var setUserRoutes = function(router){
5 |
6 | router.get('/api/v1/users/:id',
7 | function (req, res) {
8 | var userId = req.params.id;
9 | userModel.getUserProfile(userId,
10 | function(result){
11 | return res.json(result);
12 | }
13 | );
14 | }
15 | );
16 |
17 | }
18 |
19 | module.exports = setUserRoutes;
20 |
--------------------------------------------------------------------------------
/client/components/todo_components/AddTodo.jsx:
--------------------------------------------------------------------------------
1 | import React, { findDOMNode, Component, PropTypes } from 'react';
2 |
3 | export default class AddTodo extends Component {
4 | render() {
5 | return (
6 |
7 |
8 | this.handleClick(e)}>
9 | Add
10 |
11 |
12 | );
13 | }
14 |
15 | handleClick(e) {
16 | const node = findDOMNode(this.refs.input);
17 | const text = node.value.trim();
18 | this.props.onAddClick(text);
19 | node.value = '';
20 | }
21 | }
22 |
23 | AddTodo.propTypes = {
24 | onAddClick: PropTypes.func.isRequired
25 | };
--------------------------------------------------------------------------------
/server/routes/index.js:
--------------------------------------------------------------------------------
1 | var router = require('express').Router();
2 | var path = require('path');
3 |
4 | // Rest API
5 | require(path.join(__dirname, './', 'todos'))(router);
6 | require(path.join(__dirname, './', 'users'))(router);
7 |
8 | // Homepage/Client
9 | router.get('/', function(req, res, next) {
10 | // res.sendFile(path.join(__dirname, '../', 'client', 'index.html'));
11 | res.sendFile(path.join(__dirname, '../', 'client', 'index.html'));
12 | });
13 |
14 |
15 |
16 | module.exports = function(app, passport) {
17 | // set authentication routes
18 | require('./authentication.js')(app, passport);
19 |
20 | // set other routes
21 | app.use('/', router);
22 | };
23 |
--------------------------------------------------------------------------------
/server/utilities/socket.js:
--------------------------------------------------------------------------------
1 |
2 | var socketio = require('socket.io');
3 | var todoModel = require('../models/todos');
4 | var io;
5 |
6 | module.exports = {
7 |
8 | setServer: function(server){
9 | io = socketio(server);
10 |
11 | io.on('connection', function(socket){
12 | console.log('a user connected');
13 |
14 | socket.on('viewing', function(){
15 | todoModel.getAllUniversalTodos(function(results){
16 | socket.emit("current-universal-todos", results);
17 | });
18 | });
19 |
20 | socket.on('disconnect', function(){
21 | console.log('user disconnected');
22 | });
23 | });
24 | },
25 |
26 | notifyAllListenersOfNewTodo: function(todo){
27 | io.emit('new-todo', todo);
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Sample App
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/client/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 |
3 | module.exports = {
4 | devtool: 'eval',
5 | entry: [
6 | "./index.js"
7 | ],
8 | output: {
9 | path: __dirname+"/build",
10 | filename: "main.js"
11 | },
12 | plugins: [
13 | new webpack.NoErrorsPlugin(),
14 | new webpack.HotModuleReplacementPlugin(),
15 | new webpack.NoErrorsPlugin()
16 | ],
17 | resolve: {
18 | extensions: ['', '.js', '.jsx']
19 | },
20 | module: {
21 | loaders: [
22 | { test: /\.js$/, loaders: ['react-hot', 'babel-loader'], exclude: /node_modules/ },
23 | { test: /\.jsx?$/, loaders: ['react-hot', 'babel-loader'], exclude: /node_modules/ },
24 | { test: /\.css$/, loader: "style!css" },
25 | { test: /\.svg$/, loader: "raw" }
26 | ]
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/server/routes/todos.js:
--------------------------------------------------------------------------------
1 | var sockets = require('../utilities/socket');
2 | var authenticationMiddleware = require('../middlewares/authentication.js'); //todo apply to every route starting with /api/v1
3 | var todoModel = require('../models/todos.js');
4 |
5 | var setTodoRoutes = function(router){
6 |
7 | router.post('/api/v1/todos/universal/addTodo', authenticationMiddleware.isLoggedIn,
8 | function(req, res) {
9 | todoModel.createUniversalTodo(req.body.text,
10 | req.body.clientUUID,
11 | req.user.id,
12 | function(result){
13 | sockets.notifyAllListenersOfNewTodo(result);
14 | return res.json(result);
15 | }
16 | );
17 | }
18 | );
19 |
20 | }
21 |
22 | module.exports = setTodoRoutes;
23 |
--------------------------------------------------------------------------------
/client/components/static_pages/landing_page/LandingPage.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | .landing-page-header-container {
4 | width: 100%;
5 | height: 230px;
6 | margin-top: -20px;
7 | background: #D0EBFF;
8 | text-align: center;
9 | }
10 |
11 | .landing-page-header-container-title {
12 | padding-top: 50px;
13 | font-size: 50px;
14 | }
15 |
16 | .landing-page-header-container-subtitle {
17 | font-size: 20px;
18 | }
19 |
20 |
21 | .landing-page-header-container-subtitle {
22 | font-size: 20px;
23 | }
24 |
25 | .landing-page-three-column-section {
26 |
27 | padding-top: 50px;
28 | }
29 |
30 |
31 | .landing-page-three-column-section .col-lg-4 {
32 | margin-bottom: 20px;
33 | text-align: center;
34 | }
35 | .landing-page-three-column-section h2 {
36 | font-weight: normal;
37 | }
38 | .landing-page-three-column-section .col-lg-4 p {
39 | margin-right: 10px;
40 | margin-left: 10px;
41 | }
42 |
--------------------------------------------------------------------------------
/server/config/config.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | module.exports = {
4 |
5 | mysqlParams : {
6 | socketPath : '/tmp/mysql.sock',
7 | user : "angel",
8 | password : "bunnies",
9 | database : "testmysql",
10 | multipleStatements : true // allows for multiple queries. consider making this a different connection?
11 | },
12 |
13 | sessionOptions : {
14 | secret: 'somesecretkeythatweshouldgenerateandstoresomewhere', //TODO make real secret
15 | saveUninitialized: true, // save new sessions
16 | resave: false, // do not automatically write to the session store
17 | cookie : {
18 | httpOnly: true,
19 | maxAge: 2419200000
20 | } // TODO set secure to true when https is used
21 | }
22 |
23 | };
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sampleapp",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "supervisor ./index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "babel-core": "^5.8.23",
14 | "bcryptjs": "^2.2.1",
15 | "body-parser": "^1.13.3",
16 | "connect-redis": "^2.4.1",
17 | "cookie-parser": "^1.3.5",
18 | "debug": "^2.2.0",
19 | "express": "^4.13.3",
20 | "express-session": "^1.11.3",
21 | "morgan": "^1.6.1",
22 | "mysql": "^2.8.0",
23 | "passport": "^0.2.2",
24 | "passport-local": "^1.0.0",
25 | "react": "^0.13.3",
26 | "react-router": "^0.13.3",
27 | "redux-logger": "^1.0.6",
28 | "redux-thunk": "^0.1.0",
29 | "serve-favicon": "^2.3.0",
30 | "socket.io": "^1.3.6"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/docs/settingUpMySql.md:
--------------------------------------------------------------------------------
1 | ```
2 | - Download from online (requires creating an Oracle account) and install.
3 | - Make sure you add the download to your path (PATH=$PATH:/usr/local/mysql/bin);
4 | - Add an account to your mysql server
5 | - run: mysql --user=root mysql
6 | - mysql> CREATE USER 'monty'@'localhost' IDENTIFIED BY 'some_pass';
7 | mysql> GRANT ALL PRIVILEGES ON *.* TO 'monty'@'localhost' WITH GRANT OPTION;
8 | - reference: http://dev.mysql.com/doc/refman/5.1/en/adding-users.html
9 | - In the project, update params in the config file:
10 | mysqlParams : {
11 | socketPath: '/tmp/mysql.sock', // run 'mysqladmin variables' and copy the 'socket' variable
12 | user : "angel", // whatever name you choose
13 | password : "bunnies", // whatever password you choose
14 | database : "testmysql" // I used "Sequel Pro" to create this database
15 | }
16 | ```
17 |
--------------------------------------------------------------------------------
/client/utilities/ServerSocket.js:
--------------------------------------------------------------------------------
1 | import { receivedAllUniversalTodos, optimisticUniversalAddSuccess } from '../actions/TodoActions';
2 |
3 | var socket = io();
4 |
5 | export function linkSocketToStore(dispatch) {
6 | // Add listeners that dispatch actions here.
7 | socket.on("current-universal-todos", function(result){
8 | // console.log("got all todos!", result);
9 | if(result.error){
10 | //TODO handle some how
11 | alert("something went wrong retrieving all todos");
12 | } else {
13 | dispatch(receivedAllUniversalTodos(result.todos));
14 | }
15 | });
16 |
17 | socket.on("new-todo", function(result){
18 | // console.log("got a new todo!", result);
19 | if(result.error){
20 | //Do nothing
21 | } else {
22 | dispatch(optimisticUniversalAddSuccess(result.todo));
23 | }
24 | });
25 |
26 |
27 | }
28 |
29 | // Add functions that emit socket events:
30 | export function registerToUniversalTodo() {
31 | socket.emit("viewing");
32 | }
33 |
--------------------------------------------------------------------------------
/client/utilities/RegexValidators.js:
--------------------------------------------------------------------------------
1 |
2 | var emailRegex = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
3 | var displayNameRegex = /^[a-zA-Z\-\_0-9]+$/; //alphanumerics, "-" and "_"
4 | var passwordRegex = /^[a-zA-Z0-9!@#$%^&*]{6,}$/; // At least 6 characters
5 |
6 | /**
7 | * Given a trimmed string, returns true if the string matches
8 | * a proper email format.
9 | */
10 | export function validateEmail(email) {
11 | return emailRegex.test(email);
12 | }
13 |
14 |
15 | /**
16 | * Given a trimmed string, returns true if the string contains at
17 | * least one valid character (alphanumerics)
18 | */
19 | export function validateDisplayName(displayName) {
20 | return displayNameRegex.test(displayName);
21 | }
22 |
23 |
24 | /**
25 | * Given a trimmed string, returns true if the string contains at
26 | * least 6 valid character (alphanumerics and !@#$%^&*)
27 | */
28 | export function validatePassword(password) {
29 | return passwordRegex.test(password);
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/client/components/todo_components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | export default class Footer extends Component {
4 | renderFilter(filter, name) {
5 | if (filter === this.props.filter) {
6 | return name;
7 | }
8 |
9 | return (
10 | {
11 | e.preventDefault();
12 | this.props.onFilterChange(filter);
13 | }}>
14 | {name}
15 |
16 | );
17 | }
18 |
19 | render() {
20 | return (
21 |
22 | Show:
23 | {' '}
24 | {this.renderFilter('Show_All', 'All')}
25 | {', '}
26 | {this.renderFilter('Show_Completed', 'Completed')}
27 | {', '}
28 | {this.renderFilter('Show_Active', 'Active')}
29 | .
30 |
31 | );
32 | }
33 | }
34 |
35 | Footer.propTypes = {
36 | onFilterChange: PropTypes.func.isRequired,
37 | filter: PropTypes.oneOf([
38 | 'Show_All',
39 | 'Show_Completed',
40 | 'Show_Active'
41 | ]).isRequired
42 | };
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sampleapp",
3 | "version": "0.0.1",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "webpack --progress --color --watch"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "babel-core": "^5.6.20",
14 | "babel-loader": "^5.3.1",
15 | "codemirror": "^5.4.0",
16 | "core-js": "^1.1.1",
17 | "css-loader": "^0.15.4",
18 | "fixed-data-table": "^0.4.1",
19 | "flux": "^2.0.3",
20 | "node-libs-browser": "^0.5.2",
21 | "raw-loader": "^0.5.1",
22 | "react": "^0.13.3",
23 | "react-hot-loader": "^1.2.8",
24 | "react-loader": "^1.4.0",
25 | "react-redux": "^0.9.0",
26 | "react-router": "1.0.0-beta3",
27 | "redux": "^1.0.1",
28 | "redux-logger": "^1.0.6",
29 | "redux-thunk": "^0.1.0",
30 | "style-loader": "^0.12.3",
31 | "webpack": "^1.10.1",
32 | "webpack-dev-server": "^1.10.1"
33 | },
34 | "devDependencies": {
35 | "babel-loader": "^5.3.2",
36 | "redux-devtools": "^1.0.2"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/client/reducers/RootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { updateUserInfo } from './AuthReducer';
3 | import { updateUniversalTodoList, updateUniversalUnsavedTodoList } from './TodoReducer';
4 | import { updateProfileData } from './ProfileReducer';
5 |
6 | // import { Add_Todo, Complete_Todo, Set_Visibility_Filter, VisibilityFilters } from '../actions/TodoActions';
7 | // const { Show_All } = VisibilityFilters;
8 |
9 |
10 | // function updateVisibilityFilter(visibilityFilterState = Show_All, action){
11 | // switch (action.type){
12 |
13 | // case Set_Visibility_Filter:
14 | // return action.filter;
15 |
16 | // default:
17 | // return visibilityFilterState;
18 | // }
19 | // }
20 |
21 | const RootReducer = combineReducers({
22 | // visibilityFilter: updateVisibilityFilter, //TODO implement or remove...
23 | universalTodos: updateUniversalTodoList,
24 | unsavedUniversalTodos: updateUniversalUnsavedTodoList,
25 | userAuthSession: updateUserInfo,
26 |
27 | //For viewing profiles.
28 | userProfileData: updateProfileData
29 |
30 | });
31 |
32 | export default RootReducer;
33 |
34 |
--------------------------------------------------------------------------------
/client/components/todo_components/TodoList.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import Todo from './Todo';
3 |
4 | export default class TodoList extends Component {
5 | render() {
6 | var todos = this.props.todos;
7 | var unsavedTodos = this.props.unsavedTodos;
8 | var todoElements = [];
9 |
10 | Object.keys(todos).forEach( (todoId) => {
11 | todoElements.push( );
15 | }
16 | );
17 | Object.keys(unsavedTodos).forEach( (todoId) => {
18 | todoElements.push( );
22 | }
23 | );
24 | todoElements.reverse();
25 |
26 | return (
27 |
30 | );
31 | }
32 | }
33 |
34 | TodoList.propTypes = {
35 | onTodoClick: PropTypes.func.isRequired,
36 | todos: PropTypes.object.isRequired
37 | };
--------------------------------------------------------------------------------
/client/components/todo_components/TodoWidget.jsx:
--------------------------------------------------------------------------------
1 | //TODO move "isLoggedIn logic" out
2 | import React, { Component, PropTypes } from 'react';
3 |
4 | import AddTodo from './AddTodo';
5 | import TodoList from './TodoList';
6 | import Footer from './Footer';
7 |
8 | export default class TodoWidget extends Component {
9 | componentDidMount() {
10 | this.props.fetchInitialData();
11 | }
12 |
13 | render() {
14 | return (
15 |
16 |
{this.props.title}
17 |
19 |
24 |
25 | );
26 | }
27 |
28 | }
29 |
30 | // Footer.propTypes = {
31 | // onFilterChange: PropTypes.func.isRequired,
32 | // filter: PropTypes.oneOf([
33 | // 'Show_All',
34 | // 'Show_Completed',
35 | // 'Show_Active'
36 | // ]).isRequired
37 | // };
38 |
39 |
40 | //TODO add footer back:
41 | //
44 |
45 |
--------------------------------------------------------------------------------
/client/readme.md:
--------------------------------------------------------------------------------
1 | # Client
2 |
3 |
4 | ```
5 | client
6 | |-- index.html // The main container for the client application
7 | |-- index.js // The entry point for running the client application
8 | |-- webpack.config.js // The configuration for webpack
9 | |-- actions/ // Redux Actions folders
10 | | ....
11 | |-- components/ // "Dumb components" - consist of pure React components
12 | | |-- static_pages/ // Static pages used in the application written in React
13 | | | ....
14 | | ....
15 | |-- containers/ // "Smart components" - React components that work with Redux
16 | | |-- Root.jsx // Creates the routes for the application
17 | | |-- App.jsx // The main container for the applicatin
18 | | ....
19 | |-- external/ // External/vendor/Non-npm libraries
20 | | ....
21 | |-- reducers/ // Redux reducers
22 | | |-- RootReducer.js // Combines all reducers
23 | | ....
24 | |-- utilities/ // Helper functions used throughout the application
25 | | ....
26 | ```
27 |
--------------------------------------------------------------------------------
/server/readme.md:
--------------------------------------------------------------------------------
1 | # Server
2 |
3 |
4 | ```
5 | server
6 | |-- index.js // The entry point for running the project application
7 | |-- app.js // Creates the Express application
8 | |-- config/ // Configuration files used to connect to different machines or set settings
9 | | |-- passport.js // Defines strategies for passport configuration
10 | | ....
11 | |-- middlewares/ // Express middleware that process requests before REST API routes
12 | | ....
13 | |-- models/ // Represents data and handles business logic for interacting with DB
14 | | ....
15 | |-- routes/ // REST API. A.k.a. "controllers"
16 | | |-- index.js // Handles importing and setting up all other routes
17 | | .....
18 | |-- scripts/ // Scripts used to set things up
19 | | |-- database_creation_script.js // Node script for re-creating database tables (deletes old data!)
20 | | ....
21 | |-- utilities/ // Helper functions used throughout the application
22 | | ....
23 | |-- vendor/ // Non-npm libraries
24 | ```
25 |
--------------------------------------------------------------------------------
/client/reducers/ProfileReducer.js:
--------------------------------------------------------------------------------
1 | import { Start_Fetching_User_Profile,
2 | Fetch_User_Profile_Success, Fetch_User_Profile_Fail,
3 | Reload_Profile_Page
4 | } from '../actions/ProfileActions';
5 |
6 | const defaultStartState = { profileLoaded: false, //true even if results failed
7 | isFetchingProfile: false,
8 | userData: null,
9 | error: null
10 | }
11 |
12 | export function updateProfileData(userProfileState = defaultStartState , action) {
13 |
14 | switch (action.type){
15 |
16 | case Start_Fetching_User_Profile:
17 | return Object.assign({}, defaultStartState,{isFetchingProfile: true });
18 |
19 | case Fetch_User_Profile_Success:
20 | return {
21 | profileLoaded: true,
22 | isFetchingProfile: false,
23 | userData: action.userData,
24 | error: null
25 | };
26 |
27 |
28 | case Fetch_User_Profile_Fail:
29 | return {
30 | profileLoaded: true,
31 | isFetchingProfile: false,
32 | userData: null,
33 | error: action.error
34 | };
35 |
36 | case Reload_Profile_Page:
37 | return Object.assign({}, defaultStartState);
38 |
39 | default:
40 | return userProfileState;
41 | }
42 | }
--------------------------------------------------------------------------------
/client/components/todo_components/Todo.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | export default class Todo extends Component {
4 | constructor(props) {
5 | super(props);
6 | this.transferToUserPage = this.transferToUserPage.bind(this);
7 | }
8 | transferToUserPage(){
9 | this.props.onClickUserName(this.props.creator.id)
10 | }
11 | render() {
12 | var content = this.props.text;
13 | var author = "@"+ this.props.creator.displayName;
14 | var savedStatus;
15 | if (!this.props.isSaved) {
16 | savedStatus = "(not saved yet)" ;
17 | } else if(this.props.failedToAdd) {
18 | savedStatus = "(failed to save)";
19 | }
20 |
21 | return (
22 |
28 | {content}
29 |
30 | - posted by: {author}
33 |
34 | {savedStatus}
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | Todo.propTypes = {
42 | onClick: PropTypes.func.isRequired,
43 | text: PropTypes.string.isRequired,
44 | completed: PropTypes.bool.isRequired
45 | };
--------------------------------------------------------------------------------
/client/actions/ProfileActions.js:
--------------------------------------------------------------------------------
1 | /*
2 | * action types
3 | */
4 | export const Start_Fetching_User_Profile = 'Start_Fetching_User_Profile';
5 | export const Fetch_User_Profile_Success = 'Fetch_User_Profile_Success';
6 | export const Fetch_User_Profile_Fail = 'Fetch_User_Profile_Fail';
7 | export const Reload_Profile_Page = 'Reload_Profile_Page';
8 |
9 |
10 | /*
11 | * other constants
12 | */
13 |
14 | /*
15 | * action creators
16 | */
17 |
18 | export function startFetchingUserProfile() {
19 | return { type: Start_Fetching_User_Profile };
20 | }
21 |
22 |
23 | export function fetchUserProfile(userId) {
24 | return (dispatch) => {
25 | dispatch(startFetchingUserProfile());
26 |
27 | $.ajax({
28 | type: 'GET',
29 | url: ('/api/v1/users/' + userId) })
30 | .done(function(data) {
31 | if (data.error){
32 | dispatch(fetchUserProfileFail(data.error));
33 | } else {
34 | dispatch(fetchUserProfileSuccess(data.userData));
35 | }
36 | })
37 | .fail(function(a,b,c,d) {
38 | console.log("GET '/api/v1/users/' has actual failure: ", a, b, c, d)
39 | dispatch(fetchUserProfileFail()); //TODO figure out what to pass
40 | });
41 | }
42 | }
43 |
44 | export function fetchUserProfileSuccess(userData) {
45 | return { type: Fetch_User_Profile_Success, userData };
46 | }
47 |
48 | export function fetchUserProfileFail(error) {
49 | return { type: Fetch_User_Profile_Fail, error };
50 | }
51 |
52 | export function reloadingProfilePage() {
53 | return { type: Reload_Profile_Page };
54 | }
55 |
56 |
57 |
--------------------------------------------------------------------------------
/server/routes/authentication.js:
--------------------------------------------------------------------------------
1 | // TODO create and use utility fuction that converts req.user to userObject
2 | var authenticationMiddleware = require('../middlewares/authentication.js');
3 |
4 |
5 | /**
6 | * Note: if user is already signed in, this will overwrite the previous session
7 | * on the client side
8 | */
9 | function addAuthRoute(app, passport, routePath, strategy) {
10 | app.post(routePath, function(req, res, next) {
11 | passport.authenticate(strategy, function(err, user, info) {
12 | if (err) { return next(err); }
13 | if (!user) { return res.json(info); }
14 | if (user) {
15 | req.logIn(user, function(err) {
16 | if (err) { return next(err); }
17 | return res.json(user);
18 | });
19 | }
20 | })(req, res, next);
21 | });
22 | }
23 |
24 | module.exports = function(app, passport) {
25 | addAuthRoute(app, passport, "/signup", "local-signup");
26 |
27 | addAuthRoute(app, passport, "/login", "local-login");
28 |
29 | app.post('/logout', authenticationMiddleware.isLoggedIn, function(req, res) {
30 | req.logout();
31 | req.session.destroy();
32 | return res.json('logged out :)');
33 | });
34 |
35 | app.post('/checkSession', function(req, res) {
36 | var isLoggedIn = req.isAuthenticated();
37 | if (isLoggedIn) return res.json({ isLoggedIn: isLoggedIn,
38 | userObject: { displayName: req.user.display_name,
39 | id:req.user.id,
40 | email:req.user.email
41 | }
42 | });
43 | return res.json({isLoggedIn: isLoggedIn});
44 | });
45 | };
46 |
--------------------------------------------------------------------------------
/client/containers/MainLoginPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Navigation } from 'react-router';
4 |
5 | import LoginForm from '../components/authentication/LoginForm';
6 | import { attemptLogin, navigatedAwayFromAuthFormPage } from '../actions/AuthActions';
7 |
8 | class MainLoginPage extends Component {
9 | constructor(props) {
10 | super(props);
11 | }
12 | transferToDashboardIfLoggedIn(){
13 | if (this.props.userAuthSession.isLoggedIn){
14 | this.context.router.transitionTo('/dash');
15 | }
16 | }
17 | componentWillMount() {
18 | this.transferToDashboardIfLoggedIn();
19 | }
20 | componentDidUpdate() {
21 | this.transferToDashboardIfLoggedIn();
22 | }
23 | componentWillUnmount() {
24 | this.props.dispatch(navigatedAwayFromAuthFormPage());
25 | }
26 | render() {
27 | const { dispatch, userAuthSession } = this.props;
28 | // TODO is fetching logged in status, show loader...
29 | return (
30 |
31 |
Login
32 | {
33 | dispatch(attemptLogin(formData.email, formData.password))
34 | }}
35 | isFetchingData={userAuthSession.fetchingAuthUpdate}
36 | serverError={userAuthSession.error} />
37 |
38 | );
39 | }
40 | }
41 |
42 | MainLoginPage.contextTypes = {
43 | router: PropTypes.object.isRequired
44 | };
45 |
46 |
47 | function select(state) {
48 | return {
49 | userAuthSession: state.userAuthSession
50 | };
51 | }
52 |
53 | export default connect(select)(MainLoginPage);
--------------------------------------------------------------------------------
/client/containers/MainSignUpPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Navigation } from 'react-router';
4 |
5 | import SignUpForm from '../components/authentication/SignUpForm';
6 | import { attemptSignUp, navigatedAwayFromAuthFormPage } from '../actions/AuthActions';
7 |
8 | class MainSignUpPage extends Component {
9 | constructor(props) {
10 | super(props);
11 | }
12 |
13 | transferToDashboardIfLoggedIn(){
14 | if (this.props.userAuthSession.isLoggedIn){
15 | this.context.router.transitionTo('/dash');
16 | }
17 | }
18 |
19 | componentWillMount() {
20 | this.transferToDashboardIfLoggedIn();
21 | }
22 |
23 | componentDidUpdate() {
24 | this.transferToDashboardIfLoggedIn();
25 | }
26 | componentWillUnmount() {
27 | this.props.dispatch(navigatedAwayFromAuthFormPage());
28 | }
29 |
30 | render() {
31 | const { dispatch, userAuthSession } = this.props;
32 | return (
33 |
34 |
Sign Up
35 | {
36 | dispatch(attemptSignUp(formData.email, formData.password, formData.displayName))
37 | }}
38 | isFetchingData={userAuthSession.fetchingAuthUpdate}
39 | serverError={userAuthSession.error} />
40 |
41 | );
42 | }
43 | }
44 |
45 | MainSignUpPage.contextTypes = {
46 | router: PropTypes.object.isRequired
47 | };
48 |
49 |
50 | function select(state) {
51 | return {
52 | userAuthSession: state.userAuthSession
53 | };
54 | }
55 |
56 | export default connect(select)(MainSignUpPage);
--------------------------------------------------------------------------------
/client/containers/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | //Actions
5 | import { attemptLogout } from '../actions/AuthActions';
6 |
7 | //Components
8 | import Navbar from '../components/Navbar';
9 | import SignUpForm from '../components/authentication/SignUpForm';
10 | import LoginForm from '../components/authentication/LoginForm';
11 | import LandingPage from '../components/static_pages/landing_page/LandingPage.jsx';
12 |
13 | class App extends Component {
14 | constructor(props) {
15 | super(props);
16 | }
17 |
18 | render() {
19 | // Injected by connect() call:
20 | const { dispatch, userAuthSession } = this.props;
21 |
22 | // Injected by React Router
23 | const { location, children } = this.props;
24 | const { pathname } = location;
25 |
26 | const value = pathname.substring(1);
27 |
28 | var content;
29 | var landingPage; //TODO move this to components/static_pages
30 | if (children === undefined){
31 | landingPage = ;
32 | }
33 |
34 | return (
35 |
36 | dispatch(attemptLogout())}/>
38 | { landingPage }
39 | { children }
40 | { content }
41 |
42 | );
43 | }
44 | }
45 |
46 | App.contextTypes = {
47 | router: PropTypes.object.isRequired
48 | };
49 |
50 | function select(state) {
51 | return {
52 | universalTodos: state.universalTodos,
53 | unsavedUniversalTodos: state.unsavedUniversalTodos,
54 | userAuthSession: state.userAuthSession
55 | };
56 | }
57 |
58 | // Wrap the component to inject dispatch and state into it
59 | export default connect(select)(App);
--------------------------------------------------------------------------------
/server/models/users.js:
--------------------------------------------------------------------------------
1 | var mysql = require('mysql');
2 | var dbConnectionCreator = require('../utilities/mysqlConnection.js');
3 |
4 | var userModel = {
5 |
6 | convertRowsToUserProfileObject: function(rows) {
7 | var todos = {};
8 | rows.forEach(function(todo){
9 | if(todo.todo_id !== null){
10 | todos[todo.todo_id] = todo.text;
11 | }
12 | });
13 |
14 | var userInfo = {
15 | id: rows[0].user_id,
16 | email: rows[0].email,
17 | displayName: rows[0].display_name
18 | }
19 | return {
20 | userInfo : userInfo,
21 | userCreatedTodos : todos
22 | };
23 | },
24 |
25 | getUserProfile : function(userId, callback) {
26 | var dbConnection = dbConnectionCreator();
27 | var getUserSettingsSqlString = constructGetUserProfileSqlString(userId);
28 | console.log("ANGEL: getting user details");
29 | dbConnection.query(getUserSettingsSqlString, function(error, results, fields){
30 | if (error) {
31 | dbConnection.destroy();
32 | console.log("error: ", error);
33 | return (callback({error: error}));
34 | } else if (results.length === 0) {
35 | return (callback({error: "User not found."}));
36 | } else {
37 | return (callback({userData: userModel.convertRowsToUserProfileObject(results)} ));
38 | }
39 | });
40 | }
41 | };
42 |
43 | function constructGetUserProfileSqlString(userId){
44 | var query = " SELECT users.id AS user_id, " +
45 | " users.email, "+
46 | " users.display_name, " +
47 | " universal_todos.id AS todo_id, " +
48 | " universal_todos.text " +
49 |
50 | " FROM users LEFT JOIN universal_todos " +
51 | " ON universal_todos.creator_id = users.id" +
52 | " WHERE users.id = " + mysql.escape(userId);
53 | return query;
54 | }
55 |
56 |
57 | module.exports = userModel;
--------------------------------------------------------------------------------
/client/reducers/TodoReducer.js:
--------------------------------------------------------------------------------
1 | import { Received_All_Universal_Todos,
2 | Optimistically_Add_Universal_Todo,
3 | Optimistic_Add_Universal_Success, Optimistic_Add_Universal_Fail } from '../actions/TodoActions';
4 |
5 |
6 | //TODO create function that flips loader for retrieving all todos
7 |
8 |
9 | export function updateUniversalTodoList(universalTodosState = {} , action) {
10 | switch (action.type){
11 |
12 | case Received_All_Universal_Todos:
13 | // console.log("Received_All_Universal_Todos with", action.todos);
14 | return Object.assign({}, action.todos);
15 |
16 | case Optimistic_Add_Universal_Success:
17 | var setToAdd = {};
18 | setToAdd[action.todo.id] = action.todo;
19 | return Object.assign({}, universalTodosState, setToAdd);
20 |
21 | default:
22 | return universalTodosState;
23 | }
24 | }
25 |
26 |
27 | export function updateUniversalUnsavedTodoList(universalUnsavedTodosState = {} , action) {
28 | switch (action.type){
29 |
30 | case Optimistically_Add_Universal_Todo:
31 | var todo = {
32 | clientUUID : action.clientUUID,
33 | text : action.text,
34 | completed: false,
35 | creator: action.user
36 | }
37 | var setToAdd = {};
38 | setToAdd[todo.clientUUID] = todo;
39 | return Object.assign({}, universalUnsavedTodosState, setToAdd);
40 |
41 | case Optimistic_Add_Universal_Success:
42 | var setToAdd = Object.assign({}, universalUnsavedTodosState);
43 | delete setToAdd[action.todo.clientUUID];
44 | return setToAdd;
45 |
46 | case Optimistic_Add_Universal_Fail:
47 | var setToAdd = Object.assign({}, universalUnsavedTodosState);
48 | setToAdd[action.todo.clientUUID].failedToAdd = true;
49 | return setToAdd;
50 |
51 | default:
52 | return universalUnsavedTodosState;
53 | }
54 | }
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 | var app = require('./app');
7 | var debug = require('debug')('sample-app:server');
8 | var http = require('http');
9 | var socket = require('./utilities/socket');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '6969');
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server and sockets.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | socket.setServer(server);
25 |
26 | /**
27 | * Listen on provided port, on all network interfaces.
28 | */
29 |
30 | server.listen(port);
31 | server.on('error', onError);
32 | server.on('listening', onListening);
33 |
34 | /**
35 | * Normalize a port into a number, string, or false.
36 | */
37 |
38 | function normalizePort(val) {
39 | var port = parseInt(val, 10);
40 |
41 | if (isNaN(port)) {
42 | // named pipe
43 | return val;
44 | }
45 |
46 | if (port >= 0) {
47 | // port number
48 | return port;
49 | }
50 |
51 | return false;
52 | }
53 |
54 | /**
55 | * Event listener for HTTP server "error" event.
56 | */
57 |
58 | function onError(error) {
59 | if (error.syscall !== 'listen') {
60 | throw error;
61 | }
62 |
63 | var bind = typeof port === 'string'
64 | ? 'Pipe ' + port
65 | : 'Port ' + port;
66 |
67 | // handle specific listen errors with friendly messages
68 | switch (error.code) {
69 | case 'EACCES':
70 | console.error(bind + ' requires elevated privileges');
71 | process.exit(1);
72 | break;
73 | case 'EADDRINUSE':
74 | console.error(bind + ' is already in use');
75 | process.exit(1);
76 | break;
77 | default:
78 | throw error;
79 | }
80 | }
81 |
82 | /**
83 | * Event listener for HTTP server "listening" event.
84 | */
85 |
86 | function onListening() {
87 | var addr = server.address();
88 | var bind = typeof addr === 'string'
89 | ? 'pipe ' + addr
90 | : 'port ' + addr.port;
91 | debug('Listening on ' + bind);
92 | }
93 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | var config = require('./config/config.js');
2 | var express = require('express');
3 | var session = require('express-session');
4 | var RedisStore = require('connect-redis')(session);
5 | var path = require('path');
6 | var favicon = require('serve-favicon');
7 | var logger = require('morgan');
8 | var cookieParser = require('cookie-parser');
9 | var bodyParser = require('body-parser');
10 | var passport = require('passport');
11 | var LocalStrategy = require('passport-local').Strategy;
12 | var app = express();
13 |
14 | app.use(favicon(path.join(__dirname, 'favicon.ico')));
15 | app.use(logger('dev'));
16 | app.use(bodyParser.json());
17 | app.use(bodyParser.urlencoded({ extended: false }));
18 | app.use(cookieParser(config.sessionOptions.secret));
19 | app.use(express.static(path.join(__dirname, './../client')));
20 |
21 | // Sessions/PassportJS/Authentication
22 | require('./config/passport.js')(passport);
23 | var sessionOptions = config.sessionOptions;
24 | // sessionOptions.store = new RedisStore(); // TODO set up redis store
25 | app.use(session(sessionOptions));
26 | app.use(passport.initialize());
27 | app.use(passport.session());
28 |
29 | // set routes
30 | require('./routes/index')(app, passport);
31 |
32 | // catch 404 and forward to error handler
33 | app.use(function(req, res, next) {
34 | var err = new Error('Not Found');
35 | err.status = 404;
36 | next(err);
37 | });
38 |
39 | // error handlers
40 |
41 | // development error handler
42 | // will print stacktrace
43 | if (app.get('env') === 'development') {
44 | app.use(function(err, req, res, next) {
45 | console.log(err.stack);
46 |
47 | res.status(err.status || 500);
48 | res.json('error in development', {
49 | message: err.stack,
50 | error: err
51 | });
52 | });
53 | }
54 |
55 | // production error handler
56 | // no stacktraces leaked to user
57 | app.use(function(err, req, res, next) {
58 | res.status(err.status || 500);
59 | res.json('error in production', {
60 | message: err.message,
61 | error: {}
62 | });
63 | });
64 |
65 |
66 | module.exports = app;
67 |
--------------------------------------------------------------------------------
/client/reducers/AuthReducer.js:
--------------------------------------------------------------------------------
1 | import { Clicked_SignUp, SignUp_Success, SignUp_Fail,
2 | Clicked_Login, Login_Success, Login_Fail,
3 | Started_Session_Check, Checked_Session_Status,
4 | Clicked_Logout, Logout_Success,
5 | Navigate_Away_From_Auth_Form } from '../actions/AuthActions';
6 |
7 | const defaultStartState = { isLoggedIn: false,
8 | fetchingAuthUpdate: false,
9 | userObject: null,
10 | error: null
11 | }
12 |
13 | export function updateUserInfo(userAuthState = defaultStartState , action) {
14 | switch (action.type){
15 |
16 | case Started_Session_Check:
17 | case Clicked_Login:
18 | case Clicked_SignUp:
19 | case Clicked_Logout:
20 | return Object.assign({}, userAuthState, {
21 | fetchingAuthUpdate: true
22 | });
23 |
24 | case Login_Success:
25 | case SignUp_Success:
26 | return Object.assign({}, userAuthState, {
27 | isLoggedIn: true,
28 | fetchingAuthUpdate: false,
29 | userObject: action.userObject,
30 | error: null
31 | });
32 |
33 | case Login_Fail:
34 | case SignUp_Fail:
35 | return Object.assign({}, userAuthState, {
36 | isLoggedIn: false,
37 | fetchingAuthUpdate: false,
38 | error: action.error
39 | });
40 |
41 | case Checked_Session_Status:
42 | if (action.result.isLoggedIn){
43 | return Object.assign({}, userAuthState, {
44 | isLoggedIn: true,
45 | fetchingAuthUpdate: false,
46 | userObject: action.result.userObject,
47 | error: null
48 | });
49 | }
50 | // set to default conditions
51 | // (ignore errors and let login/signup handle server errors)
52 | return Object.assign({}, defaultStartState);
53 |
54 | case Logout_Success:
55 | return Object.assign({}, defaultStartState);
56 |
57 | case Navigate_Away_From_Auth_Form:
58 | return Object.assign({}, userAuthState, {
59 | error: null
60 | });
61 |
62 | default:
63 | return userAuthState;
64 | }
65 | }
--------------------------------------------------------------------------------
/client/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link } from 'react-router';
4 |
5 | class UserDropdown extends Component {
6 | render() {
7 | return (
8 |
9 |
14 |
15 | My Account
16 |
17 |
18 |
19 | Settings
20 |
21 | Logout
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | export default class Navbar extends Component {
29 | render() {
30 | var todos;
31 | var loginTab;
32 | var signupTab;
33 | var userDropdown;
34 | if (this.props.userAuthSession.isLoggedIn) {
35 | todos = Todos ;
36 | userDropdown = ();
37 | } else {
38 | loginTab = Login ;
39 | signupTab = Sign Up ;
40 | }
41 |
42 | return (
43 |
44 |
45 |
46 |
47 | Sample App
48 |
49 |
50 |
51 |
52 |
53 | { todos }
54 |
55 | About Us
56 |
57 | { loginTab }
58 | { signupTab }
59 |
60 | { userDropdown }
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/client/containers/Root.jsx:
--------------------------------------------------------------------------------
1 | // Note: This is probably an overloaded class.... TODO move the store creation to index.js
2 | // Redux
3 | import thunkMiddleware from 'redux-thunk';
4 | import loggerMiddleware from 'redux-logger';
5 | import { createStore, applyMiddleware } from 'redux';
6 | import { Provider } from 'react-redux';
7 | import RootReducer from '../reducers/RootReducer';
8 | import { checkSessionStatus } from '../actions/AuthActions';
9 | import { linkSocketToStore, registerToUniversalTodo } from '../utilities/ServerSocket';
10 |
11 | // React + React Router
12 | import React, { Component } from 'react';
13 | import { Router, Route, Link } from 'react-router';
14 | import { history } from 'react-router/lib/HashHistory';
15 |
16 | // views (containers)
17 | import App from './App';
18 | import MainSignUpPage from './MainSignUpPage';
19 | import MainLoginPage from './MainLoginPage';
20 | import Dashboard from './Dashboard';
21 | import UserProfilePage from './UserProfilePage';
22 |
23 | // Static Pages
24 | import AboutUs from '../components/static_pages/AboutUs';
25 | import SettingsPage from '../components/static_pages/SettingsPage';
26 |
27 | // Set up store
28 | const createStoreWithMiddleware = applyMiddleware(
29 | thunkMiddleware // lets us dispatch() functions
30 | // loggerMiddleware // neat middleware that logs actions //TODO get a better logger
31 | )(createStore);
32 |
33 | const store = createStoreWithMiddleware(RootReducer);
34 | linkSocketToStore(store.dispatch);
35 | store.dispatch(checkSessionStatus());
36 |
37 |
38 | // Set up routes
39 | var routes = (
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 |
53 | export default class Root extends Component {
54 | render() {
55 | return (
56 |
57 | {() => }
58 |
59 | );
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/client/actions/TodoActions.js:
--------------------------------------------------------------------------------
1 | /*
2 | * action types
3 | */
4 | export const Received_All_Universal_Todos = 'Received_All_Universal_Todos';
5 |
6 | export const Add_Universal_Todo = 'Add_Universal_Todo';
7 | export const Optimistically_Add_Universal_Todo = 'Optimistically_Add_Universal_Todo';
8 | export const Optimistic_Add_Universal_Success = 'Optimistic_Add_Universal_Success';
9 | export const Optimistic_Add_Universal_Fail = 'Optimistic_Add_Universal_Fail';
10 |
11 |
12 |
13 | /*
14 | * other constants
15 | */
16 |
17 | /*
18 | * action creators
19 | */
20 |
21 | // TODO create action for when firing off initila fetch and use loader flag.
22 |
23 | export function receivedAllUniversalTodos(todos) {
24 | return { type: Received_All_Universal_Todos, todos };
25 | }
26 |
27 |
28 | export function optimisticallyAddUniversalTodo(text, clientUUID, user) {
29 | return { type: Optimistically_Add_Universal_Todo, text, clientUUID, user };
30 | }
31 |
32 | export function addUniversalTodo(text, clientUUID, user) {
33 | return (dispatch) => {
34 | dispatch(optimisticallyAddUniversalTodo(text, clientUUID, user));
35 |
36 | $.ajax({
37 | type: 'POST',
38 | url: '/api/v1/todos/universal/addTodo',
39 | data: {text, clientUUID} })
40 | .done(function(data) {
41 | if (data.error){
42 | console.log("add todo worked but error: ", data);
43 | dispatch(optimisticUniversalAddFail());
44 | } else {
45 | console.log("add todo success", data);
46 | dispatch(optimisticUniversalAddSuccess(data.todo));
47 | }
48 | })
49 | .fail(function(a,b,c,d) {
50 | console.log("actual failure: ", a, b, c, d)
51 | dispatch(optimisticUniversalAddFail()); //TODO figure out what to pass
52 | });
53 | }
54 | }
55 |
56 | export function optimisticUniversalAddSuccess(todo) {
57 | return { type: Optimistic_Add_Universal_Success, todo };
58 | }
59 |
60 | export function optimisticUniversalAddFail(error) {
61 | return { type: Optimistic_Add_Universal_Fail, error };
62 | }
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | //TODO actually use these
71 | export function completeTodo(index) {
72 | return { type: Complete_Todo, index };
73 | }
74 |
75 | export function setVisibilityFilter(filter) {
76 | return { type: Set_Visibility_Filter, filter };
77 | }
78 |
--------------------------------------------------------------------------------
/client/components/static_pages/landing_page/LandingPage.jsx:
--------------------------------------------------------------------------------
1 | import './LandingPage.css';
2 | import React, { Component, PropTypes } from 'react';
3 |
4 | class LandingPageHeader extends Component {
5 | render() {
6 | return (
7 |
8 |
9 | Sample App
10 |
11 |
12 | Here's one way to build an App!
13 |
14 |
);
15 | }
16 | }
17 |
18 | // TODO include option for button
19 | // i.e.
20 | // //
21 | //
22 | //
Heading
23 | //
Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
24 | //
View details »
25 | //
26 | // TODO props-isize the image
27 | class LandingPageColumn extends Component {
28 | render() {
29 | return (
30 |
31 |
32 |
{this.props.heading}
33 |
{this.props.text}
34 |
35 | );
36 | }
37 | }
38 |
39 | class LandingPageThreeColumnSection extends Component {
40 | render() {
41 | return (
42 |
43 |
44 |
45 |
50 |
55 |
60 |
61 |
62 |
63 |
64 | );
65 | }
66 | }
67 |
68 |
69 | export default class LandingPage extends Component {
70 | render() {
71 | return (
72 |
73 |
74 |
75 |
76 |
77 |
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/client/containers/UserProfilePage.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { fetchUserProfile, reloadingProfilePage } from '../actions/ProfileActions';
5 |
6 |
7 |
8 | class UserTodoList extends Component {
9 | render(){
10 | var todoElements;
11 |
12 | var todoObjects = this.props.todos;
13 |
14 | if(Object.keys(todoObjects).length === 0){
15 | todoElements = This user hasn't created any todos.
;
16 | } else {
17 | todoElements = [];
18 |
19 | Object.keys(todoObjects).forEach(function(todo, value){
20 | todoElements.push( {todoObjects[todo]} );
21 | });
22 | todoElements = ;
23 | }
24 |
25 | return(
26 |
27 | {todoElements}
28 |
29 | );
30 | }
31 | }
32 |
33 |
34 |
35 | class UserProfilePage extends Component {
36 | constructor(props, context) {
37 | super(props, context);
38 | this.routerWillLeave = this.routerWillLeave.bind(this);
39 | }
40 | componentWillMount() {
41 | this.props.dispatch(fetchUserProfile(this.props.params.id));
42 | this.context.router.addTransitionHook(this.routerWillLeave);
43 | }
44 |
45 | componentWillUpdate(){
46 | const { dispatch, userProfileData, params } = this.props;
47 | if(!userProfileData.profileLoaded && !userProfileData.isFetchingProfile){
48 | dispatch(fetchUserProfile(params.id));
49 | }
50 | }
51 |
52 | componentWillUnmount() {
53 | this.context.router.removeTransitionHook(this.routerWillLeave);
54 | }
55 |
56 | routerWillLeave (nextState, router) {
57 | this.props.dispatch(reloadingProfilePage());
58 | }
59 |
60 | render() {
61 | const { dispatch, userAuthSession, userProfileData } = this.props;
62 |
63 | var content;
64 | if (!userProfileData.profileLoaded){
65 | content = Loading
;
66 | }
67 | else if (userProfileData.error) {
68 | content = Sorry Something went wrong: {userProfileData.error}
;
69 | } else {
70 | content =
71 |
display name: {userProfileData.userData.userInfo.displayName}
72 |
email: {userProfileData.userData.userInfo.email}
73 |
;
74 | }
75 |
76 | var todoList;
77 | if (userProfileData !== undefined){
78 | if(userProfileData.userData !== null){
79 | if (userProfileData.userData.userCreatedTodos !== null){
80 | todoList = ;
81 | }
82 | }
83 | }
84 |
85 | return (
86 |
87 |
Profile Page
88 | User id: {this.props.params.id}
89 | {content}
90 | {todoList}
91 |
92 | );
93 | }
94 | }
95 |
96 | UserProfilePage.contextTypes = {
97 | router: PropTypes.object.isRequired
98 | };
99 |
100 |
101 | function select(state) {
102 | return {
103 | userAuthSession: state.userAuthSession,
104 | userProfileData: state.userProfileData,
105 | };
106 | }
107 |
108 | export default connect(select)(UserProfilePage);
--------------------------------------------------------------------------------
/server/models/todos.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Sample model
3 | */
4 | var mysql = require('mysql');
5 | var dbConnectionCreator = require('../utilities/mysqlConnection.js');
6 |
7 |
8 | var todoModel = {
9 |
10 | convertRowToObject: function(row) {
11 | // console.log("passed me row:", row);
12 | return {
13 | id: row.id,
14 | clientUUID: row.client_uuid,
15 | creator: {
16 | id: row.creator_id,
17 | email: row.creator_email,
18 | displayName: row.creator_display_name
19 | },
20 | text: row.text,
21 | complete: (row.completed === 1)
22 | };
23 | },
24 |
25 | createUniversalTodo : function(text, clientUUID, creatorId, callback) {
26 | var dbConnection = dbConnectionCreator();
27 | var createTodoSqlString = constructCreateUniversalTodoSqlString(text, clientUUID, creatorId);
28 | dbConnection.query(createTodoSqlString, function(error, results, fields){
29 | if (error) {
30 | dbConnection.destroy();
31 | // return res.json({error: error, when: "inserting"});
32 | return (callback({error: error, when: "inserting"}));
33 | } else {
34 | var getTodoSqlString = constructGetUniversalTodoSqlString(results.insertId);
35 | dbConnection.query(getTodoSqlString, function(error, results, fields){
36 | dbConnection.destroy();
37 | if (error) {
38 | return res.json({error: error, when: "reading"});
39 | return (callback({error: error, when: "reading"}));
40 | } else {
41 | // return res.json({todo: todoModel.convertRowToObject(results[0])} );
42 | return (callback({todo: todoModel.convertRowToObject(results[0])} ));
43 | }
44 | });
45 | }
46 | });
47 | },
48 |
49 | getAllUniversalTodos: function(callback){
50 | var dbConnection = dbConnectionCreator();
51 | var sqlString = createGetAllUniversalTodosSqlString();
52 | dbConnection.query(sqlString, function(error, results, fields){
53 | dbConnection.destroy();
54 | if (error) {
55 | return callback({error: error});
56 | } else {
57 | var todos = {};
58 | results.forEach(function(result){
59 | todos[result.id] = todoModel.convertRowToObject(result);
60 | });
61 | return callback({todos: todos });
62 | }
63 | });
64 | }
65 | };
66 |
67 | function constructCreateUniversalTodoSqlString(text, clientUUID, creatorId){
68 | var query = "INSERT INTO universal_todos SET " +
69 | " text = " + mysql.escape(text) +
70 | ", client_uuid = " + mysql.escape(clientUUID) +
71 | ", creator_id = " + mysql.escape(creatorId);
72 | return query;
73 | }
74 |
75 | function constructGetUniversalTodoSqlString(todoId){
76 | var query = " SELECT universal_todos.*, " +
77 | " users.id AS creator_id, "+
78 | " users.email AS creator_email, "+
79 | " users.display_name AS creator_display_name " +
80 |
81 | " FROM universal_todos LEFT JOIN users " +
82 | " ON universal_todos.creator_id = users.id" +
83 | " WHERE universal_todos.id = " + mysql.escape(todoId);
84 | return query;
85 | }
86 |
87 | function createGetAllUniversalTodosSqlString(){
88 | var query = " SELECT universal_todos.*, " +
89 | " users.id AS creator_id, "+
90 | " users.email AS creator_email, "+
91 | " users.display_name AS creator_display_name " +
92 |
93 | " FROM universal_todos LEFT JOIN users " +
94 | " ON universal_todos.creator_id = users.id";
95 | return query;
96 | }
97 |
98 |
99 | module.exports = todoModel;
--------------------------------------------------------------------------------
/server/scripts/database_creation_script.js:
--------------------------------------------------------------------------------
1 | //TODO IMPORTANT figure out how to make timestamps work with timezone changes
2 |
3 |
4 | //--------------------------------------------------------------------------------------------------------//
5 | //-------------------------------------------- query strings ---------------------------------------------//
6 | //--------------------------------------------------------------------------------------------------------//
7 |
8 |
9 | var createUserTableQuery = ""+
10 | "CREATE TABLE users ( " +
11 |
12 | " id INT UNSIGNED NOT NULL AUTO_INCREMENT, " +
13 | " display_name VARCHAR(100) NULL, " +
14 | " email VARCHAR(45) NULL, " +
15 | " password VARCHAR(255) NULL, " +
16 | " last_updated DATETIME DEFAULT CURRENT_TIMESTAMP, " +
17 | " date_created DATETIME DEFAULT CURRENT_TIMESTAMP, " +
18 |
19 | " PRIMARY KEY(id) " +
20 |
21 | ");";
22 |
23 | var createUniversalTodoListTableQuery = ""+
24 | "CREATE TABLE universal_todos ( " +
25 |
26 | " id INT UNSIGNED NOT NULL AUTO_INCREMENT, " +
27 | " client_uuid TEXT NOT NULL, " +
28 | " creator_id INT UNSIGNED NOT NULL REFERENCES users(id), " +
29 | " text TEXT , " +
30 | " completed BOOLEAN DEFAULT false, " +
31 | " date_completed DATETIME, " +
32 | " date_created DATETIME DEFAULT CURRENT_TIMESTAMP, " +
33 |
34 | " PRIMARY KEY(id) " +
35 |
36 | ");";
37 |
38 |
39 | var createPersonalTodoListTableQuery = ""+
40 | "CREATE TABLE personal_todos ( " +
41 |
42 | " id INT UNSIGNED NOT NULL AUTO_INCREMENT, " +
43 | " client_uuid TEXT NOT NULL, " +
44 | " creator_id INT UNSIGNED NOT NULL REFERENCES users(id), " +
45 | " text TEXT , " +
46 | " completed BOOLEAN DEFAULT false, " +
47 | " date_completed DATETIME, " +
48 | " date_created DATETIME DEFAULT CURRENT_TIMESTAMP, " +
49 |
50 | " PRIMARY KEY(id) " +
51 |
52 | ");";
53 |
54 |
55 | //-------------------------------------------- query strings ---------------------------------------------//
56 | //--------------------------------------------------------------------------------------------------------//
57 | //--------------------------------------------------------------------------------------------------------//
58 |
59 |
60 | var mysql = require('mysql');
61 | var dbConnectionCreator = require('../utilities/mysqlConnection.js');
62 |
63 | var dbConnection = dbConnectionCreator();
64 |
65 | var listOfActions = [ createDropFunction("users"), createTable("users", createUserTableQuery),
66 | createDropFunction("universal_todos"), createTable("universal_todos", createUniversalTodoListTableQuery),
67 | createDropFunction("personal_todos"), createTable("personal_todos", createPersonalTodoListTableQuery),
68 | endClientConnection];
69 | var currentIndex = 0;
70 |
71 | function runNextQueries(){
72 | var action = listOfActions[currentIndex++];
73 | action();
74 | }
75 |
76 | function createDropFunction(table){
77 | return function(){
78 | console.log("Dropping table: ", table);
79 | var query = "DROP TABLE IF EXISTS "+ table +" CASCADE";
80 | dbConnection.query(query, function(error,b,c,d){
81 | runNextQueries();
82 | });
83 | }
84 | }
85 |
86 | function createTable(tableName, query){
87 | return function(){
88 | console.log("Creating table: ", tableName);
89 | dbConnection.query(query, function(error,b,c,d){
90 | if (error) console.log("error: ", error);
91 | runNextQueries();
92 | });
93 | }
94 | }
95 |
96 | function endClientConnection(){
97 | console.log("done creating tables!");
98 | dbConnection.destroy();
99 | }
100 |
101 | runNextQueries(currentIndex);
102 |
103 |
104 |
--------------------------------------------------------------------------------
/client/containers/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Navigation } from 'react-router';
4 |
5 | import { registerToUniversalTodo } from '../utilities/ServerSocket';
6 |
7 | //Actions
8 | // import { addUniversalTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions/TodoActions';
9 | import { addUniversalTodo } from '../actions/TodoActions';
10 |
11 | import TodoWidget from '../components/todo_components/TodoWidget';
12 |
13 | //TODO put this in some utility class somewhere and import it
14 | function generateUUID(){
15 | //Note: this is a simple implentation for this project. //TODO create a better one
16 | return (Math.round(Math.random()*10000000000000000).toString()+(Date.now()));
17 | }
18 |
19 |
20 | class Dashboard extends Component {
21 | constructor(props) {
22 | super(props);
23 | }
24 |
25 | render() {
26 | const { dispatch, visibleTodos, visibilityFilter, userAuthSession } = this.props;
27 |
28 | var content;
29 |
30 | if (userAuthSession.isLoggedIn) {
31 | content = (
32 |
35 | dispatch(addUniversalTodo(text, generateUUID(),userAuthSession.userObject))
36 | }
37 |
38 | todos = {this.props.universalTodos}
39 | unsavedTodos = {this.props.unsavedUniversalTodos}
40 |
41 | onTodoClick = {index =>
42 | dispatch(completeTodo(index))
43 | }
44 | filter = {visibilityFilter}
45 | onFilterChange = {nextFilter =>
46 | dispatch(setVisibilityFilter(nextFilter))
47 | }
48 | onClickUserName = { userId =>
49 | this.context.router.transitionTo('/user/'+userId)
50 | }
51 |
52 | />
53 | );
54 | } else {
55 | content = ( Login or Signup to add to the list!
);
56 | }
57 |
58 | return (
59 |
60 |
Dashboard
61 | { content }
62 |
63 | );
64 | }
65 | }
66 |
67 | Dashboard.contextTypes = {
68 | router: PropTypes.object.isRequired
69 | };
70 |
71 | // App.propTypes = {
72 | // visibleTodos: PropTypes.arrayOf(PropTypes.shape({
73 | // text: PropTypes.string.isRequired,
74 | // completed: PropTypes.bool.isRequired
75 | // })),
76 | // visibilityFilter: PropTypes.oneOf([
77 | // 'Show_All',
78 | // 'Show_Completed',
79 | // 'Show_Active'
80 | // ]).isRequired
81 | // };
82 |
83 | // function selectTodos(todos, filter) {
84 | // switch (filter) {
85 | // case VisibilityFilters.Show_All:
86 | // return todos;
87 | // case VisibilityFilters.Show_Completed:
88 | // return todos.filter(todo => todo.completed);
89 | // case VisibilityFilters.Show_Active:
90 | // return todos.filter(todo => !todo.completed);
91 | // }
92 | // }
93 |
94 |
95 | // TODO move this to a lower level in the app.
96 | // Which props do we want to inject, given the global state?
97 | // Note: use https://github.com/faassen/reselect for better performance.
98 | function select(state) {
99 | return {
100 | // visibleTodos: selectTodos(state.todos, state.visibilityFilter), //TODO reimplement
101 | universalTodos: state.universalTodos,
102 | unsavedUniversalTodos: state.unsavedUniversalTodos,
103 | // visibilityFilter: state.visibilityFilter,
104 | userAuthSession: state.userAuthSession
105 | };
106 | }
107 |
108 | // Wrap the component to inject dispatch and state into it
109 | export default connect(select)(Dashboard);
--------------------------------------------------------------------------------
/client/actions/AuthActions.js:
--------------------------------------------------------------------------------
1 | /*
2 | * action types
3 | */
4 |
5 | export const Clicked_SignUp = 'Clicked_SignUp';
6 | export const SignUp_Success = 'SignUp_Success';
7 | export const SignUp_Fail = 'SignUp_Fail';
8 |
9 | export const Clicked_Login = 'Clicked_Login';
10 | export const Login_Success = 'Login_Success';
11 | export const Login_Fail = 'Login_Fail';
12 |
13 | export const Started_Session_Check = 'Started_Session_Check';
14 | export const Checked_Session_Status = 'Checked_Session_Status';
15 |
16 | export const Clicked_Logout = 'Clicked_Logout';
17 | export const Logout_Success = 'Logout_Success';
18 |
19 | // Note: Considered creating a new actions file for navigation
20 | // related actions. For now, will leave these here.
21 | export const Navigate_Away_From_Auth_Form = 'Navigate_Away_From_Auth_Form';
22 |
23 | /*
24 | * other constants
25 | */
26 |
27 |
28 | /*
29 | * action creators
30 | */
31 |
32 | export function clickedSignUp() {
33 | return { type: Clicked_SignUp }
34 | }
35 |
36 | export function signUpSuccess(userObject) {
37 | return { type: SignUp_Success, userObject };
38 | }
39 |
40 | export function signUpFail(error) {
41 | return { type: SignUp_Fail, error };
42 | }
43 |
44 | export function attemptSignUp(email, password, displayName) {
45 | return (dispatch) => {
46 | dispatch(clickedSignUp());
47 |
48 | $.ajax({
49 | type: 'POST',
50 | url: '/signup',
51 | data: {email, password, displayName} })
52 | .done(function(data) {
53 | if (data.error){
54 | dispatch(signUpFail(data.error));
55 | } else {
56 | dispatch(signUpSuccess(data));
57 | }
58 | })
59 | .fail(function(a,b,c,d) {
60 | // console.log('failed to signup',a,b,c,d);
61 | dispatch(signUpFail("TODO find the error..."));
62 | });
63 | }
64 | }
65 |
66 |
67 | export function clickedLogin() {
68 | return { type: Clicked_Login };
69 | }
70 |
71 | export function loginSuccess(userObject) {
72 | return { type: Login_Success, userObject };
73 | }
74 |
75 | export function loginFail(error) {
76 | return { type: Login_Fail, error };
77 | }
78 |
79 |
80 | export function attemptLogin(email, password) {
81 | return (dispatch) => {
82 | dispatch(clickedLogin());
83 |
84 | $.ajax({
85 | type: 'POST',
86 | url: '/login',
87 | data: {email, password} })
88 | .done(function(data) {
89 | if (data.error){
90 | dispatch(loginFail(data.error));
91 | } else {
92 | dispatch(loginSuccess(data));
93 | }
94 | })
95 | .fail(function(a,b,c,d) {
96 | // console.log('failed to login',a,b,c,d);
97 | dispatch(loginFail("TODO find the error..."));
98 | });
99 | }
100 | }
101 |
102 |
103 | export function startedSessionCheck() {
104 | return { type: Started_Session_Check };
105 | }
106 |
107 | export function checkedSessionStatus(result) {
108 | return { type: Checked_Session_Status, result };
109 | }
110 |
111 | export function checkSessionStatus(email, password) {
112 | return (dispatch) => {
113 | dispatch(startedSessionCheck());
114 |
115 | $.ajax({
116 | type: 'POST',
117 | url: '/checkSession',
118 | data: {} })
119 | .done(function(result) {
120 | dispatch(checkedSessionStatus(result));
121 | })
122 | .fail(function(a,b,c,d) {
123 | // console.log('failed to check',a,b,c,d);
124 | dispatch(checkedSessionStatus("TODO find the error..."));
125 | });
126 | }
127 | }
128 |
129 |
130 | export function clickedLogout() {
131 | return { type: Clicked_Logout };
132 | }
133 |
134 | export function logoutSuccess() {
135 | return { type: Logout_Success };
136 | }
137 |
138 | export function attemptLogout(){
139 | return (dispatch) => {
140 | dispatch(clickedLogout());
141 |
142 | $.ajax({
143 | type: 'POST',
144 | url: '/logout'})
145 | .done(function() {
146 | dispatch(logoutSuccess());
147 | })
148 | .fail(function() {
149 | // Not the redux way, but I think it's fair enough.
150 | alert("Can't log you out at the moment. Try again in a bit");
151 | });
152 | }
153 | }
154 |
155 |
156 | export function navigatedAwayFromAuthFormPage() {
157 | return { type: Navigate_Away_From_Auth_Form }
158 | }
159 |
--------------------------------------------------------------------------------
/server/config/passport.js:
--------------------------------------------------------------------------------
1 | // TODO design what to store in the session object other than just user.id
2 | // TODO see what happens if someone sends a request with an invalid session but actual id
3 | // - I assume the session store just rejects it or wipes it or something....
4 | // TODO create utility function that takes row and converts to userObject for client
5 |
6 | var LocalStrategy = require('passport-local').Strategy;
7 | var mysql = require('mysql');
8 | var bcrypt = require('bcryptjs');
9 | var dbConnection = require('../utilities/mysqlConnection.js')();
10 |
11 | module.exports = function(passport) {
12 |
13 | // =========================================================================
14 | // passport session setup ==================================================
15 | // =========================================================================
16 | // required for persistent login sessions
17 | // passport needs ability to serialize and unserialize users out of session
18 |
19 | // used to serialize the user for the session
20 | passport.serializeUser(function(user, done) {
21 | done(null, user.id);
22 | });
23 |
24 | // used to deserialize the user
25 | passport.deserializeUser(function(id, done) {
26 | dbConnection.query("SELECT * FROM users WHERE id = ? ",[id], function(err, rows){
27 | done(err, rows[0]);
28 | });
29 | });
30 |
31 | // =========================================================================
32 | // LOCAL SIGNUP ============================================================
33 | // =========================================================================
34 |
35 | passport.use(
36 | 'local-signup',
37 | new LocalStrategy({
38 | usernameField : 'email',
39 | passwordField : 'password',
40 | passReqToCallback : true // allows us to pass back the entire request to the callback
41 | },
42 | function(req, email, password, done) {
43 | var displayName = req.body.displayName;
44 | // find a user whose email is the same as the forms email
45 | // we are checking to see if the user trying to login already exists
46 | dbConnection.query("SELECT * FROM users WHERE email = ?",[email], function(err, rows) {
47 | if (err) {
48 | return done(err);
49 | }
50 | if (rows.length) {
51 | return done(null, false, { error: 'That email is already being used.' });
52 | } else {
53 | // if there is no user with that email
54 | // create the user
55 | var salt = bcrypt.genSaltSync(10);
56 | var passwordHash = bcrypt.hashSync(password, salt);
57 |
58 | var userInfo = {
59 | email: email,
60 | displayName: displayName
61 | };
62 |
63 | var insertQuery = "INSERT INTO users ( email, password, display_name) values (?,?,?)";
64 |
65 | dbConnection.query(insertQuery,[email, passwordHash, displayName],function(err, rows) {
66 | userInfo.id = rows.insertId;
67 | return done(null, userInfo);
68 | });
69 | }
70 | });
71 | })
72 | );
73 |
74 | // =========================================================================
75 | // LOCAL LOGIN =============================================================
76 | // =========================================================================
77 |
78 | passport.use(
79 | 'local-login',
80 | new LocalStrategy({
81 | usernameField : 'email',
82 | passwordField : 'password',
83 | passReqToCallback : true
84 | },
85 | function(req, email, password, done) {
86 | dbConnection.query("SELECT * FROM users WHERE email = ?",[email], function(err, rows){
87 | if (err){
88 | return done(err);
89 | }
90 | if (!rows.length) {
91 | console.log("no user found...");
92 | return done(null, false, {error: 'Email not found.'});
93 | }
94 | // if the user is found but the password is wrong
95 | if (!bcrypt.compareSync( password, rows[0].password)){
96 | return done(null, false, {error: 'Incorrect password.'});
97 | }
98 | // all is well, return successful user
99 | var userObject = { id: rows[0].id,
100 | email: rows[0].email,
101 | displayName: rows[0].display_name};
102 | return done(null, userObject);
103 | });
104 | })
105 | );
106 |
107 | };
108 |
109 |
110 |
--------------------------------------------------------------------------------
/client/components/authentication/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import { validateEmail, validateDisplayName, validatePassword } from '../../utilities/RegexValidators';
2 |
3 | import React, { Component, PropTypes } from 'react';
4 |
5 | const initialFormState = {
6 | errorMessage: null,
7 | isEmailFieldIncorrect : false,
8 | isPasswordFieldIncorrect : false
9 | };
10 |
11 | export default class LoginForm extends Component {
12 | constructor(props){
13 | super(props);
14 | this.state = Object.assign({}, initialFormState);
15 | this.handleOnClickLogin = this.handleOnClickLogin.bind(this);
16 | }
17 |
18 | getInputContainerClass(inputIncorrect){
19 | return ("form-group " + (inputIncorrect ? "has-error" : "") );
20 | }
21 |
22 | findErrorsInLoginForm(formData) {
23 | // Only finding one error at a time.
24 | let newState = Object.assign({}, initialFormState);
25 |
26 | // Checking email
27 | if (formData.email === "") {
28 | newState.errorMessage = "Email is required";
29 | newState.isEmailFieldIncorrect = true;
30 | }
31 | else if (!validateEmail(formData.email)) {
32 | newState.errorMessage = "Please enter a valid email address";
33 | newState.isEmailFieldIncorrect = true;
34 | }
35 | // Checking password
36 | else if (formData.password === "") {
37 | newState.errorMessage = "Password is required";
38 | newState.isPasswordFieldIncorrect = true;
39 | }
40 | else if(!validatePassword(formData.password)) {
41 | newState.errorMessage = "Passwords must contain at least 6 valid characters";
42 | newState.isPasswordFieldIncorrect = true;
43 | }
44 |
45 | return newState;
46 | }
47 |
48 | handleOnClickLogin(){
49 | var formData = {
50 | email : this.refs.email.getDOMNode().value.trim(),
51 | password : this.refs.password.getDOMNode().value.trim(),
52 | }
53 |
54 | let newState = this.findErrorsInLoginForm(formData);
55 | this.setState(newState);
56 | if (!newState.errorMessage){
57 | this.props.onClickLogin(formData);
58 | }
59 | }
60 |
61 | componentDidMount(){
62 | React.findDOMNode(this.refs.email).focus();
63 | }
64 |
65 | componentDidUpdate(){
66 | console.log(this.props.serverError);
67 | if(this.props.serverError === "Email not found."){ //TODO fix this - use constants
68 | if(!this.state.isEmailFieldIncorrect){
69 | let newState = Object.assign({}, this.state);
70 | newState.isEmailFieldIncorrect = true;
71 | this.setState(newState);
72 | }
73 | React.findDOMNode(this.refs.email).focus();
74 | }
75 | if(this.props.serverError === "Incorrect password."){ //TODO fix this - use constants
76 | if(!this.state.isPasswordFieldIncorrect){
77 | let newState = Object.assign({}, this.state);
78 | newState.isPasswordFieldIncorrect = true;
79 | this.setState(newState);
80 | }
81 | React.findDOMNode(this.refs.password).focus();
82 | }
83 | }
84 |
85 | render() {
86 | var loader; //TODO implement a better loader
87 | var errorLabel;
88 | if (this.props.isFetchingData){
89 | loader = loading
;
90 | }
91 | //TODO create a "FormErrorMessage" component
92 | if(this.state.errorMessage){
93 | errorLabel = (
94 |
95 | {this.state.errorMessage}
96 |
);
97 | }
98 | else if(this.props.serverError){
99 | errorLabel = (
100 |
101 | {this.props.serverError}
102 |
);
103 | }
104 | return (
105 |
106 | { loader }
107 | { errorLabel }
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
Login
116 |
117 | );
118 | }
119 | }
120 |
121 | LoginForm.propTypes = {
122 | onClickLogin: PropTypes.func.isRequired,
123 | isFetchingData: PropTypes.bool.isRequired,
124 | serverError: PropTypes.string
125 | };
126 |
127 |
128 |
--------------------------------------------------------------------------------
/client/components/authentication/SignUpForm.jsx:
--------------------------------------------------------------------------------
1 | import { validateEmail, validateDisplayName, validatePassword } from '../../utilities/RegexValidators';
2 |
3 | import React, { Component, PropTypes } from 'react';
4 |
5 | const initialFormState = {
6 | errorMessage: null,
7 | isDisplayNameFieldIncorrect : false,
8 | isEmailFieldIncorrect : false,
9 | isPasswordFieldIncorrect : false,
10 | isConfirmPasswordFieldIncorrect : false
11 | };
12 |
13 | export default class SignUpForm extends Component {
14 | constructor(props){
15 | super(props);
16 | this.state = Object.assign({}, initialFormState);
17 | this.handleOnClickSignUp = this.handleOnClickSignUp.bind(this);
18 | }
19 |
20 | getInputContainerClass(inputIncorrect){
21 | return ("form-group " + (inputIncorrect ? "has-error" : "") );
22 | }
23 |
24 | findErrorsInSignupForm(formData) {
25 | // Only finding one error at a time.
26 | let newState = Object.assign({}, initialFormState);
27 | // Checking display name
28 | if (formData.displayName === "") {
29 | newState.errorMessage = "A display name is required";
30 | newState.isDisplayNameFieldIncorrect = true;
31 | }
32 | else if (!validateDisplayName(formData.displayName)) {
33 | newState.errorMessage = "Please enter a valid display name containing alphanumerics, dashes (-), and/or underscores (_)";
34 | newState.isDisplayNameFieldIncorrect = true;
35 | }
36 | // Checking email
37 | else if (formData.email === "") {
38 | newState.errorMessage = "Email is required";
39 | newState.isEmailFieldIncorrect = true;
40 | }
41 | else if (!validateEmail(formData.email)) {
42 | newState.errorMessage = "Please enter a valid email address";
43 | newState.isEmailFieldIncorrect = true;
44 | }
45 | // Checking password
46 | else if (formData.password === "") {
47 | newState.errorMessage = "Password is required";
48 | newState.isPasswordFieldIncorrect = true;
49 | }
50 | else if(!validatePassword(formData.password)) {
51 | newState.errorMessage = "Your password must contain at least 6 valid characters";
52 | newState.isPasswordFieldIncorrect = true;
53 | }
54 | // Checking confirmed password
55 | else if (formData.confirmedPassword === "") {
56 | newState.errorMessage = "Please confirm your password";
57 | newState.isConfirmPasswordFieldIncorrect = true;
58 | }
59 | else if (formData.confirmedPassword !== formData.password) {
60 | newState.errorMessage = "The passwords don't match";
61 | newState.isConfirmPasswordFieldIncorrect = true;
62 | newState.isPasswordFieldIncorrect = true;
63 | }
64 |
65 | return newState;
66 | }
67 |
68 | handleOnClickSignUp(){
69 | var formData = {
70 | displayName : this.refs.displayName.getDOMNode().value.trim(),
71 | email : this.refs.email.getDOMNode().value.trim(),
72 | password : this.refs.password.getDOMNode().value.trim(),
73 | confirmedPassword : this.refs.confirmPassword.getDOMNode().value.trim()
74 | }
75 |
76 | let newState = this.findErrorsInSignupForm(formData);
77 | this.setState(newState);
78 | if (!newState.errorMessage){
79 | this.props.onClickSignUp(formData);
80 | }
81 | }
82 |
83 | componentDidMount(){
84 | React.findDOMNode(this.refs.displayName).focus();
85 | }
86 |
87 | componentDidUpdate(){
88 | if(this.props.serverError === "That email is already being used."){ //TODO fix this - use constants
89 | if(!this.state.isEmailFieldIncorrect){
90 | let newState = Object.assign({}, this.state);
91 | newState.isEmailFieldIncorrect = true;
92 | this.setState(newState);
93 | }
94 | React.findDOMNode(this.refs.email).focus();
95 | }
96 | }
97 |
98 | render() {
99 | var loader; //TODO implement a better loader
100 | var errorLabel;
101 | if (this.props.isFetchingData){
102 | loader = loading
;
103 | }
104 | //TODO create a "FormErrorMessage" component
105 | if(this.state.errorMessage){
106 | errorLabel = (
107 |
108 | {this.state.errorMessage}
109 |
);
110 | }
111 | else if(this.props.serverError){
112 | errorLabel = (
113 |
114 | {this.props.serverError}
115 |
);
116 | }
117 | return (
118 |
135 | );
136 | }
137 |
138 | }
139 |
140 | SignUpForm.propTypes = {
141 | onClickSignUp: PropTypes.func.isRequired,
142 | isFetchingData: PropTypes.bool.isRequired,
143 | serverError: PropTypes.string
144 | };
145 |
146 |
147 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Sample Express-Redux Application
2 |
3 | ## Purpose
4 |
5 | This is a sample project to help you bootstrap an entire web application from end to end!
6 |
7 | When trying to build an end-to-end system (client + server) from scratch, I couldn't find any examples that were complete, so I decided to put one together. I then decided to publish the sample project to share with others in hopes of:
8 |
9 | * Receiving feedback, fine tuning my example and learning more myself.
10 | * Helping others get insight into solving certain problems or using certain libraries.
11 |
12 | So with that, please reach out with suggestions for improvements or questions if something doesn't make sense or isn't clear!
13 |
14 | ## Table of Content
15 |
16 | 1. [Stack](https://github.com/aybmab/express-redux-sample#stack)
17 | 2. [The Sample Project](https://github.com/aybmab/express-redux-sample#the-sample-project)
18 | 3. [Setting Up](https://github.com/aybmab/express-redux-sample#setting-up)
19 | 4. [Todo List](https://github.com/aybmab/express-redux-sample#todo-list)
20 | 4. [Other Useful Things](https://github.com/aybmab/express-redux-sample#other-useful-things)
21 |
22 | ## Stack
23 |
24 | * [Express](http://expressjs.com/) - Node.js web application framework used for server. Sets up the REST API and handles communication with the database.
25 | * [Redux](http://rackt.github.io/redux/) - A state container for Javascript web applications derived from [Facebook's flux architecture](https://facebook.github.io/flux/docs/overview.html).
26 | * [React](http://facebook.github.io/react/) - A Javascript library for building UI components.
27 | * [React Router](http://rackt.github.io/react-router/) - A routing solution to handle routing on the client side.
28 | * [PassportJs](http://passportjs.org/) - Authentication middleware used to implement the user system.
29 | * [SocketIO](http://socket.io/) - Used to push updates to users via open sockets.
30 | * [MySql](https://www.mysql.com/) - Database (you could easily interchange this with another).
31 | * [webpack](https://webpack.github.io/) - A module bundler (like [Browserify](http://browserify.org/)).
32 |
33 | ## The Sample Project
34 |
35 | I started by taking [Redux's todo list example](http://rackt.github.io/redux/docs/basics/ExampleTodoList.html) and hooking it up with a backend (losing the ability to filter and mark items as complete in the process - to be re-implemented eventually). I wanted to mostly focus on the following:
36 |
37 | ### A Good System Architecture and Project Directory
38 |
39 | I spent some time coming up with a clean and organized structure for this implementation. I had two criteria for "good". The first was that anyone can easily understand what's going on. The second was that it'd be easy to implement new features. If either of those two weren't met, please let me know what could be done differently!
40 |
41 | On the highest level, I chose the classic client-server architecture - I wanted a clear separation between the two. I then looked for some inspiration on how to organize each directory (look at the readme's in each respective folder).
42 |
43 |
44 | ### REST API Server Using Node
45 |
46 | Using [Express](http://expressjs.com/), this was fairly straightforward.
47 |
48 | ### A User System
49 |
50 | I'm using [PassportJs](http://passportjs.org/) to implement a user system. Essentially, a session token is generated when a client connects, it is then associated with an account if the user successfully signs on and saved to a store (currently the dev session store, but soon to be redis - though it could also be saved in the DB). The token is then used to authorize subsequent requests.
51 |
52 | ### Redux
53 |
54 | Initially, I was using the flux architecture for the client side implementation, but then switched to redux. The idea is to have an immutable object that represents the state of the entire application. Everytime something happens, a new state object is created to reflect the change, and the views update accordingly. I definitely suggest reading up on redux and their examples [here](http://rackt.github.io/redux/).
55 |
56 | ### Optimistic Updates
57 |
58 | After having a redux application connected to a backend, I wanted to implement optimistic updates (a.k.a. reflect user updates immediately, even though the change wasn't necessarily saved). This was implemented by generating a unique id on the client side and then using that to reconcile after hearing back from the server. By using the client-side-generated id, react nicely handles updating the view and notifying the user on the status of each change.
59 |
60 | ### Live Updates/Push Notifications
61 |
62 | After users were able to make changes, I didn't want them to have to refresh their page to see changes made by other users. I used [SocketIO](http://socket.io/) to alert each client of any update. Please let me know what you think about this! I've never used backbone, but it seems to have a nice model and event system that could be worth exploring.
63 |
64 | ### Client Side Routing
65 |
66 | I refused to use Angular for this project (wanted to learn something new), but become worried when I started to think about client-side routing. I'm currently using the react router - the version which is still in beta and isn't properly documented yet. It works well enough to get the job done, but I still need to do my research. It's still not clear to me what the best way of passing variables down the hierarchy is when using the router.
67 |
68 | ## Setting up
69 |
70 | 1. Follow the steps in docs/settingUpMySql.md to set up mysql locally and create a DB user.
71 | 2. Run 'npm install' in both the /server and /client directory (I am treating both as different projects).
72 | 3. Run the database set up script using 'node /server/config/database_creation_script.js'. This will clear any tables and recreate them. Note: this is in place until we can come up with a better migration process.
73 |
74 | ## Running the project
75 |
76 | Note: There currently isn't a "one step" script to run the entire application, so you may need 2 terminals.
77 |
78 | After setting up...
79 |
80 | 1. Make sure that the myql server is running on your machine.
81 | 2. Run 'npm start' in the /client folder. This will compile the current client code using [webpack](https://webpack.github.io/) and continue to compile future changes. It's nice to keep an eye on this as you update the client project.
82 | 3. Run 'npm start' from the server folder. This will run the server using [supervisor](https://github.com/petruisfan/node-supervisor) and rerun on new server changes.
83 |
84 | ## Todo List
85 |
86 | Here is a list of things that I still need to implement/fix and/or learn about:
87 |
88 | * Testing (for all parts of the project...).
89 | * Finish implementing the todo example (allow users to mark items as completed and filter the todo list).
90 | * Implement pagination or infinite scroll.
91 | * Implement private todos (and then use this to make sure data can be kept private between users).
92 | * Figure out security holes within the current system.
93 | * Set up redis, specifically for the session store.
94 | * Params validation on the server side.
95 | * Write a script that handles all steps to running the project.
96 | * Add a loader/spinner (something like react-loader).
97 | * When forms fail on the server side, pass back which field failed (remove the current hack on the client side).
98 | * Write a more generic form class that other forms can inherit from.
99 | * FB login
100 | * Use connection pools rather than creating a new connection every time.
101 | * Prevent mysql from converting strings to ints when searching the database.
102 | * Use constants for erros.
103 |
104 | Message me if I'm missing anything or if you have a suggestion for how to do any of these!
105 |
106 | ### Other Useful Things
107 |
108 | 1. Download [SequelPro](http://www.sequelpro.com/) if you want a db query tool for your MySQL server.
109 | 2. Want friends to test you app running on localhost? Use [localtunnel](http://localtunnel.me/)
110 |
111 | Again, let me know if anything is work going on this list!
112 |
113 |
--------------------------------------------------------------------------------