├── .babelrc ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── client ├── app │ ├── actions │ │ └── CounterActions.js │ ├── containers │ │ ├── App │ │ │ ├── App.js │ │ │ └── NotFound.js │ │ ├── Counters │ │ │ └── Counters.js │ │ ├── HelloWorld │ │ │ └── HelloWorld.js │ │ ├── Home │ │ │ └── Home.js │ │ └── SignIn │ │ │ └── SignInCallbackPage.js │ ├── index.js │ ├── reducers │ │ ├── CountersReducer.js │ │ └── index.js │ ├── store.js │ ├── styles │ │ ├── styles.scss │ │ └── vendor │ │ │ └── normalize.css │ └── utils │ │ ├── restapi.js │ │ └── storage.js └── public │ ├── assets │ └── img │ │ └── logo.png │ └── index.html ├── config ├── config.example.js ├── helpers.js ├── tables │ ├── create-fruits-table.json │ ├── create-user-sessions-table.json │ ├── create-users-table.json │ └── structure.md ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js ├── package.json ├── postcss.config.js ├── server.js ├── server ├── routes │ ├── api │ │ ├── fruits.js │ │ └── signin.js │ └── index.js ├── server.js └── utils │ └── unique.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,css,scss,html}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | dist/ 3 | 4 | # Config file 5 | config/config.js 6 | 7 | # Node stuff 8 | node_modules 9 | .npm 10 | .node_repl_history 11 | .lock-wscript 12 | logs 13 | *.log 14 | npm-debug.log* 15 | 16 | # OS stuff 17 | Desktop.ini 18 | ehthumbs.db 19 | Thumbs.db 20 | $RECYCLE.BIN/ 21 | ._* 22 | .DS_Store 23 | .Spotlight-V100 24 | .Trashes 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Eugene Cheung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js + React.js + AWS DynamoDB + Github OAuth 2 | 3 | This is boilerplate code for a local or remote AWS DynamoDB for a backend using Node.js with a React.js frontend and integrated Github OAuth to handle log in. 4 | 5 | 6 | ## Setup 7 | 8 | For either production or local, rename `config.example.js` to `config.js` in the `config` folder. You will need to add values (Github Secret & AWS IAM credentials). 9 | 10 | 11 | ### Local 12 | 13 | You will have to download the [local dev JAR](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html) to execute DynamoDB on your machine. You will also need Node.js. In your first terminal window run: 14 | 15 | ``` 16 | java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb 17 | ``` 18 | 19 | In your second terminal window: 20 | 21 | ``` 22 | npm install 23 | 24 | aws dynamodb create-table --cli-input-json file://__YOUR__PATH__/node-react-aws-dynamodb-boilerplate/config/tables/create-users-table.json --endpoint-url http://localhost:8000 25 | aws dynamodb create-table --cli-input-json file://__YOUR__PATH__/node-react-aws-dynamodb-boilerplate/config/tables/create-user-sessions-table.json --endpoint-url http://localhost:8000 26 | ``` 27 | 28 | ### Production 29 | 30 | Assuming, you have a nginx server setup. You will need to run: 31 | 32 | ``` 33 | npm install 34 | ``` 35 | 36 | 37 | ## Running 38 | 39 | ### Local 40 | 41 | ```shell 42 | npm run start:dev 43 | ``` 44 | 45 | ### Production 46 | 47 | ```shell 48 | npm start 49 | ``` 50 | -------------------------------------------------------------------------------- /client/app/actions/CounterActions.js: -------------------------------------------------------------------------------- 1 | // import axios from 'axios'; 2 | // This is the action creator. It's made up 3 | // of two parts. The function is the creator 4 | // of the action and the return attribute is 5 | // the actual action. 6 | export function selectCounter(counter) { 7 | return { 8 | // type of action occurred 9 | type: 'COUNTER_SELECT', 10 | payload: counter, 11 | }; 12 | }; 13 | 14 | export function getCounters() { 15 | return { 16 | type: 'COUNTER_LOADED', 17 | payload: new Promise((resolve, reject) => { 18 | // setTimeout(() => { 19 | // axios.get('/api/counters').then(response => { 20 | // const { data } = response; 21 | // resolve(data); 22 | // }); 23 | // }, 2000); 24 | }) 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /client/app/containers/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | const App = ({ children }) => ( 4 |
5 |
6 | {children} 7 |
8 |
9 | ); 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /client/app/containers/App/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const NotFound = () => ( 5 |
6 |

Page not found

7 | 8 | Go home 9 |
10 | ); 11 | 12 | export default NotFound; 13 | -------------------------------------------------------------------------------- /client/app/containers/Counters/Counters.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | 5 | // Redux Actions 6 | import { selectCounter, getCounters } from '../../actions/CounterActions'; 7 | // Technically, you can just add selectCounter to the onClick 8 | // action of the list of counters. However, it's not using 9 | // Redux in that case. To do it, properly you need to use 10 | // connect. 11 | 12 | class Counters extends Component { 13 | constructor() { 14 | super(); 15 | 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 |

Counters

