├── diagrams
├── 01
│ ├── .gitkeep
│ └── diagrams.xml
├── 02
│ ├── .gitkeep
│ └── diagrams.xml
├── 03
│ ├── .gitkeep
│ └── diagrams.xml
└── 04
│ ├── .gitkeep
│ └── diagrams.xml
├── testing
├── .env
├── public
│ ├── favicon.ico
│ ├── manifest.json
│ └── index.html
├── src
│ ├── setupTests.js
│ ├── actions
│ │ ├── types.js
│ │ ├── __tests__
│ │ │ └── index.test.js
│ │ └── index.js
│ ├── reducers
│ │ ├── index.js
│ │ ├── auth.js
│ │ ├── comments.js
│ │ └── __tests__
│ │ │ └── comments.test.js
│ ├── index.js
│ ├── Root.js
│ ├── components
│ │ ├── __tests__
│ │ │ ├── App.test.js
│ │ │ ├── CommentList.test.js
│ │ │ └── CommentBox.test.js
│ │ ├── CommentList.js
│ │ ├── requireAuth.js
│ │ ├── CommentBox.js
│ │ └── App.js
│ └── __tests__
│ │ └── integrations.test.js
├── .gitignore
└── package.json
├── middleware
├── .env
├── public
│ ├── favicon.ico
│ ├── manifest.json
│ └── index.html
├── src
│ ├── setupTests.js
│ ├── actions
│ │ ├── types.js
│ │ ├── __tests__
│ │ │ └── index.test.js
│ │ └── index.js
│ ├── reducers
│ │ ├── index.js
│ │ ├── auth.js
│ │ ├── comments.js
│ │ └── __tests__
│ │ │ └── comments.test.js
│ ├── middlewares
│ │ ├── stateValidator.js
│ │ ├── async.js
│ │ └── stateSchema.js
│ ├── index.js
│ ├── Root.js
│ ├── components
│ │ ├── __tests__
│ │ │ ├── App.test.js
│ │ │ ├── CommentList.test.js
│ │ │ └── CommentBox.test.js
│ │ ├── CommentList.js
│ │ ├── requireAuth.js
│ │ ├── CommentBox.js
│ │ └── App.js
│ └── __tests__
│ │ └── integrations.test.js
├── .gitignore
└── package.json
├── .gitignore
├── auth
├── server
│ ├── .gitignore
│ ├── router.js
│ ├── package.json
│ ├── index.js
│ ├── models
│ │ └── user.js
│ ├── controllers
│ │ └── authentication.js
│ └── services
│ │ └── passport.js
└── client
│ ├── src
│ ├── actions
│ │ ├── types.js
│ │ └── index.js
│ ├── components
│ │ ├── Welcome.js
│ │ ├── HeaderStyle.css
│ │ ├── App.js
│ │ ├── Feature.js
│ │ ├── auth
│ │ │ ├── Signout.js
│ │ │ ├── Signin.js
│ │ │ └── Signup.js
│ │ ├── requireAuth.js
│ │ └── Header.js
│ ├── reducers
│ │ ├── index.js
│ │ └── auth.js
│ └── index.js
│ ├── public
│ ├── favicon.ico
│ ├── manifest.json
│ └── index.html
│ ├── .gitignore
│ └── package.json
├── README.md
└── LICENSE.md
/diagrams/01/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/diagrams/02/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/diagrams/03/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/diagrams/04/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/testing/.env:
--------------------------------------------------------------------------------
1 | NODE_PATH=src/
2 |
--------------------------------------------------------------------------------
/middleware/.env:
--------------------------------------------------------------------------------
1 | NODE_PATH=src/
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/auth/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | config.js
3 |
--------------------------------------------------------------------------------
/auth/client/src/actions/types.js:
--------------------------------------------------------------------------------
1 | export const AUTH_USER = 'auth_user';
2 | export const AUTH_ERROR = 'auth_error';
3 |
--------------------------------------------------------------------------------
/testing/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenGrider/AdvancedReduxCode/HEAD/testing/public/favicon.ico
--------------------------------------------------------------------------------
/auth/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenGrider/AdvancedReduxCode/HEAD/auth/client/public/favicon.ico
--------------------------------------------------------------------------------
/middleware/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StephenGrider/AdvancedReduxCode/HEAD/middleware/public/favicon.ico
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AdvancedReduxCode
2 |
3 | Code repository for an awesome course on React and Redux. See [here](https://www.udemy.com/react-redux-tutorial)
4 |
--------------------------------------------------------------------------------
/auth/client/src/components/Welcome.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () => {
4 | return
Welcome! Sign up or sign in! ;
5 | };
6 |
--------------------------------------------------------------------------------
/middleware/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | Enzyme.configure({ adapter: new Adapter() });
5 |
--------------------------------------------------------------------------------
/testing/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | Enzyme.configure({ adapter: new Adapter() });
5 |
--------------------------------------------------------------------------------
/auth/client/src/components/HeaderStyle.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | justify-content: space-between;
4 | }
5 |
6 | .header a {
7 | margin: 0 10px;
8 | }
9 |
--------------------------------------------------------------------------------
/middleware/src/actions/types.js:
--------------------------------------------------------------------------------
1 | export const SAVE_COMMENT = 'save_comment';
2 | export const FETCH_COMMENTS = 'fetch_comments';
3 | export const CHANGE_AUTH = 'change_auth';
4 |
--------------------------------------------------------------------------------
/testing/src/actions/types.js:
--------------------------------------------------------------------------------
1 | export const SAVE_COMMENT = 'save_comment';
2 | export const FETCH_COMMENTS = 'fetch_comments';
3 | export const CHANGE_AUTH = 'change_auth';
4 |
--------------------------------------------------------------------------------
/auth/client/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { reducer as formReducer } from 'redux-form';
3 | import auth from './auth';
4 |
5 | export default combineReducers({
6 | auth,
7 | form: formReducer
8 | });
9 |
--------------------------------------------------------------------------------
/auth/client/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from './Header';
3 |
4 | export default ({ children }) => {
5 | return (
6 |
7 |
8 | {children}
9 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/middleware/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import commentsReducer from 'reducers/comments';
3 | import authReducer from 'reducers/auth';
4 |
5 | export default combineReducers({
6 | comments: commentsReducer,
7 | auth: authReducer
8 | });
9 |
--------------------------------------------------------------------------------
/testing/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import commentsReducer from 'reducers/comments';
3 | import authReducer from 'reducers/auth';
4 |
5 | export default combineReducers({
6 | comments: commentsReducer,
7 | auth: authReducer
8 | });
9 |
--------------------------------------------------------------------------------
/middleware/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import { CHANGE_AUTH } from 'actions/types';
2 |
3 | export default function(state = false, action) {
4 | switch (action.type) {
5 | case CHANGE_AUTH:
6 | return action.payload;
7 | default:
8 | return state;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/testing/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import { CHANGE_AUTH } from 'actions/types';
2 |
3 | export default function(state = false, action) {
4 | switch (action.type) {
5 | case CHANGE_AUTH:
6 | return action.payload;
7 | default:
8 | return state;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/auth/client/src/components/Feature.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import requireAuth from './requireAuth';
3 |
4 | class Feature extends Component {
5 | render() {
6 | return This is the feature!
;
7 | }
8 | }
9 |
10 | export default requireAuth(Feature);
11 |
--------------------------------------------------------------------------------
/middleware/src/middlewares/stateValidator.js:
--------------------------------------------------------------------------------
1 | import tv4 from 'tv4';
2 | import stateSchema from './stateSchema';
3 |
4 | export default ({ dispatch, getState }) => next => action => {
5 | next(action);
6 |
7 | if (!tv4.validate(getState(), stateSchema)) {
8 | console.warn('Invalid state schema detected');
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/auth/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/middleware/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/testing/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/testing/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/auth/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/middleware/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/middleware/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter, Route } from 'react-router-dom';
4 | import Root from 'Root';
5 | import App from 'components/App';
6 |
7 | ReactDOM.render(
8 |
9 |
10 |
11 |
12 | ,
13 | document.querySelector('#root')
14 | );
15 |
--------------------------------------------------------------------------------
/testing/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter, Route } from 'react-router-dom';
4 | import Root from 'Root';
5 | import App from 'components/App';
6 |
7 | ReactDOM.render(
8 |
9 |
10 |
11 |
12 | ,
13 | document.querySelector('#root')
14 | );
15 |
--------------------------------------------------------------------------------
/auth/client/src/components/auth/Signout.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import * as actions from '../../actions';
4 |
5 | class Signout extends Component {
6 | componentDidMount() {
7 | this.props.signout();
8 | }
9 |
10 | render() {
11 | return Sorry to see you go
;
12 | }
13 | }
14 |
15 | export default connect(null, actions)(Signout);
16 |
--------------------------------------------------------------------------------
/testing/src/reducers/comments.js:
--------------------------------------------------------------------------------
1 | import { SAVE_COMMENT, FETCH_COMMENTS } from 'actions/types';
2 |
3 | export default function(state = [], action) {
4 | switch (action.type) {
5 | case SAVE_COMMENT:
6 | return [...state, action.payload];
7 | case FETCH_COMMENTS:
8 | const comments = action.payload.data.map(comment => comment.name);
9 | return [...state, ...comments];
10 | default:
11 | return state;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/middleware/src/reducers/comments.js:
--------------------------------------------------------------------------------
1 | import { SAVE_COMMENT, FETCH_COMMENTS } from 'actions/types';
2 |
3 | export default function(state = [], action) {
4 | switch (action.type) {
5 | case SAVE_COMMENT:
6 | return [...state, action.payload, {}];
7 | case FETCH_COMMENTS:
8 | const comments = action.payload.data.map(comment => comment.name);
9 | return [...state, ...comments];
10 | default:
11 | return state;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/testing/src/Root.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { createStore, applyMiddleware } from 'redux';
4 | import reduxPromise from 'redux-promise';
5 | import reducers from 'reducers';
6 |
7 | export default ({ children, initialState = {} }) => {
8 | const store = createStore(
9 | reducers,
10 | initialState,
11 | applyMiddleware(reduxPromise)
12 | );
13 |
14 | return {children} ;
15 | };
16 |
--------------------------------------------------------------------------------
/testing/src/actions/__tests__/index.test.js:
--------------------------------------------------------------------------------
1 | import { saveComment } from 'actions';
2 | import { SAVE_COMMENT } from 'actions/types';
3 |
4 | describe('saveComment', () => {
5 | it('has the correct type', () => {
6 | const action = saveComment();
7 |
8 | expect(action.type).toEqual(SAVE_COMMENT);
9 | });
10 |
11 | it('has the correct payload', () => {
12 | const action = saveComment('New Comment');
13 |
14 | expect(action.payload).toEqual('New Comment');
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/auth/client/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import { AUTH_USER, AUTH_ERROR } from '../actions/types';
2 |
3 | const INITIAL_STATE = {
4 | authenticated: '',
5 | errorMessage: ''
6 | };
7 |
8 | export default function(state = INITIAL_STATE, action) {
9 | switch (action.type) {
10 | case AUTH_USER:
11 | return { ...state, authenticated: action.payload };
12 | case AUTH_ERROR:
13 | return { ...state, errorMessage: action.payload };
14 | default:
15 | return state;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/middleware/src/actions/__tests__/index.test.js:
--------------------------------------------------------------------------------
1 | import { saveComment } from 'actions';
2 | import { SAVE_COMMENT } from 'actions/types';
3 |
4 | describe('saveComment', () => {
5 | it('has the correct type', () => {
6 | const action = saveComment();
7 |
8 | expect(action.type).toEqual(SAVE_COMMENT);
9 | });
10 |
11 | it('has the correct payload', () => {
12 | const action = saveComment('New Comment');
13 |
14 | expect(action.payload).toEqual('New Comment');
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/middleware/src/Root.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { createStore, applyMiddleware } from 'redux';
4 | import async from 'middlewares/async';
5 | import stateValidator from 'middlewares/stateValidator';
6 | import reducers from 'reducers';
7 |
8 | export default ({ children, initialState = {} }) => {
9 | const store = createStore(
10 | reducers,
11 | initialState,
12 | applyMiddleware(async, stateValidator)
13 | );
14 |
15 | return {children} ;
16 | };
17 |
--------------------------------------------------------------------------------
/middleware/src/components/__tests__/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import App from 'components/App';
4 | import CommentBox from 'components/CommentBox';
5 | import CommentList from 'components/CommentList';
6 |
7 | let wrapped;
8 |
9 | beforeEach(() => {
10 | wrapped = shallow( );
11 | });
12 |
13 | it('shows a comment box', () => {
14 | expect(wrapped.find(CommentBox).length).toEqual(1);
15 | });
16 |
17 | it('shows a comment list', () => {
18 | expect(wrapped.find(CommentList).length).toEqual(1);
19 | });
20 |
--------------------------------------------------------------------------------
/testing/src/components/__tests__/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import App from 'components/App';
4 | import CommentBox from 'components/CommentBox';
5 | import CommentList from 'components/CommentList';
6 |
7 | let wrapped;
8 |
9 | beforeEach(() => {
10 | wrapped = shallow( );
11 | });
12 |
13 | it('shows a comment box', () => {
14 | expect(wrapped.find(CommentBox).length).toEqual(1);
15 | });
16 |
17 | it('shows a comment list', () => {
18 | expect(wrapped.find(CommentList).length).toEqual(1);
19 | });
20 |
--------------------------------------------------------------------------------
/middleware/src/reducers/__tests__/comments.test.js:
--------------------------------------------------------------------------------
1 | import commentsReducer from 'reducers/comments';
2 | import { SAVE_COMMENT } from 'actions/types';
3 |
4 | it('handles actions of type SAVE_COMMENT', () => {
5 | const action = {
6 | type: SAVE_COMMENT,
7 | payload: 'New Comment'
8 | };
9 |
10 | const newState = commentsReducer([], action);
11 |
12 | expect(newState).toEqual(['New Comment']);
13 | });
14 |
15 | it('handles action with unknown type', () => {
16 | const newState = commentsReducer([], { type: 'LKAFDSJLKAFD' });
17 |
18 | expect(newState).toEqual([]);
19 | });
20 |
--------------------------------------------------------------------------------
/testing/src/reducers/__tests__/comments.test.js:
--------------------------------------------------------------------------------
1 | import commentsReducer from 'reducers/comments';
2 | import { SAVE_COMMENT } from 'actions/types';
3 |
4 | it('handles actions of type SAVE_COMMENT', () => {
5 | const action = {
6 | type: SAVE_COMMENT,
7 | payload: 'New Comment'
8 | };
9 |
10 | const newState = commentsReducer([], action);
11 |
12 | expect(newState).toEqual(['New Comment']);
13 | });
14 |
15 | it('handles action with unknown type', () => {
16 | const newState = commentsReducer([], { type: 'LKAFDSJLKAFD' });
17 |
18 | expect(newState).toEqual([]);
19 | });
20 |
--------------------------------------------------------------------------------
/auth/server/router.js:
--------------------------------------------------------------------------------
1 | const Authentication = require('./controllers/authentication');
2 | const passportService = require('./services/passport');
3 | const passport = require('passport');
4 |
5 | const requireAuth = passport.authenticate('jwt', { session: false });
6 | const requireSignin = passport.authenticate('local', { session: false });
7 |
8 | module.exports = function(app) {
9 | app.get('/', requireAuth, function(req, res) {
10 | res.send({ hi: 'there' });
11 | });
12 | app.post('/signin', requireSignin, Authentication.signin);
13 | app.post('/signup', Authentication.signup);
14 | }
15 |
--------------------------------------------------------------------------------
/auth/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.18.0",
7 | "react": "^16.3.2",
8 | "react-dom": "^16.3.2",
9 | "react-redux": "^5.0.7",
10 | "react-router-dom": "^4.2.2",
11 | "react-scripts": "1.1.4",
12 | "redux": "^4.0.0",
13 | "redux-form": "^7.3.0",
14 | "redux-thunk": "^2.2.0"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test --env=jsdom",
20 | "eject": "react-scripts eject"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/testing/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { SAVE_COMMENT, FETCH_COMMENTS, CHANGE_AUTH } from 'actions/types';
3 |
4 | export function saveComment(comment) {
5 | return {
6 | type: SAVE_COMMENT,
7 | payload: comment
8 | };
9 | }
10 |
11 | export function fetchComments() {
12 | const response = axios.get('http://jsonplaceholder.typicode.com/comments');
13 |
14 | return {
15 | type: FETCH_COMMENTS,
16 | payload: response
17 | };
18 | }
19 |
20 | export function changeAuth(isLoggedIn) {
21 | return {
22 | type: CHANGE_AUTH,
23 | payload: isLoggedIn
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/middleware/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { SAVE_COMMENT, FETCH_COMMENTS, CHANGE_AUTH } from 'actions/types';
3 |
4 | export function saveComment(comment) {
5 | return {
6 | type: SAVE_COMMENT,
7 | payload: comment
8 | };
9 | }
10 |
11 | export function fetchComments() {
12 | const response = axios.get('http://jsonplaceholder.typicode.com/comments');
13 |
14 | return {
15 | type: FETCH_COMMENTS,
16 | payload: response
17 | };
18 | }
19 |
20 | export function changeAuth(isLoggedIn) {
21 | return {
22 | type: CHANGE_AUTH,
23 | payload: isLoggedIn
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/auth/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "dev": "nodemon index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "bcrypt-nodejs": "*",
14 | "body-parser": "*",
15 | "cors": "^2.8.4",
16 | "express": "*",
17 | "jwt-simple": "*",
18 | "mongoose": "*",
19 | "morgan": "*",
20 | "nodemon": "*",
21 | "passport": "*",
22 | "passport-jwt": "*",
23 | "passport-local": "*"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/testing/src/components/CommentList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | class CommentList extends Component {
5 | renderComments() {
6 | return this.props.comments.map(comment => {
7 | return {comment} ;
8 | });
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
Comment List
15 |
16 |
17 | );
18 | }
19 | }
20 |
21 | function mapStateToProps(state) {
22 | return { comments: state.comments };
23 | }
24 |
25 | export default connect(mapStateToProps)(CommentList);
26 |
--------------------------------------------------------------------------------
/middleware/src/components/CommentList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | class CommentList extends Component {
5 | renderComments() {
6 | return this.props.comments.map(comment => {
7 | return {comment} ;
8 | });
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
Comment List
15 |
16 |
17 | );
18 | }
19 | }
20 |
21 | function mapStateToProps(state) {
22 | return { comments: state.comments };
23 | }
24 |
25 | export default connect(mapStateToProps)(CommentList);
26 |
--------------------------------------------------------------------------------
/testing/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testing",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.18.0",
7 | "enzyme": "^3.3.0",
8 | "enzyme-adapter-react-16": "^1.1.1",
9 | "moxios": "^0.4.0",
10 | "react": "^16.3.2",
11 | "react-dom": "^16.3.2",
12 | "react-redux": "^5.0.7",
13 | "react-router-dom": "^4.2.2",
14 | "react-scripts": "1.1.4",
15 | "redux": "^4.0.0",
16 | "redux-promise": "^0.6.0"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test --env=jsdom",
22 | "eject": "react-scripts eject"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/middleware/src/middlewares/async.js:
--------------------------------------------------------------------------------
1 | export default ({ dispatch }) => next => action => {
2 | // Check to see if the action
3 | // has a promise on its 'payload' property
4 | // If it does, then wait for it to resolve
5 | // If it doesn't, then send the action on to the
6 | // next middleware
7 | if (!action.payload || !action.payload.then) {
8 | return next(action);
9 | }
10 |
11 | // We want to wait for the promise to resolve
12 | // (get its data!!) and then create a new action
13 | // with that data and dispatch it
14 | action.payload.then(function(response) {
15 | const newAction = { ...action, payload: response };
16 | dispatch(newAction);
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/middleware/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testing",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.18.0",
7 | "enzyme": "^3.3.0",
8 | "enzyme-adapter-react-16": "^1.1.1",
9 | "moxios": "^0.4.0",
10 | "react": "^16.3.2",
11 | "react-dom": "^16.3.2",
12 | "react-redux": "^5.0.7",
13 | "react-router-dom": "^4.2.2",
14 | "react-scripts": "1.1.4",
15 | "redux": "^4.0.0",
16 | "redux-promise": "^0.6.0",
17 | "tv4": "^1.3.0"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test --env=jsdom",
23 | "eject": "react-scripts eject"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/middleware/src/middlewares/stateSchema.js:
--------------------------------------------------------------------------------
1 | export default {
2 | $id: 'http://example.com/example.json',
3 | type: 'object',
4 | definitions: {},
5 | $schema: 'http://json-schema.org/draft-07/schema#',
6 | properties: {
7 | comments: {
8 | $id: '/properties/comments',
9 | type: 'array',
10 | items: {
11 | $id: '/properties/comments/items',
12 | type: 'string',
13 | title: 'The 0th Schema ',
14 | default: '',
15 | examples: ['Comment #1', 'Comment #2']
16 | }
17 | },
18 | auth: {
19 | $id: '/properties/auth',
20 | type: 'boolean',
21 | title: 'The Auth Schema ',
22 | default: false,
23 | examples: [true]
24 | }
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/auth/server/index.js:
--------------------------------------------------------------------------------
1 | // Main starting point of the application
2 | const express = require('express');
3 | const http = require('http');
4 | const bodyParser = require('body-parser');
5 | const morgan = require('morgan');
6 | const app = express();
7 | const router = require('./router');
8 | const mongoose = require('mongoose');
9 | const cors = require('cors');
10 |
11 | // DB Setup
12 | mongoose.connect('mongodb://localhost/auth');
13 |
14 | // App Setup
15 | app.use(morgan('combined'));
16 | app.use(cors());
17 | app.use(bodyParser.json({ type: '*/*' }));
18 | router(app);
19 |
20 | // Server Setup
21 | const port = process.env.PORT || 3090;
22 | const server = http.createServer(app);
23 | server.listen(port);
24 | console.log('Server listening on:', port);
25 |
--------------------------------------------------------------------------------
/testing/src/components/__tests__/CommentList.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 |
4 | import CommentList from 'components/CommentList';
5 | import Root from 'Root';
6 |
7 | let wrapped;
8 |
9 | beforeEach(() => {
10 | const initialState = {
11 | comments: ['Comment 1', 'Comment 2']
12 | };
13 |
14 | wrapped = mount(
15 |
16 |
17 |
18 | );
19 | });
20 |
21 | it('creates one LI per comment', () => {
22 | expect(wrapped.find('li').length).toEqual(2);
23 | });
24 |
25 | it('shows the text for each comment', () => {
26 | expect(wrapped.render().text()).toContain('Comment 1');
27 | expect(wrapped.render().text()).toContain('Comment 2');
28 | });
29 |
--------------------------------------------------------------------------------
/middleware/src/components/__tests__/CommentList.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 |
4 | import CommentList from 'components/CommentList';
5 | import Root from 'Root';
6 |
7 | let wrapped;
8 |
9 | beforeEach(() => {
10 | const initialState = {
11 | comments: ['Comment 1', 'Comment 2']
12 | };
13 |
14 | wrapped = mount(
15 |
16 |
17 |
18 | );
19 | });
20 |
21 | it('creates one LI per comment', () => {
22 | expect(wrapped.find('li').length).toEqual(2);
23 | });
24 |
25 | it('shows the text for each comment', () => {
26 | expect(wrapped.render().text()).toContain('Comment 1');
27 | expect(wrapped.render().text()).toContain('Comment 2');
28 | });
29 |
--------------------------------------------------------------------------------
/testing/src/components/requireAuth.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | export default ChildComponent => {
5 | class ComposedComponent extends Component {
6 | // Our component just got rendered
7 | componentDidMount() {
8 | this.shouldNavigateAway();
9 | }
10 |
11 | // Our component just got updated
12 | componentDidUpdate() {
13 | this.shouldNavigateAway();
14 | }
15 |
16 | shouldNavigateAway() {
17 | if (!this.props.auth) {
18 | this.props.history.push('/');
19 | }
20 | }
21 |
22 | render() {
23 | return ;
24 | }
25 | }
26 |
27 | function mapStateToProps(state) {
28 | return { auth: state.auth };
29 | }
30 |
31 | return connect(mapStateToProps)(ComposedComponent);
32 | };
33 |
--------------------------------------------------------------------------------
/middleware/src/components/requireAuth.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | export default ChildComponent => {
5 | class ComposedComponent extends Component {
6 | // Our component just got rendered
7 | componentDidMount() {
8 | this.shouldNavigateAway();
9 | }
10 |
11 | // Our component just got updated
12 | componentDidUpdate() {
13 | this.shouldNavigateAway();
14 | }
15 |
16 | shouldNavigateAway() {
17 | if (!this.props.auth) {
18 | this.props.history.push('/');
19 | }
20 | }
21 |
22 | render() {
23 | return ;
24 | }
25 | }
26 |
27 | function mapStateToProps(state) {
28 | return { auth: state.auth };
29 | }
30 |
31 | return connect(mapStateToProps)(ComposedComponent);
32 | };
33 |
--------------------------------------------------------------------------------
/auth/client/src/components/requireAuth.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | export default ChildComponent => {
5 | class ComposedComponent extends Component {
6 | // Our component just got rendered
7 | componentDidMount() {
8 | this.shouldNavigateAway();
9 | }
10 |
11 | // Our component just got updated
12 | componentDidUpdate() {
13 | this.shouldNavigateAway();
14 | }
15 |
16 | shouldNavigateAway() {
17 | if (!this.props.auth) {
18 | this.props.history.push('/');
19 | }
20 | }
21 |
22 | render() {
23 | return ;
24 | }
25 | }
26 |
27 | function mapStateToProps(state) {
28 | return { auth: state.auth.authenticated };
29 | }
30 |
31 | return connect(mapStateToProps)(ComposedComponent);
32 | };
33 |
--------------------------------------------------------------------------------
/testing/src/__tests__/integrations.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import moxios from 'moxios';
4 | import Root from 'Root';
5 | import App from 'components/App';
6 |
7 | beforeEach(() => {
8 | moxios.install();
9 | moxios.stubRequest('http://jsonplaceholder.typicode.com/comments', {
10 | status: 200,
11 | response: [{ name: 'Fetched #1' }, { name: 'Fetched #2' }]
12 | });
13 | });
14 |
15 | afterEach(() => {
16 | moxios.uninstall();
17 | });
18 |
19 | it('can fetch a list of comments and display them', done => {
20 | const wrapped = mount(
21 |
22 |
23 |
24 | );
25 |
26 | wrapped.find('.fetch-comments').simulate('click');
27 |
28 | moxios.wait(() => {
29 | wrapped.update();
30 |
31 | expect(wrapped.find('li').length).toEqual(2);
32 |
33 | done();
34 | wrapped.unmount();
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/middleware/src/__tests__/integrations.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import moxios from 'moxios';
4 | import Root from 'Root';
5 | import App from 'components/App';
6 |
7 | beforeEach(() => {
8 | moxios.install();
9 | moxios.stubRequest('http://jsonplaceholder.typicode.com/comments', {
10 | status: 200,
11 | response: [{ name: 'Fetched #1' }, { name: 'Fetched #2' }]
12 | });
13 | });
14 |
15 | afterEach(() => {
16 | moxios.uninstall();
17 | });
18 |
19 | it('can fetch a list of comments and display them', done => {
20 | const wrapped = mount(
21 |
22 |
23 |
24 | );
25 |
26 | wrapped.find('.fetch-comments').simulate('click');
27 |
28 | moxios.wait(() => {
29 | wrapped.update();
30 |
31 | expect(wrapped.find('li').length).toEqual(2);
32 |
33 | done();
34 | wrapped.unmount();
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/auth/client/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { connect } from 'react-redux';
4 | import './HeaderStyle.css';
5 |
6 | class Header extends Component {
7 | renderLinks() {
8 | if (this.props.authenticated) {
9 | return (
10 |
11 | Sign Out
12 | Feature
13 |
14 | );
15 | } else {
16 | return (
17 |
18 | Sign Up
19 | Sign In
20 |
21 | );
22 | }
23 | }
24 |
25 | render() {
26 | return (
27 |
28 | Redux Auth
29 | {this.renderLinks()}
30 |
31 | );
32 | }
33 | }
34 |
35 | function mapStateToProps(state) {
36 | return { authenticated: state.auth.authenticated };
37 | }
38 |
39 | export default connect(mapStateToProps)(Header);
40 |
--------------------------------------------------------------------------------
/testing/src/components/CommentBox.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import * as actions from 'actions';
4 | import requireAuth from 'components/requireAuth';
5 |
6 | class CommentBox extends Component {
7 | state = { comment: '' };
8 |
9 | handleChange = event => {
10 | this.setState({ comment: event.target.value });
11 | };
12 |
13 | handleSubmit = event => {
14 | event.preventDefault();
15 |
16 | this.props.saveComment(this.state.comment);
17 | this.setState({ comment: '' });
18 | };
19 |
20 | render() {
21 | return (
22 |
23 |
30 |
31 | Fetch Comments
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | export default connect(null, actions)(requireAuth(CommentBox));
39 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Stephen Grider
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/middleware/src/components/CommentBox.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import * as actions from 'actions';
4 | import requireAuth from 'components/requireAuth';
5 |
6 | class CommentBox extends Component {
7 | state = { comment: '' };
8 |
9 | handleChange = event => {
10 | this.setState({ comment: event.target.value });
11 | };
12 |
13 | handleSubmit = event => {
14 | event.preventDefault();
15 |
16 | this.props.saveComment(this.state.comment);
17 | this.setState({ comment: '' });
18 | };
19 |
20 | render() {
21 | return (
22 |
23 |
24 | Add a Comment
25 |
26 |
27 | Submit Comment
28 |
29 |
30 |
31 | Fetch Comments
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | export default connect(null, actions)(requireAuth(CommentBox));
39 |
--------------------------------------------------------------------------------
/middleware/src/components/__tests__/CommentBox.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import CommentBox from 'components/CommentBox';
4 | import Root from 'Root';
5 |
6 | let wrapped;
7 |
8 | beforeEach(() => {
9 | wrapped = mount(
10 |
11 |
12 |
13 | );
14 | });
15 |
16 | afterEach(() => {
17 | wrapped.unmount();
18 | });
19 |
20 | it('has a text area and two buttons', () => {
21 | expect(wrapped.find('textarea').length).toEqual(1);
22 | expect(wrapped.find('button').length).toEqual(2);
23 | });
24 |
25 | describe('the text area', () => {
26 | beforeEach(() => {
27 | wrapped.find('textarea').simulate('change', {
28 | target: { value: 'new comment' }
29 | });
30 | wrapped.update();
31 | });
32 |
33 | it('has a text area that users can type in', () => {
34 | expect(wrapped.find('textarea').prop('value')).toEqual('new comment');
35 | });
36 |
37 | it('when form is submitted, text area gets emptied', () => {
38 | wrapped.find('form').simulate('submit');
39 | wrapped.update();
40 | expect(wrapped.find('textarea').prop('value')).toEqual('');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/testing/src/components/__tests__/CommentBox.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import CommentBox from 'components/CommentBox';
4 | import Root from 'Root';
5 |
6 | let wrapped;
7 |
8 | beforeEach(() => {
9 | wrapped = mount(
10 |
11 |
12 |
13 | );
14 | });
15 |
16 | afterEach(() => {
17 | wrapped.unmount();
18 | });
19 |
20 | it('has a text area and two buttons', () => {
21 | expect(wrapped.find('textarea').length).toEqual(1);
22 | expect(wrapped.find('button').length).toEqual(2);
23 | });
24 |
25 | describe('the text area', () => {
26 | beforeEach(() => {
27 | wrapped.find('textarea').simulate('change', {
28 | target: { value: 'new comment' }
29 | });
30 | wrapped.update();
31 | });
32 |
33 | it('has a text area that users can type in', () => {
34 | expect(wrapped.find('textarea').prop('value')).toEqual('new comment');
35 | });
36 |
37 | it('when form is submitted, text area gets emptied', () => {
38 | wrapped.find('form').simulate('submit');
39 | wrapped.update();
40 | expect(wrapped.find('textarea').prop('value')).toEqual('');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/auth/client/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { AUTH_USER, AUTH_ERROR } from './types';
3 |
4 | export const signup = (formProps, callback) => async dispatch => {
5 | try {
6 | const response = await axios.post(
7 | 'http://localhost:3090/signup',
8 | formProps
9 | );
10 |
11 | dispatch({ type: AUTH_USER, payload: response.data.token });
12 | localStorage.setItem('token', response.data.token);
13 | callback();
14 | } catch (e) {
15 | dispatch({ type: AUTH_ERROR, payload: 'Email in use' });
16 | }
17 | };
18 |
19 | export const signin = (formProps, callback) => async dispatch => {
20 | try {
21 | const response = await axios.post(
22 | 'http://localhost:3090/signin',
23 | formProps
24 | );
25 |
26 | dispatch({ type: AUTH_USER, payload: response.data.token });
27 | localStorage.setItem('token', response.data.token);
28 | callback();
29 | } catch (e) {
30 | dispatch({ type: AUTH_ERROR, payload: 'Invalid login credentials' });
31 | }
32 | };
33 |
34 | export const signout = () => {
35 | localStorage.removeItem('token');
36 |
37 | return {
38 | type: AUTH_USER,
39 | payload: ''
40 | };
41 | };
42 |
--------------------------------------------------------------------------------
/auth/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter, Route } from 'react-router-dom';
4 | import { Provider } from 'react-redux';
5 | import { createStore, applyMiddleware } from 'redux';
6 | import reduxThunk from 'redux-thunk';
7 |
8 | import reducers from './reducers';
9 | import App from './components/App';
10 | import Welcome from './components/Welcome';
11 | import Signup from './components/auth/Signup';
12 | import Feature from './components/Feature';
13 | import Signout from './components/auth/Signout';
14 | import Signin from './components/auth/Signin';
15 |
16 | const store = createStore(
17 | reducers,
18 | {
19 | auth: { authenticated: localStorage.getItem('token') }
20 | },
21 | applyMiddleware(reduxThunk)
22 | );
23 |
24 | ReactDOM.render(
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | ,
36 | document.querySelector('#root')
37 | );
38 |
--------------------------------------------------------------------------------
/testing/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Route, Link } from 'react-router-dom';
3 | import { connect } from 'react-redux';
4 | import CommentBox from 'components/CommentBox';
5 | import CommentList from 'components/CommentList';
6 | import * as actions from 'actions';
7 |
8 | class App extends Component {
9 | renderButton() {
10 | if (this.props.auth) {
11 | return (
12 | this.props.changeAuth(false)}>Sign Out
13 | );
14 | } else {
15 | return (
16 | this.props.changeAuth(true)}>Sign In
17 | );
18 | }
19 | }
20 |
21 | renderHeader() {
22 | return (
23 |
24 |
25 | Home
26 |
27 |
28 | Post A Comment
29 |
30 | {this.renderButton()}
31 |
32 | );
33 | }
34 |
35 | render() {
36 | return (
37 |
38 | {this.renderHeader()}
39 |
40 |
41 |
42 | );
43 | }
44 | }
45 |
46 | function mapStateToProps(state) {
47 | return { auth: state.auth };
48 | }
49 |
50 | export default connect(mapStateToProps, actions)(App);
51 |
--------------------------------------------------------------------------------
/auth/server/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 | const bcrypt = require('bcrypt-nodejs');
4 |
5 | // Define our model
6 | const userSchema = new Schema({
7 | email: { type: String, unique: true, lowercase: true },
8 | password: String
9 | });
10 |
11 | // On Save Hook, encrypt password
12 | // Before saving a model, run this function
13 | userSchema.pre('save', function(next) {
14 | // get access to the user model
15 | const user = this;
16 |
17 | // generate a salt then run callback
18 | bcrypt.genSalt(10, function(err, salt) {
19 | if (err) { return next(err); }
20 |
21 | // hash (encrypt) our password using the salt
22 | bcrypt.hash(user.password, salt, null, function(err, hash) {
23 | if (err) { return next(err); }
24 |
25 | // overwrite plain text password with encrypted password
26 | user.password = hash;
27 | next();
28 | });
29 | });
30 | });
31 |
32 | userSchema.methods.comparePassword = function(candidatePassword, callback) {
33 | bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
34 | if (err) { return callback(err); }
35 |
36 | callback(null, isMatch);
37 | });
38 | }
39 |
40 | // Create the model class
41 | const ModelClass = mongoose.model('user', userSchema);
42 |
43 | // Export the model
44 | module.exports = ModelClass;
45 |
--------------------------------------------------------------------------------
/middleware/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Route, Link } from 'react-router-dom';
3 | import { connect } from 'react-redux';
4 | import CommentBox from 'components/CommentBox';
5 | import CommentList from 'components/CommentList';
6 | import * as actions from 'actions';
7 |
8 | class App extends Component {
9 | renderButton() {
10 | if (this.props.auth) {
11 | return (
12 | this.props.changeAuth(false)}>Sign Out
13 | );
14 | } else {
15 | return (
16 | this.props.changeAuth(true)}>Sign In
17 | );
18 | }
19 | }
20 |
21 | renderHeader() {
22 | return (
23 |
24 |
25 | Home
26 |
27 |
28 | Post A Comment
29 |
30 | {this.renderButton()}
31 |
32 | );
33 | }
34 |
35 | render() {
36 | return (
37 |
38 | {this.renderHeader()}
39 |
40 |
41 |
42 | );
43 | }
44 | }
45 |
46 | function mapStateToProps(state) {
47 | return { auth: state.auth };
48 | }
49 |
50 | export default connect(mapStateToProps, actions)(App);
51 |
--------------------------------------------------------------------------------
/auth/client/src/components/auth/Signin.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { reduxForm, Field } from 'redux-form';
3 | import { compose } from 'redux';
4 | import { connect } from 'react-redux';
5 | import * as actions from '../../actions';
6 |
7 | class Signin extends Component {
8 | onSubmit = formProps => {
9 | this.props.signin(formProps, () => {
10 | this.props.history.push('/feature');
11 | });
12 | };
13 |
14 | render() {
15 | const { handleSubmit } = this.props;
16 |
17 | return (
18 |
19 |
20 | Email
21 |
27 |
28 |
29 | Password
30 |
36 |
37 | {this.props.errorMessage}
38 | Sign In!
39 |
40 | );
41 | }
42 | }
43 |
44 | function mapStateToProps(state) {
45 | return { errorMessage: state.auth.errorMessage };
46 | }
47 |
48 | export default compose(
49 | connect(mapStateToProps, actions),
50 | reduxForm({ form: 'signin' })
51 | )(Signin);
52 |
--------------------------------------------------------------------------------
/auth/client/src/components/auth/Signup.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { reduxForm, Field } from 'redux-form';
3 | import { compose } from 'redux';
4 | import { connect } from 'react-redux';
5 | import * as actions from '../../actions';
6 |
7 | class Signup extends Component {
8 | onSubmit = formProps => {
9 | this.props.signup(formProps, () => {
10 | this.props.history.push('/feature');
11 | });
12 | };
13 |
14 | render() {
15 | const { handleSubmit } = this.props;
16 |
17 | return (
18 |
19 |
20 | Email
21 |
27 |
28 |
29 | Password
30 |
36 |
37 | {this.props.errorMessage}
38 | Sign Up!
39 |
40 | );
41 | }
42 | }
43 |
44 | function mapStateToProps(state) {
45 | return { errorMessage: state.auth.errorMessage };
46 | }
47 |
48 | export default compose(
49 | connect(mapStateToProps, actions),
50 | reduxForm({ form: 'signup' })
51 | )(Signup);
52 |
--------------------------------------------------------------------------------
/auth/server/controllers/authentication.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jwt-simple');
2 | const User = require('../models/user');
3 | const config = require('../config');
4 |
5 | function tokenForUser(user) {
6 | const timestamp = new Date().getTime();
7 | return jwt.encode({ sub: user.id, iat: timestamp }, config.secret);
8 | }
9 |
10 | exports.signin = function(req, res, next) {
11 | // User has already had their email and password auth'd
12 | // We just need to give them a token
13 | res.send({ token: tokenForUser(req.user) });
14 | }
15 |
16 | exports.signup = function(req, res, next) {
17 | const email = req.body.email;
18 | const password = req.body.password;
19 |
20 | if (!email || !password) {
21 | return res.status(422).send({ error: 'You must provide email and password'});
22 | }
23 |
24 | // See if a user with the given email exists
25 | User.findOne({ email: email }, function(err, existingUser) {
26 | if (err) { return next(err); }
27 |
28 | // If a user with email does exist, return an error
29 | if (existingUser) {
30 | return res.status(422).send({ error: 'Email is in use' });
31 | }
32 |
33 | // If a user with email does NOT exist, create and save user record
34 | const user = new User({
35 | email: email,
36 | password: password
37 | });
38 |
39 | user.save(function(err) {
40 | if (err) { return next(err); }
41 |
42 | // Repond to request indicating the user was created
43 | res.json({ token: tokenForUser(user) });
44 | });
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/testing/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/auth/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/middleware/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/auth/server/services/passport.js:
--------------------------------------------------------------------------------
1 | const passport = require('passport');
2 | const User = require('../models/user');
3 | const config = require('../config');
4 | const JwtStrategy = require('passport-jwt').Strategy;
5 | const ExtractJwt = require('passport-jwt').ExtractJwt;
6 | const LocalStrategy = require('passport-local');
7 |
8 | // Create local strategy
9 | const localOptions = { usernameField: 'email' };
10 | const localLogin = new LocalStrategy(localOptions, function(email, password, done) {
11 | // Verify this email and password, call done with the user
12 | // if it is the correct email and password
13 | // otherwise, call done with false
14 | User.findOne({ email: email }, function(err, user) {
15 | if (err) { return done(err); }
16 | if (!user) { return done(null, false); }
17 |
18 | // compare passwords - is `password` equal to user.password?
19 | user.comparePassword(password, function(err, isMatch) {
20 | if (err) { return done(err); }
21 | if (!isMatch) { return done(null, false); }
22 |
23 | return done(null, user);
24 | });
25 | });
26 | });
27 |
28 | // Setup options for JWT Strategy
29 | const jwtOptions = {
30 | jwtFromRequest: ExtractJwt.fromHeader('authorization'),
31 | secretOrKey: config.secret
32 | };
33 |
34 | // Create JWT strategy
35 | const jwtLogin = new JwtStrategy(jwtOptions, function(payload, done) {
36 | // See if the user ID in the payload exists in our database
37 | // If it does, call 'done' with that other
38 | // otherwise, call done without a user object
39 | User.findById(payload.sub, function(err, user) {
40 | if (err) { return done(err, false); }
41 |
42 | if (user) {
43 | done(null, user);
44 | } else {
45 | done(null, false);
46 | }
47 | });
48 | });
49 |
50 | // Tell passport to use this strategy
51 | passport.use(jwtLogin);
52 | passport.use(localLogin);
53 |
--------------------------------------------------------------------------------
/diagrams/03/diagrams.xml:
--------------------------------------------------------------------------------
1 | 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
--------------------------------------------------------------------------------
/diagrams/04/diagrams.xml:
--------------------------------------------------------------------------------
1 | 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
--------------------------------------------------------------------------------
/diagrams/02/diagrams.xml:
--------------------------------------------------------------------------------
1 | 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
--------------------------------------------------------------------------------
/diagrams/01/diagrams.xml:
--------------------------------------------------------------------------------
1 | 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
--------------------------------------------------------------------------------