├── .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 |
44 | );
45 | }
46 | return (
47 |
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 |
--------------------------------------------------------------------------------