22 | { 23 | (this.props.counters) ? ( 24 | (this.props.counters).map(counter => { 25 | return ( 26 |
  • this.props.selectCounter(counter)} 29 | > 30 | {counter._id} 31 |
  • 32 | ) 33 | }) 34 | ) : (null) 35 | } 36 | { 37 | (this.props.selectedCounter) ? ( 38 |
    39 |

    40 | Current Count for {this.props.selectedCounter._id} 41 | is {this.props.selectedCounter.count}. 42 |

    43 |
    44 | ) : (null) 45 | } 46 |
    47 | ); 48 | } 49 | } 50 | 51 | // Takes the application store (main data) and passes into 52 | // your container as a prop. It can pass any aspect of the 53 | // store. So we want counters, so state.counters. We can 54 | // now reference this.props.counters to grab counters in 55 | // store. 56 | function mapStateToProps(state) { 57 | return { 58 | counters: state.counters, 59 | selectedCounter: state.selectedCounter, 60 | }; 61 | }; 62 | 63 | // Passing the selectCounter action in as a prop. 64 | // Dispatch is a way saying call a function. 65 | function mapDispatchToProps(dispatch) { 66 | // Connect this function creator to prop. 67 | // return bindActionCreators({ 68 | // selectCounter: selectCounter, 69 | // getCounters: getCounters, 70 | // }, dispatch); 71 | return { 72 | selectCounter: (counter) => { 73 | dispatch(selectCounter(counter)); 74 | }, 75 | getCounters: dispatch(getCounters()) 76 | }; 77 | } 78 | 79 | export default connect(mapStateToProps, mapDispatchToProps)(Counters); 80 | -------------------------------------------------------------------------------- /client/app/containers/HelloWorld/HelloWorld.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const HelloWorld = () => ( 4 |

    Hello World

    5 | ); 6 | 7 | export default HelloWorld; 8 | -------------------------------------------------------------------------------- /client/app/containers/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import 'whatwg-fetch'; 3 | 4 | import { 5 | github_scope, 6 | github_client_id, 7 | } from '../../../../config/config'; 8 | import { 9 | getFromStorage, 10 | STORAGE_KEY, 11 | } from '../../utils/storage'; 12 | 13 | class Home extends Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | sessionToken: '', 19 | }; 20 | } 21 | 22 | componentDidMount() { 23 | const localObj = getFromStorage(STORAGE_KEY); 24 | if (localObj && localObj.token) { 25 | this.setState({ 26 | sessionToken: localObj.token, 27 | }); 28 | } 29 | } 30 | 31 | render() { 32 | const { 33 | sessionToken, 34 | } = this.state; 35 | 36 | if (!sessionToken) { 37 | const openLink = `https://github.com/login/oauth/authorize?client_id=${github_client_id}&scope=${github_scope}` 38 | return ( 39 |
    40 | 41 | Sign in with Github 42 | 43 |
    44 | ); 45 | } 46 | return ( 47 |
    48 |

    Logged in

    49 |
    50 | ); 51 | } 52 | } 53 | 54 | export default Home; 55 | -------------------------------------------------------------------------------- /client/app/containers/SignIn/SignInCallbackPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Redirect } from 'react-router'; 3 | 4 | import { 5 | signInOnServer, 6 | } from '../../utils/restapi'; 7 | import { 8 | setInStorage, 9 | STORAGE_KEY, 10 | } from '../../utils/storage'; 11 | 12 | class SignInCallbackPage extends Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | error: null, 18 | redirectTo: null, 19 | }; 20 | } 21 | 22 | componentDidMount() { 23 | const params = new URLSearchParams(this.props.location.search); 24 | const code = params.get('code'); 25 | console.log('code', code); 26 | signInOnServer(code).then((response) => { 27 | console.log('response', response); 28 | if (!response.success) { 29 | this.setState({ 30 | error: response.message, 31 | }); 32 | } else { 33 | setInStorage(STORAGE_KEY, { 34 | token: response.sessionToken, 35 | }); 36 | 37 | this.setState({ 38 | redirectTo: '/', 39 | }); 40 | } 41 | }); 42 | } 43 | 44 | render() { 45 | const { 46 | error, 47 | redirectTo, 48 | } = this.state; 49 | 50 | if (error) { 51 | return (

    {error}

    ); 52 | } 53 | 54 | if (redirectTo) { 55 | return ( 56 | 59 | ) 60 | } 61 | 62 | return (

    Logging in

    ); 63 | } 64 | } 65 | 66 | export default SignInCallbackPage; 67 | -------------------------------------------------------------------------------- /client/app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | // React Router related 5 | import { 6 | BrowserRouter as Router, 7 | Route, 8 | Link, 9 | Switch 10 | } from 'react-router-dom'; 11 | 12 | // Redux related 13 | import { Provider } from 'react-redux'; 14 | import store from './store'; 15 | 16 | // Containers 17 | import App from './containers/App/App'; 18 | import NotFound from './containers/App/NotFound'; 19 | import Home from './containers/Home/Home'; 20 | import HelloWorld from './containers/HelloWorld/HelloWorld'; 21 | import Counters from './containers/Counters/Counters'; 22 | import SignInCallbackPage from './containers/SignIn/SignInCallbackPage'; 23 | 24 | // Styles 25 | import './styles/styles.scss'; 26 | 27 | render(( 28 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ), document.getElementById('app')); 44 | -------------------------------------------------------------------------------- /client/app/reducers/CountersReducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This function returns an array of counters. Any element 3 | * you want to be saved into the store. 4 | */ 5 | export const getCounters = (state = null, action) => { 6 | console.log('CountersReducer_getCounters', action); 7 | switch (action.type) { 8 | case 'COUNTER_LOADED': 9 | console.log('COUNTER_LOADED', action.payload); 10 | return action.payload; 11 | break; 12 | case 'COUNTER_LOADED_FULFILLED': 13 | console.log('COUNTER_LOADED_FULFILLED', action.payload); 14 | return action.payload; 15 | break; 16 | } 17 | return state; 18 | }; 19 | 20 | // This function lists for the ACTION. Whenever, any action gets 21 | // called. It goes to all reducers. 22 | // This will return the current active counter (selected one). 23 | // State starts as null so if it's not selected. This is where 24 | // you can default user if want. 25 | // It will pass in the action too. You can check the type and 26 | // payload. 27 | // Reducers need to return some piece of data, hence payload. 28 | export const selectedCounter = (state = null, action) => { 29 | switch (action.type) { 30 | case 'COUNTER_SELECT': 31 | return action.payload; 32 | break; 33 | } 34 | return state; 35 | }; 36 | -------------------------------------------------------------------------------- /client/app/reducers/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Combines together all reducers to save old one 3 | * object to your store. 4 | */ 5 | import { combineReducers } from 'redux'; 6 | import { getCounters, selectedCounter } from './CountersReducer'; 7 | 8 | // I want to refer to counters as "counters" so that's why 9 | // it's spelled this way. Adding another reducer? Add it below 10 | // on a new line. 11 | const allReducers = combineReducers({ 12 | counters: getCounters, 13 | selectedCounter: selectedCounter, 14 | }); 15 | 16 | export default allReducers; 17 | -------------------------------------------------------------------------------- /client/app/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import allReducers from './reducers'; 3 | import thunk from 'redux-thunk'; 4 | import { createLogger } from 'redux-logger'; 5 | import promise from 'redux-promise-middleware'; 6 | 7 | 8 | // Redux 9 | // Store is all this application data. 10 | const logger = createLogger({}); 11 | const store = createStore( 12 | allReducers, 13 | {}, 14 | applyMiddleware(logger, thunk, promise()), 15 | ); 16 | // Reducer is a function that tells what data to store in store. 17 | // They take an action adn update part of the application 18 | // state. Reducers are broken down by parts. 19 | // Provider makes your store/data available to the containers. 20 | 21 | export default store; 22 | -------------------------------------------------------------------------------- /client/app/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | @import 'vendor/normalize'; 4 | -------------------------------------------------------------------------------- /client/app/styles/vendor/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Prevent adjustments of font size after orientation changes in IE and iOS. 6 | */ 7 | 8 | html { 9 | font-family: sans-serif; /* 1 */ 10 | -ms-text-size-adjust: 100%; /* 2 */ 11 | -webkit-text-size-adjust: 100%; /* 2 */ 12 | } 13 | 14 | /** 15 | * Remove the margin in all browsers (opinionated). 16 | */ 17 | 18 | body { 19 | margin: 0; 20 | } 21 | 22 | /* HTML5 display definitions 23 | ========================================================================== */ 24 | 25 | /** 26 | * Add the correct display in IE 9-. 27 | * 1. Add the correct display in Edge, IE, and Firefox. 28 | * 2. Add the correct display in IE. 29 | */ 30 | 31 | article, 32 | aside, 33 | details, /* 1 */ 34 | figcaption, 35 | figure, 36 | footer, 37 | header, 38 | main, /* 2 */ 39 | menu, 40 | nav, 41 | section, 42 | summary { /* 1 */ 43 | display: block; 44 | } 45 | 46 | /** 47 | * Add the correct display in IE 9-. 48 | */ 49 | 50 | audio, 51 | canvas, 52 | progress, 53 | video { 54 | display: inline-block; 55 | } 56 | 57 | /** 58 | * Add the correct display in iOS 4-7. 59 | */ 60 | 61 | audio:not([controls]) { 62 | display: none; 63 | height: 0; 64 | } 65 | 66 | /** 67 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 68 | */ 69 | 70 | progress { 71 | vertical-align: baseline; 72 | } 73 | 74 | /** 75 | * Add the correct display in IE 10-. 76 | * 1. Add the correct display in IE. 77 | */ 78 | 79 | template, /* 1 */ 80 | [hidden] { 81 | display: none; 82 | } 83 | 84 | /* Links 85 | ========================================================================== */ 86 | 87 | /** 88 | * 1. Remove the gray background on active links in IE 10. 89 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 90 | */ 91 | 92 | a { 93 | background-color: transparent; /* 1 */ 94 | -webkit-text-decoration-skip: objects; /* 2 */ 95 | } 96 | 97 | /** 98 | * Remove the outline on focused links when they are also active or hovered 99 | * in all browsers (opinionated). 100 | */ 101 | 102 | a:active, 103 | a:hover { 104 | outline-width: 0; 105 | } 106 | 107 | /* Text-level semantics 108 | ========================================================================== */ 109 | 110 | /** 111 | * 1. Remove the bottom border in Firefox 39-. 112 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: none; /* 1 */ 117 | text-decoration: underline; /* 2 */ 118 | text-decoration: underline dotted; /* 2 */ 119 | } 120 | 121 | /** 122 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 123 | */ 124 | 125 | b, 126 | strong { 127 | font-weight: inherit; 128 | } 129 | 130 | /** 131 | * Add the correct font weight in Chrome, Edge, and Safari. 132 | */ 133 | 134 | b, 135 | strong { 136 | font-weight: bolder; 137 | } 138 | 139 | /** 140 | * Add the correct font style in Android 4.3-. 141 | */ 142 | 143 | dfn { 144 | font-style: italic; 145 | } 146 | 147 | /** 148 | * Correct the font size and margin on `h1` elements within `section` and 149 | * `article` contexts in Chrome, Firefox, and Safari. 150 | */ 151 | 152 | h1 { 153 | font-size: 2em; 154 | margin: 0.67em 0; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Remove the border on images inside links in IE 10-. 200 | */ 201 | 202 | img { 203 | border-style: none; 204 | } 205 | 206 | /** 207 | * Hide the overflow in IE. 208 | */ 209 | 210 | svg:not(:root) { 211 | overflow: hidden; 212 | } 213 | 214 | /* Grouping content 215 | ========================================================================== */ 216 | 217 | /** 218 | * 1. Correct the inheritance and scaling of font size in all browsers. 219 | * 2. Correct the odd `em` font sizing in all browsers. 220 | */ 221 | 222 | code, 223 | kbd, 224 | pre, 225 | samp { 226 | font-family: monospace, monospace; /* 1 */ 227 | font-size: 1em; /* 2 */ 228 | } 229 | 230 | /** 231 | * Add the correct margin in IE 8. 232 | */ 233 | 234 | figure { 235 | margin: 1em 40px; 236 | } 237 | 238 | /** 239 | * 1. Add the correct box sizing in Firefox. 240 | * 2. Show the overflow in Edge and IE. 241 | */ 242 | 243 | hr { 244 | box-sizing: content-box; /* 1 */ 245 | height: 0; /* 1 */ 246 | overflow: visible; /* 2 */ 247 | } 248 | 249 | /* Forms 250 | ========================================================================== */ 251 | 252 | /** 253 | * 1. Change font properties to `inherit` in all browsers (opinionated). 254 | * 2. Remove the margin in Firefox and Safari. 255 | */ 256 | 257 | button, 258 | input, 259 | select, 260 | textarea { 261 | font: inherit; /* 1 */ 262 | margin: 0; /* 2 */ 263 | } 264 | 265 | /** 266 | * Restore the font weight unset by the previous rule. 267 | */ 268 | 269 | optgroup { 270 | font-weight: bold; 271 | } 272 | 273 | /** 274 | * Show the overflow in IE. 275 | * 1. Show the overflow in Edge. 276 | */ 277 | 278 | button, 279 | input { /* 1 */ 280 | overflow: visible; 281 | } 282 | 283 | /** 284 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 285 | * 1. Remove the inheritance of text transform in Firefox. 286 | */ 287 | 288 | button, 289 | select { /* 1 */ 290 | text-transform: none; 291 | } 292 | 293 | /** 294 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 295 | * controls in Android 4. 296 | * 2. Correct the inability to style clickable types in iOS and Safari. 297 | */ 298 | 299 | button, 300 | html [type="button"], /* 1 */ 301 | [type="reset"], 302 | [type="submit"] { 303 | -webkit-appearance: button; /* 2 */ 304 | } 305 | 306 | /** 307 | * Remove the inner border and padding in Firefox. 308 | */ 309 | 310 | button::-moz-focus-inner, 311 | [type="button"]::-moz-focus-inner, 312 | [type="reset"]::-moz-focus-inner, 313 | [type="submit"]::-moz-focus-inner { 314 | border-style: none; 315 | padding: 0; 316 | } 317 | 318 | /** 319 | * Restore the focus styles unset by the previous rule. 320 | */ 321 | 322 | button:-moz-focusring, 323 | [type="button"]:-moz-focusring, 324 | [type="reset"]:-moz-focusring, 325 | [type="submit"]:-moz-focusring { 326 | outline: 1px dotted ButtonText; 327 | } 328 | 329 | /** 330 | * Change the border, margin, and padding in all browsers (opinionated). 331 | */ 332 | 333 | fieldset { 334 | border: 1px solid #c0c0c0; 335 | margin: 0 2px; 336 | padding: 0.35em 0.625em 0.75em; 337 | } 338 | 339 | /** 340 | * 1. Correct the text wrapping in Edge and IE. 341 | * 2. Correct the color inheritance from `fieldset` elements in IE. 342 | * 3. Remove the padding so developers are not caught out when they zero out 343 | * `fieldset` elements in all browsers. 344 | */ 345 | 346 | legend { 347 | box-sizing: border-box; /* 1 */ 348 | color: inherit; /* 2 */ 349 | display: table; /* 1 */ 350 | max-width: 100%; /* 1 */ 351 | padding: 0; /* 3 */ 352 | white-space: normal; /* 1 */ 353 | } 354 | 355 | /** 356 | * Remove the default vertical scrollbar in IE. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; 361 | } 362 | 363 | /** 364 | * 1. Add the correct box sizing in IE 10-. 365 | * 2. Remove the padding in IE 10-. 366 | */ 367 | 368 | [type="checkbox"], 369 | [type="radio"] { 370 | box-sizing: border-box; /* 1 */ 371 | padding: 0; /* 2 */ 372 | } 373 | 374 | /** 375 | * Correct the cursor style of increment and decrement buttons in Chrome. 376 | */ 377 | 378 | [type="number"]::-webkit-inner-spin-button, 379 | [type="number"]::-webkit-outer-spin-button { 380 | height: auto; 381 | } 382 | 383 | /** 384 | * 1. Correct the odd appearance in Chrome and Safari. 385 | * 2. Correct the outline style in Safari. 386 | */ 387 | 388 | [type="search"] { 389 | -webkit-appearance: textfield; /* 1 */ 390 | outline-offset: -2px; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove the inner padding and cancel buttons in Chrome and Safari on OS X. 395 | */ 396 | 397 | [type="search"]::-webkit-search-cancel-button, 398 | [type="search"]::-webkit-search-decoration { 399 | -webkit-appearance: none; 400 | } 401 | 402 | /** 403 | * Correct the text style of placeholders in Chrome, Edge, and Safari. 404 | */ 405 | 406 | ::-webkit-input-placeholder { 407 | color: inherit; 408 | opacity: 0.54; 409 | } 410 | 411 | /** 412 | * 1. Correct the inability to style clickable types in iOS and Safari. 413 | * 2. Change font properties to `inherit` in Safari. 414 | */ 415 | 416 | ::-webkit-file-upload-button { 417 | -webkit-appearance: button; /* 1 */ 418 | font: inherit; /* 2 */ 419 | } 420 | -------------------------------------------------------------------------------- /client/app/utils/restapi.js: -------------------------------------------------------------------------------- 1 | exports.signInOnServer = (code) => { 2 | return fetch(`/api/signin?code=${code}`, { 3 | method: 'GET', 4 | headers: { 5 | 'Content-Type': 'application/json' 6 | } 7 | }).then(res => res.json()); 8 | }; 9 | -------------------------------------------------------------------------------- /client/app/utils/storage.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { 3 | local_storage_key, 4 | } from '../../../config/config.js'; 5 | export const STORAGE_KEY = local_storage_key; 6 | 7 | export function getFromStorage(key) { 8 | if (!key) { 9 | return null; 10 | } 11 | try { 12 | const valueStr = localStorage.getItem(key); 13 | if (valueStr) { 14 | return JSON.parse(valueStr); 15 | } else { 16 | return null; 17 | } 18 | } catch (err) { 19 | return null; 20 | } 21 | } 22 | export function setInStorage(key, obj) { 23 | if (!key) { 24 | console.error('Error: Cannot save in storage. Key is null.'); 25 | } 26 | try { 27 | localStorage.setItem(key, JSON.stringify(obj)); 28 | } catch (err) { 29 | console.error('Error: Unable to parse the object.'); 30 | console.error(err); 31 | } 32 | } 33 | /* eslint-enable */ 34 | -------------------------------------------------------------------------------- /client/public/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithweaver/node-react-aws-dynamodb-boilerplate/98d4e75feaae63cf416c5f70ce1a6c8b6a0e45f0/client/public/assets/img/logo.png -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MERN boilerplate 8 | 9 | 10 | 11 | 12 | 13 |
    14 | 15 | 16 | -------------------------------------------------------------------------------- /config/config.example.js: -------------------------------------------------------------------------------- 1 | // Copy this file as config.js in the same folder, with the proper database connection URI. 2 | // More info for setting up credentials for AWS DynamoDB. 3 | // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/SettingUp.DynamoWebService.html 4 | module.exports = { 5 | local_storage_key: 'node-react-aws-dynamodb', 6 | aws_user_table_name: 'usersTab', 7 | aws_user_session_table_name: 'userSessionsTab', 8 | aws_table_name: 'fruitsTab', 9 | aws_local_config: { 10 | region: 'local', 11 | endpoint: 'http://localhost:8000' 12 | }, 13 | aws_remote_config: { 14 | accessKeyId: 'YOUR_KEY', 15 | secretAccessKey: 'YOUR_SECRET_KEY', 16 | region: 'us-east-1', 17 | }, 18 | github_client_id: '', 19 | github_client_secret: '', 20 | github_scope: 'user' 21 | }; 22 | 23 | // A little more about it. After creating a table. 24 | // 1. You need to create an IAM Role. Download the IAM Key 25 | // and IAM secret. 26 | // 27 | // 2. You need the AWS CLI. Dont have it? Run: 28 | // pip install awscli --upgrade --user 29 | // 30 | // 3. Enter your Access Key and Secret with running: 31 | // aws configure 32 | // 33 | // AKIAIHE2WWIOZFWRIASQ 34 | // 1vYHX3s185I8W1O0kcdvcKeAPDmOVJtIDQUHOh0+ 35 | -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Helper functions 4 | function root(args) { 5 | args = Array.prototype.slice.call(arguments, 0); 6 | return path.join.apply(path, [__dirname].concat('../', ...args)); 7 | } 8 | 9 | exports.root = root; 10 | -------------------------------------------------------------------------------- /config/tables/create-fruits-table.json: -------------------------------------------------------------------------------- 1 | { 2 | "TableName": "fruitsTable", 3 | "KeySchema": [ 4 | { 5 | "AttributeName": "fruitId", 6 | "KeyType": "HASH" 7 | } 8 | ], 9 | "AttributeDefinitions": [ 10 | { 11 | "AttributeName": "fruitId", 12 | "AttributeType": "S" 13 | } 14 | ], 15 | "ProvisionedThroughput": { 16 | "ReadCapacityUnits": 5, 17 | "WriteCapacityUnits": 5 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config/tables/create-user-sessions-table.json: -------------------------------------------------------------------------------- 1 | { 2 | "TableName": "userSessionsTab", 3 | "KeySchema": [ 4 | { 5 | "AttributeName": "sessionToken", 6 | "KeyType": "HASH" 7 | } 8 | ], 9 | "AttributeDefinitions": [ 10 | { 11 | "AttributeName": "sessionToken", 12 | "AttributeType": "S" 13 | } 14 | ], 15 | "ProvisionedThroughput": { 16 | "ReadCapacityUnits": 5, 17 | "WriteCapacityUnits": 5 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config/tables/create-users-table.json: -------------------------------------------------------------------------------- 1 | { 2 | "TableName": "usersTab", 3 | "KeySchema": [ 4 | { 5 | "AttributeName": "userId", 6 | "KeyType": "HASH" 7 | }, 8 | { 9 | "AttributeName": "email", 10 | "KeyType": "Range" 11 | } 12 | ], 13 | "AttributeDefinitions": [ 14 | { 15 | "AttributeName": "userId", 16 | "AttributeType": "S" 17 | }, 18 | { 19 | "AttributeName": "email", 20 | "AttributeType": "S" 21 | } 22 | ], 23 | "ProvisionedThroughput": { 24 | "ReadCapacityUnits": 5, 25 | "WriteCapacityUnits": 5 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /config/tables/structure.md: -------------------------------------------------------------------------------- 1 | # Table Structure 2 | 3 | ## Users 4 | 5 | ```json 6 | { 7 | "userId": "", 8 | "email": "", 9 | "user": {}, 10 | "emailVerified": true, 11 | "emails": [], 12 | "name": "", 13 | } 14 | ``` 15 | 16 | ## UserSessions 17 | 18 | ```json 19 | { 20 | "userId": "", 21 | "sessionToken": "", 22 | "email": "", 23 | "isDeleted": false, 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /config/webpack.common.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const autoprefixer = require('autoprefixer'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | 7 | const helpers = require('./helpers'); 8 | 9 | const NODE_ENV = process.env.NODE_ENV; 10 | const isProd = NODE_ENV === 'production'; 11 | 12 | module.exports = { 13 | entry: { 14 | 'app': [ 15 | helpers.root('client/app/index.js') 16 | ] 17 | }, 18 | 19 | output: { 20 | path: helpers.root('dist'), 21 | publicPath: '/' 22 | }, 23 | 24 | resolve: { 25 | extensions: ['.js', '.json', '.css', '.scss', '.html'], 26 | alias: { 27 | 'app': 'client/app' 28 | } 29 | }, 30 | 31 | module: { 32 | rules: [ 33 | // JS files 34 | { 35 | test: /\.jsx?$/, 36 | include: helpers.root('client'), 37 | loader: 'babel-loader' 38 | }, 39 | 40 | // SCSS files 41 | { 42 | test: /\.scss$/, 43 | loader: ExtractTextPlugin.extract({ 44 | fallback: 'style-loader', 45 | use: [ 46 | { 47 | loader: 'css-loader', 48 | options: { 49 | 'sourceMap': true, 50 | 'importLoaders': 1 51 | } 52 | }, 53 | { 54 | loader: 'postcss-loader', 55 | options: { 56 | plugins: () => [ 57 | autoprefixer 58 | ] 59 | } 60 | }, 61 | 'sass-loader' 62 | ] 63 | }) 64 | } 65 | ] 66 | }, 67 | 68 | plugins: [ 69 | new webpack.HotModuleReplacementPlugin(), 70 | 71 | new webpack.DefinePlugin({ 72 | 'process.env': { 73 | NODE_ENV: JSON.stringify(NODE_ENV) 74 | } 75 | }), 76 | 77 | new HtmlWebpackPlugin({ 78 | template: helpers.root('client/public/index.html'), 79 | inject: 'body' 80 | }), 81 | 82 | new ExtractTextPlugin({ 83 | filename: 'css/[name].[hash].css', 84 | disable: !isProd 85 | }), 86 | 87 | new CopyWebpackPlugin([{ 88 | from: helpers.root('client/public') 89 | }]) 90 | ] 91 | }; 92 | -------------------------------------------------------------------------------- /config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const merge = require('webpack-merge'); 3 | 4 | const commonConfig = require('./webpack.common'); 5 | 6 | module.exports = merge(commonConfig, { 7 | devtool: 'eval-source-map', 8 | 9 | mode: 'development', 10 | 11 | entry: { 12 | 'app': [ 13 | 'webpack-hot-middleware/client?reload=true' 14 | ] 15 | }, 16 | 17 | output: { 18 | filename: 'js/[name].js', 19 | chunkFilename: '[id].chunk.js' 20 | }, 21 | 22 | devServer: { 23 | contentBase: './client/public', 24 | historyApiFallback: true, 25 | stats: 'minimal' // none (or false), errors-only, minimal, normal (or true) and verbose 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /config/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const merge = require('webpack-merge'); 3 | 4 | const helpers = require('./helpers'); 5 | const commonConfig = require('./webpack.common'); 6 | 7 | module.exports = merge(commonConfig, { 8 | mode: 'production', 9 | 10 | output: { 11 | filename: 'js/[name].[hash].js', 12 | chunkFilename: '[id].[hash].chunk.js' 13 | }, 14 | 15 | plugins: [] 16 | }); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-react-aws-dynamodb-boilerplate", 3 | "version": "1.0.0", 4 | "description": "Node.js React.js AWS DynamoDB project boilerplate", 5 | "author": "Keith Weaver", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/keithweaver/node-react-aws-dynamodb-boilerplate.git" 9 | }, 10 | "license": "MIT", 11 | "private": true, 12 | "scripts": { 13 | "start": "webpack -p --progress --profile --colors --mode production && NODE_ENV=production node server", 14 | "start:dev": "node server" 15 | }, 16 | "engines": { 17 | "node": ">=6" 18 | }, 19 | "dependencies": { 20 | "@babel/core": "^7.0.0-beta.42", 21 | "@babel/preset-env": "^7.0.0-beta.42", 22 | "@babel/preset-react": "^7.0.0-beta.42", 23 | "async": "^2.6.0", 24 | "autoprefixer": "^8.2.0", 25 | "aws-sdk": "^2.228.1", 26 | "babel-loader": "^8.0.0-beta.2", 27 | "connect-history-api-fallback": "^1.5.0", 28 | "copy-webpack-plugin": "^4.5.1", 29 | "css-loader": "^0.28.11", 30 | "express": "^4.16.3", 31 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 32 | "html-webpack-plugin": "^3.1.0", 33 | "lodash": "^4.17.10", 34 | "lodash-clean": "^2.0.1", 35 | "mongoose": "^5.0.11", 36 | "node-sass": "^4.7.2", 37 | "nodemon": "^1.17.2", 38 | "postcss-loader": "^2.1.3", 39 | "react": "^16.2.0", 40 | "react-dom": "^16.2.0", 41 | "react-hot-loader": "^4.0.0", 42 | "react-redux": "^5.0.7", 43 | "react-router": "^4.2.0", 44 | "react-router-dom": "^4.2.2", 45 | "redux": "^4.0.0", 46 | "redux-logger": "^3.0.6", 47 | "redux-promise-middleware": "^5.1.1", 48 | "redux-thunk": "^2.2.0", 49 | "sass-loader": "^6.0.7", 50 | "style-loader": "^0.20.3", 51 | "superagent": "^3.8.3", 52 | "uglifyjs-webpack-plugin": "^1.2.5", 53 | "webpack": "^4.2.0", 54 | "webpack-cli": "^2.0.13", 55 | "webpack-dev-middleware": "^3.0.1", 56 | "webpack-hot-middleware": "^2.21.2", 57 | "webpack-merge": "^4.1.2", 58 | "whatwg-fetch": "^2.0.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const nodemon = require('nodemon'); 2 | const path = require('path'); 3 | 4 | nodemon({ 5 | execMap: { 6 | js: 'node' 7 | }, 8 | script: path.join(__dirname, 'server/server'), 9 | ignore: [], 10 | watch: process.env.NODE_ENV !== 'production' ? ['server/*'] : false, 11 | ext: 'js' 12 | }) 13 | .on('restart', function() { 14 | console.log('Server restarted!'); 15 | }) 16 | .once('exit', function () { 17 | console.log('Shutting down server'); 18 | process.exit(); 19 | }); 20 | -------------------------------------------------------------------------------- /server/routes/api/fruits.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const config = require('../../../config/config.js'); 3 | const isDev = process.env.NODE_ENV !== 'production'; 4 | 5 | module.exports = (app) => { 6 | // Gets all fruits 7 | app.get('/api/fruits', (req, res, next) => { 8 | if (isDev) { 9 | console.log('isDev'); 10 | AWS.config.update(config.aws_local_config); 11 | } else { 12 | console.log('isProd'); 13 | AWS.config.update(config.aws_remote_config); 14 | } 15 | 16 | const docClient = new AWS.DynamoDB.DocumentClient(); 17 | 18 | const params = { 19 | TableName: config.aws_table_name 20 | }; 21 | 22 | docClient.scan(params, function(err, data) { 23 | if (err) { 24 | res.send({ 25 | success: false, 26 | message: 'Error: Server error' 27 | }); 28 | } else { 29 | const { Items } = data; 30 | 31 | res.send({ 32 | success: true, 33 | message: 'Loaded fruits', 34 | fruits: Items 35 | }); 36 | } 37 | }); 38 | }); // end of app.get(/api/fruits) 39 | 40 | // Get a single fruit by id 41 | app.get('/api/fruit', (req, res, next) => { 42 | if (isDev) { 43 | AWS.config.update(config.aws_local_config); 44 | } else { 45 | AWS.config.update(config.aws_remote_config); 46 | } 47 | 48 | const fruitId = req.query.id; 49 | 50 | const docClient = new AWS.DynamoDB.DocumentClient(); 51 | 52 | const params = { 53 | TableName: config.aws_table_name, 54 | KeyConditionExpression: 'fruitId = :i', 55 | ExpressionAttributeValues: { 56 | ':i': fruitId 57 | } 58 | }; 59 | 60 | docClient.query(params, function(err, data) { 61 | if (err) { 62 | res.send({ 63 | success: false, 64 | message: 'Error: Server error' 65 | }); 66 | } else { 67 | console.log('data', data); 68 | const { Items } = data; 69 | 70 | res.send({ 71 | success: true, 72 | message: 'Loaded fruits', 73 | fruits: Items 74 | }); 75 | } 76 | }); 77 | }); 78 | 79 | // Add a fruit 80 | app.post('/api/fruit', (req, res, next) => { 81 | if (isDev) { 82 | AWS.config.update(config.aws_local_config); 83 | } else { 84 | AWS.config.update(config.aws_remote_config); 85 | } 86 | 87 | const { type, color } = req.body; 88 | // Not actually unique and can create problems. 89 | const fruitId = (Math.random() * 1000).toString(); 90 | 91 | const docClient = new AWS.DynamoDB.DocumentClient(); 92 | 93 | const params = { 94 | TableName: config.aws_table_name, 95 | Item: { 96 | fruitId: fruitId, 97 | fruitType: type, 98 | color: color 99 | } 100 | }; 101 | 102 | docClient.put(params, function(err, data) { 103 | if (err) { 104 | res.send({ 105 | success: false, 106 | message: 'Error: Server error' 107 | }); 108 | } else { 109 | console.log('data', data); 110 | const { Items } = data; 111 | 112 | res.send({ 113 | success: true, 114 | message: 'Added fruit', 115 | fruitId: fruitId 116 | }); 117 | } 118 | }); 119 | }); 120 | }; 121 | -------------------------------------------------------------------------------- /server/routes/api/signin.js: -------------------------------------------------------------------------------- 1 | const superagent = require('superagent'); 2 | const async = require('async'); 3 | const AWS = require('aws-sdk'); 4 | const _ = require('lodash'); 5 | 6 | const config = require('../../../config/config'); 7 | const unique = require('../../utils/unique'); 8 | const isDev = process.env.NODE_ENV !== 'production'; 9 | 10 | module.exports = (app) => { 11 | /* 12 | * Sign into a Github account. 13 | * 1. Pass in the code returned by Github (Approve to sign in) 14 | * 2. Post code to get Access Token 15 | * 3. Get User information 16 | * 4. Get user emails 17 | * 5. Check for User 18 | * 6. Sign Up User if not there 19 | * 7. Create User Session 20 | * 8. Save User Session 21 | */ 22 | app.get('/api/signin', (req, res, next) => { 23 | const { code } = req.query; 24 | 25 | console.log('/api/signin'); 26 | 27 | if (!code) { 28 | res.end('Error: Invalid sign in. Please try again.'); 29 | } 30 | 31 | // Set up AWS 32 | if (isDev) { 33 | console.log('isDev'); 34 | AWS.config.update(config.aws_local_config); 35 | } else { 36 | console.log('isProd'); 37 | AWS.config.update(config.aws_remote_config); 38 | } 39 | 40 | const docClient = new AWS.DynamoDB.DocumentClient(); 41 | 42 | async.waterfall([ 43 | (callback) => { 44 | console.log('callback1'); 45 | // Generate access token 46 | superagent 47 | .post('https://github.com/login/oauth/access_token') 48 | .send({ 49 | client_id: config.github_client_id, 50 | client_secret: config.github_client_secret, 51 | code: code 52 | }) 53 | .set('Accept', 'application/json') 54 | .then((response) => { 55 | console.log('response', response); 56 | const { access_token } = response.body; 57 | console.log('generate_access_token_response', response.body); 58 | 59 | if (!access_token) { 60 | callback(null, true, { 61 | success: false, 62 | message: 'Error: Invalid code' 63 | }); 64 | } else { 65 | callback(null, false, { access_token: access_token }); 66 | } 67 | }); 68 | }, 69 | (hasCompleted, results, callback) => { 70 | if (hasCompleted) { 71 | callback(null, hasCompleted, results); 72 | } else { 73 | const access_token = results.access_token; 74 | // Get User 75 | superagent 76 | .get('https://api.github.com/user') 77 | .set('Authorization', `token ${access_token}`) 78 | .set('Accept', 'application/json') 79 | .then((responseUser) => { 80 | const userBody = responseUser.body; 81 | console.log('get_user_response', responseUser.body); 82 | 83 | callback(null, false, { 84 | access_token: access_token, 85 | user: userBody 86 | }); 87 | }); 88 | } // end of else for hasCompleted 89 | }, 90 | (hasCompleted, results, callback) => { 91 | console.log('results0', results); 92 | if (hasCompleted) { 93 | callback(null, hasCompleted, results); 94 | } else { 95 | const { access_token } = results; 96 | // Get user emails 97 | superagent 98 | .get('https://api.github.com/user/emails') 99 | .set('Authorization', `token ${access_token}`) 100 | .set('Accept', 'application/json') 101 | .then((responseEmails) => { 102 | const emailsBody = responseEmails.body; 103 | console.log('get_emails_response', emailsBody); 104 | 105 | let primaryEmail = ''; 106 | let isVerified = false; 107 | for (let i = 0;i < emailsBody.length; i++) { 108 | if (emailsBody[i].primary) { 109 | primaryEmail = emailsBody[i].email; 110 | isVerified = emailsBody[i].verified; 111 | 112 | break; 113 | } 114 | } 115 | 116 | results.primaryEmail = primaryEmail; 117 | results.isPrimaryEmailVerified = isVerified; 118 | results.emails = emailsBody; 119 | 120 | callback(null, false, results); 121 | }); 122 | } 123 | }, 124 | (hasCompleted, results, callback) => { 125 | console.log('results1', results); 126 | if (hasCompleted) { 127 | callback(null, hasCompleted, results); 128 | } else { 129 | // Check for user 130 | const params = { 131 | TableName: config.aws_user_table_name, 132 | FilterExpression: 'email = :e', 133 | ExpressionAttributeValues: { 134 | ':e': results.primaryEmail 135 | } 136 | }; 137 | console.log('params', params); 138 | 139 | // Change this to a scan 140 | docClient.scan(params, (err, data) => { 141 | if (err) { 142 | console.log('err', err); 143 | callback(null, true, { 144 | success: false, 145 | message: 'Error: User look up' 146 | }); 147 | } else { 148 | console.log('data', data); 149 | const { Items } = data; 150 | 151 | console.log('users_Items', Items); 152 | 153 | results.userAccounts = Items; 154 | 155 | callback(null, false, results); 156 | } 157 | }); 158 | } 159 | }, 160 | (hasCompleted, results, callback) => { 161 | console.log('results2', results); 162 | if (hasCompleted) { 163 | callback(null, hasCompleted, results); 164 | } else { 165 | // Sign Up or nothing 166 | if (results.userAccounts.length == 0) { 167 | // Sign up 168 | console.log('Sign Up'); 169 | let bio = ''; 170 | if (results.user.bio) { 171 | bio = results.user.bio; 172 | } 173 | let company = ''; 174 | if (results.user.company) { 175 | company = results.user.company; 176 | } 177 | console.log('results', results); 178 | // TODO - b/c of the null in the objet. AWS throws error. 179 | const githubUser = { 180 | // bio: bio, 181 | company: company, 182 | login: results.user.login, 183 | id: results.user.id, 184 | // avatar_url: results.user.avatar_url, 185 | // gravatar_id: results.user.gravatar_id, 186 | // url: results.user.url, 187 | // html_url: results.user.html_url, 188 | // followers_url: results.user.followers_url, 189 | // following_url: results.user.following_url, 190 | // gists_url: results.user.gists_url, 191 | // starred_url: results.user.starred_url, 192 | // subscriptions_url: results.user.subscriptions_url, 193 | // organizations_url: results.user.organizations_url, 194 | // repos_url: results.user.repos_url, 195 | // events_url: results.user.events_url, 196 | // received_events_url: results.user.received_events_url, 197 | // type: results.user.type, 198 | // site_admin: results.user.site_admin, 199 | // name: results.user.name, 200 | // blog: results.user.blog, 201 | // location: results.user.location, 202 | // hireable: results.user.hireable, 203 | // public_repos: results.user.public_repos, 204 | // public_gists: results.user.public_gists, 205 | // followers: results.user.followers, 206 | // following: results.user.following, 207 | // created_at: results.user.created_at, 208 | // updated_at: results.user.updated_at, 209 | // private_gists: results.user.private_gists, 210 | // total_private_repos: results.user.total_private_repos, 211 | // owned_private_repos: results.user.owned_private_repos, 212 | // disk_usage: results.user.disk_usage, 213 | // collaborators: results.user.collaborators, 214 | // two_factor_authentication: results.user.two_factor_authentication 215 | }; 216 | 217 | let emails = []; 218 | for (let i = 0;i < results.emails.length;i++) { 219 | emails.push({ 220 | email: results.emails[i].email, 221 | primary: results.emails[i].primary, 222 | verified: results.emails[i].verified 223 | }); 224 | } 225 | 226 | const userId = unique.generateUserId(); 227 | const newUser = { 228 | userId: userId, 229 | email: results.primaryEmail, 230 | // user: githubUser, 231 | emailVerified: results.isPrimaryEmailVerified, 232 | // emails: emails, 233 | name: results.user.name 234 | }; 235 | console.log('newUser1', newUser); 236 | 237 | const params = { 238 | TableName: config.aws_user_table_name, 239 | Item: newUser 240 | }; 241 | 242 | console.log('params2', params); 243 | 244 | docClient.put(params, (err, data) => { 245 | if (err) { 246 | console.log('err', err); 247 | callback(null, true, { 248 | success: false, 249 | message: 'Error: User sign up' 250 | }); 251 | } else { 252 | console.log('data', data); 253 | results.userId = data.userId; 254 | results.name = newUser.name; 255 | callback(null, false, results); 256 | } 257 | }); 258 | } else { 259 | console.log('Sign In'); 260 | callback(null, false, results); 261 | } 262 | } 263 | }, 264 | (hasCompleted, results, callback) => { 265 | console.log('results3', results); 266 | if (hasCompleted) { 267 | callback(null, hasCompleted, results); 268 | } else { 269 | // Generate user session 270 | const sessionToken = unique.generateSessionToken(); 271 | const newSession = { 272 | "userId": results.userId, 273 | "sessionToken": sessionToken, 274 | "email": results.primaryEmail, 275 | "isDeleted": false, 276 | }; 277 | 278 | const params = { 279 | TableName: config.aws_user_session_table_name, 280 | Item: newSession 281 | }; 282 | 283 | docClient.put(params, (err, data) => { 284 | if (err) { 285 | console.log('err', err); 286 | callback(null, true, { 287 | success: false, 288 | message: 'Error: User session' 289 | }); 290 | } else { 291 | results.sessionToken = sessionToken; 292 | callback(null, results); 293 | } 294 | }); 295 | } 296 | } 297 | ], (err, result) => { 298 | if (err) { 299 | console.log('err', err); 300 | res.send({ 301 | success: false, 302 | message: 'Error: Server error' 303 | }); 304 | } else { 305 | console.log('result', result); 306 | 307 | result.success = true; 308 | result.message = 'Signed in'; 309 | 310 | res.send(result); 311 | } 312 | }); // end of async waterfall 313 | }); // end of app.get 314 | }; 315 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = (app) => { 5 | // API routes 6 | fs.readdirSync(__dirname + '/api/').forEach((file) => { 7 | require(`./api/${file.substr(0, file.indexOf('.'))}`)(app); 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const fs = require('fs'); 3 | const historyApiFallback = require('connect-history-api-fallback'); 4 | const mongoose = require('mongoose'); 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const webpackDevMiddleware = require('webpack-dev-middleware'); 8 | const webpackHotMiddleware = require('webpack-hot-middleware'); 9 | 10 | const config = require('../config/config'); 11 | const webpackConfig = require('../webpack.config'); 12 | 13 | const isDev = process.env.NODE_ENV !== 'production'; 14 | const port = process.env.PORT || 8080; 15 | 16 | 17 | // Configuration 18 | // ================================================================================================ 19 | 20 | // Set up Mongoose 21 | mongoose.connect(isDev ? config.db_dev : config.db); 22 | mongoose.Promise = global.Promise; 23 | 24 | const app = express(); 25 | app.use(express.urlencoded({ extended: true })); 26 | app.use(express.json()); 27 | 28 | // API routes 29 | require('./routes')(app); 30 | 31 | if (isDev) { 32 | const compiler = webpack(webpackConfig); 33 | 34 | app.use(historyApiFallback({ 35 | verbose: false 36 | })); 37 | 38 | app.use(webpackDevMiddleware(compiler, { 39 | publicPath: webpackConfig.output.publicPath, 40 | contentBase: path.resolve(__dirname, '../client/public'), 41 | stats: { 42 | colors: true, 43 | hash: false, 44 | timings: true, 45 | chunks: false, 46 | chunkModules: false, 47 | modules: false 48 | } 49 | })); 50 | 51 | app.use(webpackHotMiddleware(compiler)); 52 | app.use(express.static(path.resolve(__dirname, '../dist'))); 53 | } else { 54 | app.use(express.static(path.resolve(__dirname, '../dist'))); 55 | app.get('*', function (req, res) { 56 | res.sendFile(path.resolve(__dirname, '../dist/index.html')); 57 | res.end(); 58 | }); 59 | } 60 | 61 | app.listen(port, '0.0.0.0', (err) => { 62 | if (err) { 63 | console.log(err); 64 | } 65 | 66 | console.info('>>> 🌎 Open http://0.0.0.0:%s/ in your browser.', port); 67 | }); 68 | 69 | module.exports = app; 70 | -------------------------------------------------------------------------------- /server/utils/unique.js: -------------------------------------------------------------------------------- 1 | exports.generateUserId = function() { 2 | return 'id-' + Math.random().toString(36).substr(2, 16); 3 | }; 4 | exports.generateSessionToken = function() { 5 | return 'id-' + Math.random().toString(36).substr(2, 16); 6 | }; 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | switch (process.env.NODE_ENV) { 2 | case 'prod': 3 | case 'production': 4 | module.exports = require('./config/webpack.prod'); 5 | break; 6 | 7 | case 'dev': 8 | case 'development': 9 | default: 10 | module.exports = require('./config/webpack.dev'); 11 | } 12 | --------------------------------------------------------------------------------