├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierignore ├── .prettierrc.yml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── client ├── app.js ├── components │ ├── auth-form.js │ ├── index.js │ ├── navbar.js │ ├── user-home.js │ └── user-home.spec.js ├── history.js ├── index.js ├── routes.js ├── socket.js └── store │ ├── index.js │ ├── user.js │ └── user.spec.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── style.css ├── script ├── deploy ├── encrypt-heroku-auth-token.js ├── keyComments.json ├── seed.js └── seed.spec.js ├── server ├── api │ ├── index.js │ ├── users.js │ └── users.spec.js ├── auth │ ├── google.js │ └── index.js ├── db │ ├── db.js │ ├── index.js │ └── models │ │ ├── index.js │ │ ├── user.js │ │ └── user.spec.js ├── index.js └── socket │ └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/react", 4 | "@babel/env" 5 | /* 6 | Babel uses these "presets" to know how to transpile your Javascript code. Here's what we're saying with these: 7 | 8 | 'react': teaches Babel to recognize JSX - a must have for React! 9 | 10 | 'env': teaches Babel to transpile Javascript . This preset is highly configurable, and you can reduce the size of your bundle by limiting the number of features you transpile. Learn more here: https://github.com/babel/babel-preset-env 11 | */ 12 | ], 13 | "plugins": [ 14 | /* 15 | These plugins teach Babel to recognize EcmaScript language features that have reached "stage 2" in the process of approval for inclusion in the official EcmaScript specification (called the "TC39 process"). There are 5 stages in the process, starting at 0 (basically a brand new proposal) going up to 4 (finished and ready for inclusion). Read more about it here: http://2ality.com/2015/11/tc39-process.html. Using new language features before they're officially part of EcmaScript is fun, but it also carries a risk: sometimes proposed features can change substantially (or be rejected entirely) before finally being included in the language, so if you jump on the bandwagon too early, you risk having your code be dependent on defunct/nonstandard syntax! "Stage 2" is a fairly safe place to start - after stage 2, the feature is well on its way to official inclusion and only minor changes are expected. 16 | */ 17 | "@babel/plugin-syntax-dynamic-import", 18 | "@babel/plugin-syntax-import-meta", 19 | "@babel/plugin-proposal-class-properties", 20 | "@babel/plugin-proposal-json-strings", 21 | [ 22 | "@babel/plugin-proposal-decorators", 23 | { 24 | "legacy": true 25 | } 26 | ], 27 | "@babel/plugin-proposal-function-sent", 28 | "@babel/plugin-proposal-export-namespace-from", 29 | "@babel/plugin-proposal-numeric-separator", 30 | "@babel/plugin-proposal-throw-expressions" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.{yml,yaml}] 16 | indent_style = space 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["fullstack", "prettier", "prettier/react"], 4 | "rules": { 5 | "semi": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this Project 2 | 3 | For pull requests to be merged, authors should: 4 | 5 | * Write any applicable unit tests 6 | * Add any relevant documentation 7 | * Reference any relevant issues 8 | * Obtain a review from a team member 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | For bugs, please include the following: 2 | 3 | * What is the expected behavior? 4 | * What is the actual behavior? 5 | * What steps reproduce the behavior? 6 | 7 | For features, please specify at least minimal requirements, e.g.: 8 | 9 | * "As a user, I want a notification badge showing unread count, so I can easily manage my messages" 10 | * "As a developer, I want linting to work properly with JSX, so I can see when there is a mistake" 11 | * "As an admin, I want a management panel for users, so I can delete spurious accounts" 12 | 13 | --- 14 | 15 | _Issue description here…_ 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Assignee Tasks 2 | 3 | * [ ] added unit tests (or none needed) 4 | * [ ] written relevant docs (or none needed) 5 | * [ ] referenced any relevant issues (or none exist) 6 | 7 | ### Guidelines 8 | 9 | Please add a description of this Pull Request's motivation, scope, outstanding issues or potential alternatives, reasoning behind the current solution, and any other relevant information for posterity. 10 | 11 | --- 12 | 13 | _Your PR Notes Here_ 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/bundle.js 3 | public/bundle.js.map 4 | secrets.js 5 | .DS_Store 6 | npm-debug.log 7 | yarn-error.log 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | public/bundle.js 2 | public/bundle.js.map 3 | package-lock.json 4 | package.json 5 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | # printWidth: 80 # 80 2 | # tabWidth: 2 # 2 3 | # useTabs: false # false 4 | semi: false # true 5 | singleQuote: true # false 6 | # trailingComma: none # none | es5 | all 7 | bracketSpacing: false # true 8 | # jsxBracketSameLine: false # false 9 | # arrowParens: avoid # avoid | always 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 14 # uses version 14 4 | services: 5 | - postgresql # starts up postgres 6 | addons: 7 | postgresql: '10' # recent postgres version on Travis 8 | dist: xenial # uses xenial environment 9 | notifications: 10 | email: 11 | on_success: change # default: change (only when going from broken to fixed) 12 | on_failure: always # default: always (which is annoying, as it should be) 13 | install: 14 | - npm ci # faster, goes only from package-lock 15 | before_script: 16 | - psql -c 'create database "boilermaker-test";' -U postgres # remember to change this name if you change it elsewhere (e.g. package.json) 17 | script: 18 | - npm test # test the code 19 | - npm run build-client # make the bundle 20 | # before_deploy: 21 | # - rm -rf node_modules # omit from the tarball, since we skip cleanup 22 | # deploy: 23 | # skip_cleanup: true # prevents travis from deleting the build 24 | # provider: heroku 25 | # app: YOUR-HEROKU-APP-NAME-HERE # see README 26 | # api_key: 27 | # secure: YOUR-***ENCRYPTED***-API-KEY-HERE # see README 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Updates to Boilermaker 2 | 3 | ## Tuesday, April 9th, 2019 4 | 5 | ### Dependencies 6 | 7 | * axios update to 0.18.0 from 0.15.3 8 | * connect-session-sequelize update to 6.0.0 from 4.1.0 9 | * history update to 4.9.0 from 4.6.3 10 | * morgan update to 1.9.1 from 1.8.1 11 | * passport update to 0.4.0 from 0.3.2 12 | * passport-google-oauth update to 2.0.0 from 1.0.0 13 | * pg update to 7.9.0 from 6.1.2 14 | * prop-types update to 15.7.2 from 15.6.2 15 | * **react-redux update to 5.0.7 from 5.0.2** 16 | * There are some known issues with this and other react packages; will update after some testing 17 | * react-router-dom update to 5.0.0 from 4.3.1 18 | * redux update to 4.0.1 from 3.6.0 19 | * redux-logger update to 3.0.6 from 2.8.1 20 | * sequelize update to 5.2.15 from 4.38.0 21 | * socket.io update to 2.2.0 from 2.1.0 22 | 23 | ### DevDependencies 24 | 25 | * axios-mock-adatper update to 1.16.0 from 1.15.0 26 | * babel-eslint update to 10.0.1 from 8.2.6 27 | * chai update to 4.2.0 from 3.5.0 28 | * enzyme update to 3.9.0 from 3.0.0 29 | * enzyme-adapter-react-16 update to 1.12.1 from 1.0.0 30 | * eslint update to 5.16.0 from 4.19.1 31 | * eslint-config-fullstack update to 6.0.0 from 5.1.0 32 | * eslint-config-prettier update to 4.1.0 from 2.9.0 33 | * husky update to 1.3.1 from 0.14.3 34 | * lint-staged update to 8.1.5 from 7.2.0 35 | * mocha update to 6.1.2 from 5.2.0 36 | * supertest update to 4.0.2 from 3.1.0 37 | * @babel/core update to 7.4.3 from 7.0.0-beta.55 38 | * @babel/plugin-proposal-class-properties update to 7.4.0 from 7.0.0-beta.54 39 | * @babel/plugin-proposal-decorators update to 7.4.0 from 7.0.0-beta.54 40 | * @babel/plugin-proposal-export-namespace-from update to 7.2.0 from 7.0.0-beta.54 41 | * @babel/plugin-proposal-function-sent update to 7.2.0 from 7.0.0-beta.54 42 | * @babel/plugin-proposal-numeric-separator update to 7.2.0 from 7.0.0-beta.54 43 | * @babel/plugin-proposal-throw-expressions update to 7.2.0 from 7.0.0-beta.54 44 | * @babel/plugin-syntax-dynamic-import update to 7.2.0 from 7.0.0-beta.54 45 | * @babel/plugin-syntax-import-meta update to 7.2.0 from 7.0.0-beta.54 46 | * @babel/polyfill update to 7.4.3 from 7.0.0-beta.55 47 | * @babel/preset-env update to 7.4.3 from 7.0.0-beta.55 48 | * @babel/preset-react update to 7.0.0 from 7.0.0-beta.55 49 | * @babel/register update to 7.4.0 from 7.0.0-beta.55 50 | * babel-loader update to 8.0.5 from 8.0.0-beta.4 51 | 52 | `npm i enzyme` to fix lodash dependency: [Prototype Polution](https://www.npmjs.com/advisories/782) 53 | 54 | ## Wednesday, April 10th, 2019 55 | 56 | ### Dependencies 57 | 58 | * react-redux update to 7.0.1 from 5.0.7 59 | * Found out that as long as react- is 16.4+, the updates should be fine 60 | * react update to 16.8.6 from 16.4.2 61 | * react-dom update to 16.8.6 from 16.4.2 62 | 63 | ## Thursday, April 11th, 2019 64 | 65 | ### Dependencies 66 | 67 | * sequelize update to 5.3.1 from 5.2.15 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Fullstack Academy 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 | # Boilermaker 2 | 3 | _Good things come in pairs_ 4 | 5 | Looking to mix up a backend with `express`/`sequelize` and a frontend with 6 | `react`/`redux`? That's `boilermaker`! 7 | 8 | Follow along with the boilerplate workshop to make your own! This canonical 9 | version can serve as a reference, or a starting point. For an in depth 10 | discussion into the code that makes up this repository, see the 11 | [Boilermaker Guided Tour][boilermaker-yt] 12 | 13 | [boilermaker-yt]: https://www.youtube.com/playlist?list=PLx0iOsdUOUmn7D5XL4mRUftn8hvAJGs8H 14 | 15 | ## Setup 16 | 17 | To use this as boilerplate, you'll need to take the following steps: 18 | 19 | * Don't fork or clone this repo! Instead, create a new, empty 20 | directory on your machine and `git init` (or create an empty repo on 21 | Github and clone it to your local machine) 22 | * Run the following commands: 23 | 24 | ``` 25 | git remote add boilermaker https://github.com/FullstackAcademy/boilermaker.git 26 | git fetch boilermaker 27 | git merge boilermaker/master 28 | ``` 29 | 30 | Why did we do that? Because every once in a while, `boilermaker` may 31 | be updated with additional features or bug fixes, and you can easily 32 | get those changes from now on by entering: 33 | 34 | ``` 35 | git fetch boilermaker 36 | git merge boilermaker/master 37 | ``` 38 | 39 | ## Customize 40 | 41 | Now that you've got the code, follow these steps to get acclimated: 42 | 43 | * Update project name and description in `package.json` and 44 | `.travis.yml` files 45 | * `npm install` 46 | * Create two postgres databases (`MY_APP_NAME` should match the `name` 47 | parameter in `package.json`): 48 | 49 | ``` 50 | export MY_APP_NAME=boilermaker 51 | createdb $MY_APP_NAME 52 | createdb $MY_APP_NAME-test 53 | ``` 54 | 55 | * By default, running `npm test` will use `boilermaker-test`, while 56 | regular development uses `boilermaker` 57 | * Create a file called `secrets.js` in the project root 58 | * This file is listed in `.gitignore`, and will _only_ be required 59 | in your _development_ environment 60 | * Its purpose is to attach the secret environment variables that you 61 | will use while developing 62 | * However, it's **very** important that you **not** push it to 63 | Github! Otherwise, _prying eyes_ will find your secret API keys! 64 | * It might look like this: 65 | 66 | ``` 67 | process.env.GOOGLE_CLIENT_ID = 'hush hush' 68 | process.env.GOOGLE_CLIENT_SECRET = 'pretty secret' 69 | process.env.GOOGLE_CALLBACK = '/auth/google/callback' 70 | ``` 71 | 72 | ### OAuth 73 | 74 | * To use OAuth with Google, complete the steps above with a real client 75 | ID and client secret supplied from Google 76 | * You can get them from the [Google APIs dashboard][google-apis]. 77 | 78 | [google-apis]: https://console.developers.google.com/apis/credentials 79 | 80 | ## Linting 81 | 82 | Linters are fundamental to any project. They ensure that your code 83 | has a consistent style, which is critical to writing readable code. 84 | 85 | Boilermaker comes with a working linter (ESLint, with 86 | `eslint-config-fullstack`) "out of the box." However, everyone has 87 | their own style, so we recommend that you and your team work out yours 88 | and stick to it. Any linter rule that you object to can be "turned 89 | off" in `.eslintrc.json`. You may also choose an entirely different 90 | config if you don't like ours: 91 | 92 | * [Standard style guide](https://standardjs.com/) 93 | * [Airbnb style guide](https://github.com/airbnb/javascript) 94 | * [Google style guide](https://google.github.io/styleguide/jsguide.html) 95 | 96 | ## Start 97 | 98 | Running `npm run start-dev` will make great things happen! 99 | 100 | If you want to run the server and/or `webpack` separately, you can also 101 | `npm run start-server` and `npm run build-client`. 102 | 103 | From there, just follow your bliss. 104 | 105 | ## Deployment 106 | 107 | Ready to go world wide? Here's a guide to deployment! There are two 108 | supported ways to deploy in Boilermaker: 109 | 110 | * automatically, via continuous deployment with Travis. 111 | * "manually", from your local machine via the `deploy` script. 112 | 113 | Either way, you'll need to set up your deployment server to start. 114 | The steps below are also covered in the CI/CD workshop. 115 | 116 | ### Heroku 117 | 118 | 1. Set up the [Heroku command line tools][heroku-cli] 119 | 2. `heroku login` 120 | 3. Add a git remote for heroku: 121 | 122 | [heroku-cli]: https://devcenter.heroku.com/articles/heroku-cli 123 | 124 | * **If you are creating a new app...** 125 | 126 | 1. `heroku create` or `heroku create your-app-name` if you have a 127 | name in mind. 128 | 2. `heroku addons:create heroku-postgresql:hobby-dev` to add 129 | ("provision") a postgres database to your heroku dyno 130 | 131 | * **If you already have a Heroku app...** 132 | 133 | 1. `heroku git:remote your-app-name` You'll need to be a 134 | collaborator on the app. 135 | 136 | ### Travis 137 | 138 | _**NOTE**_ that this step assumes that Travis-CI is already testing your code. 139 | Continuous Integration is not about testing per se – it's about _continuously 140 | integrating_ your changes into the live application, instead of periodically 141 | _releasing_ new versions. CI tools can not only test your code, but then 142 | automatically deploy your app. This is known as Continuous Deployment. 143 | Boilermaker comes with a `.travis.yml` configuration almost ready for 144 | continuous deployment; follow these steps to the job. 145 | 146 | 1. Run the following commands to create a new branch: 147 | 148 | ``` 149 | git checkout master 150 | git pull 151 | git checkout -b f/travis-deploy 152 | ``` 153 | 154 | 2. Run the following script to finish configuring `travis.yml` : 155 | `npm run heroku-token` 156 | This will use your `heroku` CLI (that you configured previously, if 157 | not then see [above](#Heroku)) to generate an authentication token. It 158 | will then use `openssl` to encrypt this token using a public key that 159 | Travis has generated for you. It will then update your `.travis.yml` 160 | file with the encrypted value to be sent with the `secure` key under 161 | the `api_key`. 162 | 3. Run the following commands to commit these changes 163 | 164 | ``` 165 | git add .travis.yml 166 | git commit -m 'travis: activate deployment' 167 | git push -u origin f/travis-deploy 168 | ``` 169 | 170 | 4. Make a Pull Request for the new branch, get it approved, and merge it into 171 | the master branch. 172 | 173 | _**NOTE**_ that this script depends on your local `origin` Git remote matching 174 | your GitHub URL, and your local `heroku` remote matching the name of your 175 | Heroku app. This is only an issue if you rename your GitHub organization, 176 | repository name or Heroku app name. You can update these values using 177 | `git remote` and its related commands. 178 | 179 | #### Travis CLI 180 | 181 | There is a procedure to complete the above steps by installing the official 182 | [Travis CLI tools][travis-cli]. This requires a recent Ruby, but this step 183 | should not be, strictly speaking, necessary. Only explore this option when the 184 | above has failed. 185 | 186 | [travis-cli]: https://github.com/travis-ci/travis.rb#installation 187 | 188 | That's it! From now on, whenever `master` is updated on GitHub, Travis 189 | will automatically push the app to Heroku for you. 190 | 191 | ### Cody's own deploy script 192 | 193 | Your local copy of the application can be pushed up to Heroku at will, 194 | using Boilermaker's handy deployment script: 195 | 196 | 1. Make sure that all your work is fully committed and merged into your 197 | master branch on Github. 198 | 2. If you currently have an existing branch called "deploy", delete 199 | it now (`git branch -d deploy`). We will use a dummy branch 200 | with the name `deploy` (see below), so and the script below will error if a 201 | branch with that name already exists. 202 | 3. `npm run deploy` 203 | _ this will cause the following commands to happen in order: 204 | _ `git checkout -b deploy`: checks out a new branch called 205 | `deploy`. Note that the name `deploy` here is not magical, but it needs 206 | to match the name of the branch we specify when we push to our `heroku` 207 | remote. 208 | _ `webpack -p`: webpack will run in "production mode" 209 | _ `git add -f public/bundle.js public/bundle.js.map`: "force" add 210 | these files which are listed in `.gitignore`. 211 | _ `git commit --allow-empty -m 'Deploying'`: create a commit, even 212 | if nothing changed 213 | _ `git push --force heroku deploy:master`: push your local 214 | `deploy` branch to the `master` branch on `heroku` 215 | _ `git checkout master`: return to your master branch 216 | _ `git branch -D deploy`: remove the deploy branch 217 | 218 | Now, you should be deployed! 219 | 220 | Why do all of these steps? The big reason is because we don't want our 221 | production server to be cluttered up with dev dependencies like 222 | `webpack`, but at the same time we don't want our development 223 | git-tracking to be cluttered with production build files like 224 | `bundle.js`! By doing these steps, we make sure our development and 225 | production environments both stay nice and clean! 226 | -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import {Navbar} from './components' 4 | import Routes from './routes' 5 | 6 | const App = () => { 7 | return ( 8 |
9 | 10 | 11 |
12 | ) 13 | } 14 | 15 | export default App 16 | -------------------------------------------------------------------------------- /client/components/auth-form.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {connect} from 'react-redux' 3 | import PropTypes from 'prop-types' 4 | import {auth} from '../store' 5 | 6 | /** 7 | * COMPONENT 8 | */ 9 | const AuthForm = props => { 10 | const {name, displayName, handleSubmit, error} = props 11 | 12 | return ( 13 |
14 |
15 |
16 | 19 | 20 |
21 |
22 | 25 | 26 |
27 |
28 | 29 |
30 | {error && error.response &&
{error.response.data}
} 31 |
32 | {displayName} with Google 33 |
34 | ) 35 | } 36 | 37 | /** 38 | * CONTAINER 39 | * Note that we have two different sets of 'mapStateToProps' functions - 40 | * one for Login, and one for Signup. However, they share the same 'mapDispatchToProps' 41 | * function, and share the same Component. This is a good example of how we 42 | * can stay DRY with interfaces that are very similar to each other! 43 | */ 44 | const mapLogin = state => { 45 | return { 46 | name: 'login', 47 | displayName: 'Login', 48 | error: state.user.error 49 | } 50 | } 51 | 52 | const mapSignup = state => { 53 | return { 54 | name: 'signup', 55 | displayName: 'Sign Up', 56 | error: state.user.error 57 | } 58 | } 59 | 60 | const mapDispatch = dispatch => { 61 | return { 62 | handleSubmit(evt) { 63 | evt.preventDefault() 64 | const formName = evt.target.name 65 | const email = evt.target.email.value 66 | const password = evt.target.password.value 67 | dispatch(auth(email, password, formName)) 68 | } 69 | } 70 | } 71 | 72 | export const Login = connect(mapLogin, mapDispatch)(AuthForm) 73 | export const Signup = connect(mapSignup, mapDispatch)(AuthForm) 74 | 75 | /** 76 | * PROP TYPES 77 | */ 78 | AuthForm.propTypes = { 79 | name: PropTypes.string.isRequired, 80 | displayName: PropTypes.string.isRequired, 81 | handleSubmit: PropTypes.func.isRequired, 82 | error: PropTypes.object 83 | } 84 | -------------------------------------------------------------------------------- /client/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `components/index.js` exists simply as a 'central export' for our components. 3 | * This way, we can import all of our components from the same place, rather than 4 | * having to figure out which file they belong to! 5 | */ 6 | export {default as Navbar} from './navbar' 7 | export {default as UserHome} from './user-home' 8 | export {Login, Signup} from './auth-form' 9 | -------------------------------------------------------------------------------- /client/components/navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {connect} from 'react-redux' 4 | import {Link} from 'react-router-dom' 5 | import {logout} from '../store' 6 | 7 | const Navbar = ({handleClick, isLoggedIn}) => ( 8 |
9 |

BOILERMAKER

10 | 27 |
28 |
29 | ) 30 | 31 | /** 32 | * CONTAINER 33 | */ 34 | const mapState = state => { 35 | return { 36 | isLoggedIn: !!state.user.id 37 | } 38 | } 39 | 40 | const mapDispatch = dispatch => { 41 | return { 42 | handleClick() { 43 | dispatch(logout()) 44 | } 45 | } 46 | } 47 | 48 | export default connect(mapState, mapDispatch)(Navbar) 49 | 50 | /** 51 | * PROP TYPES 52 | */ 53 | Navbar.propTypes = { 54 | handleClick: PropTypes.func.isRequired, 55 | isLoggedIn: PropTypes.bool.isRequired 56 | } 57 | -------------------------------------------------------------------------------- /client/components/user-home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {connect} from 'react-redux' 4 | 5 | /** 6 | * COMPONENT 7 | */ 8 | export const UserHome = props => { 9 | const {email} = props 10 | 11 | return ( 12 |
13 |

Welcome, {email}

14 |
15 | ) 16 | } 17 | 18 | /** 19 | * CONTAINER 20 | */ 21 | const mapState = state => { 22 | return { 23 | email: state.user.email 24 | } 25 | } 26 | 27 | export default connect(mapState)(UserHome) 28 | 29 | /** 30 | * PROP TYPES 31 | */ 32 | UserHome.propTypes = { 33 | email: PropTypes.string 34 | } 35 | -------------------------------------------------------------------------------- /client/components/user-home.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe beforeEach it */ 2 | 3 | import {expect} from 'chai' 4 | import React from 'react' 5 | import enzyme, {shallow} from 'enzyme' 6 | import Adapter from 'enzyme-adapter-react-16' 7 | import {UserHome} from './user-home' 8 | 9 | const adapter = new Adapter() 10 | enzyme.configure({adapter}) 11 | 12 | describe('UserHome', () => { 13 | let userHome 14 | 15 | beforeEach(() => { 16 | userHome = shallow() 17 | }) 18 | 19 | it('renders the email in an h3', () => { 20 | expect(userHome.find('h3').text()).to.be.equal('Welcome, cody@email.com') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /client/history.js: -------------------------------------------------------------------------------- 1 | import {createMemoryHistory, createBrowserHistory} from 'history' 2 | 3 | const history = 4 | process.env.NODE_ENV === 'test' 5 | ? createMemoryHistory() 6 | : createBrowserHistory() 7 | 8 | export default history 9 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import {Provider} from 'react-redux' 4 | import {Router} from 'react-router-dom' 5 | import history from './history' 6 | import store from './store' 7 | import App from './app' 8 | 9 | // establishes socket connection 10 | import './socket' 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById('app') 19 | ) 20 | -------------------------------------------------------------------------------- /client/routes.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {connect} from 'react-redux' 3 | import {withRouter, Route, Switch} from 'react-router-dom' 4 | import PropTypes from 'prop-types' 5 | import {Login, Signup, UserHome} from './components' 6 | import {me} from './store' 7 | 8 | /** 9 | * COMPONENT 10 | */ 11 | class Routes extends Component { 12 | componentDidMount() { 13 | this.props.loadInitialData() 14 | } 15 | 16 | render() { 17 | const {isLoggedIn} = this.props 18 | 19 | return ( 20 | 21 | {/* Routes placed here are available to all visitors */} 22 | 23 | 24 | {isLoggedIn && ( 25 | 26 | {/* Routes placed here are only available after logging in */} 27 | 28 | 29 | )} 30 | {/* Displays our Login component as a fallback */} 31 | 32 | 33 | ) 34 | } 35 | } 36 | 37 | /** 38 | * CONTAINER 39 | */ 40 | const mapState = state => { 41 | return { 42 | // Being 'logged in' for our purposes will be defined has having a state.user that has a truthy id. 43 | // Otherwise, state.user will be an empty object, and state.user.id will be falsey 44 | isLoggedIn: !!state.user.id 45 | } 46 | } 47 | 48 | const mapDispatch = dispatch => { 49 | return { 50 | loadInitialData() { 51 | dispatch(me()) 52 | } 53 | } 54 | } 55 | 56 | // The `withRouter` wrapper makes sure that updates are not blocked 57 | // when the url changes 58 | export default withRouter(connect(mapState, mapDispatch)(Routes)) 59 | 60 | /** 61 | * PROP TYPES 62 | */ 63 | Routes.propTypes = { 64 | loadInitialData: PropTypes.func.isRequired, 65 | isLoggedIn: PropTypes.bool.isRequired 66 | } 67 | -------------------------------------------------------------------------------- /client/socket.js: -------------------------------------------------------------------------------- 1 | import io from 'socket.io-client' 2 | 3 | const socket = io(window.location.origin) 4 | 5 | socket.on('connect', () => { 6 | console.log('Connected!') 7 | }) 8 | 9 | export default socket 10 | -------------------------------------------------------------------------------- /client/store/index.js: -------------------------------------------------------------------------------- 1 | import {createStore, combineReducers, applyMiddleware} from 'redux' 2 | import {createLogger} from 'redux-logger' 3 | import thunkMiddleware from 'redux-thunk' 4 | import {composeWithDevTools} from 'redux-devtools-extension' 5 | import user from './user' 6 | 7 | const reducer = combineReducers({user}) 8 | const middleware = composeWithDevTools( 9 | applyMiddleware(thunkMiddleware, createLogger({collapsed: true})) 10 | ) 11 | const store = createStore(reducer, middleware) 12 | 13 | export default store 14 | export * from './user' 15 | -------------------------------------------------------------------------------- /client/store/user.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import history from '../history' 3 | 4 | /** 5 | * ACTION TYPES 6 | */ 7 | const GET_USER = 'GET_USER' 8 | const REMOVE_USER = 'REMOVE_USER' 9 | 10 | /** 11 | * INITIAL STATE 12 | */ 13 | const defaultUser = {} 14 | 15 | /** 16 | * ACTION CREATORS 17 | */ 18 | const getUser = user => ({type: GET_USER, user}) 19 | const removeUser = () => ({type: REMOVE_USER}) 20 | 21 | /** 22 | * THUNK CREATORS 23 | */ 24 | export const me = () => async dispatch => { 25 | try { 26 | const res = await axios.get('/auth/me') 27 | dispatch(getUser(res.data || defaultUser)) 28 | } catch (err) { 29 | console.error(err) 30 | } 31 | } 32 | 33 | export const auth = (email, password, method) => async dispatch => { 34 | let res 35 | try { 36 | res = await axios.post(`/auth/${method}`, {email, password}) 37 | } catch (authError) { 38 | return dispatch(getUser({error: authError})) 39 | } 40 | 41 | try { 42 | dispatch(getUser(res.data)) 43 | history.push('/home') 44 | } catch (dispatchOrHistoryErr) { 45 | console.error(dispatchOrHistoryErr) 46 | } 47 | } 48 | 49 | export const logout = () => async dispatch => { 50 | try { 51 | await axios.post('/auth/logout') 52 | dispatch(removeUser()) 53 | history.push('/login') 54 | } catch (err) { 55 | console.error(err) 56 | } 57 | } 58 | 59 | /** 60 | * REDUCER 61 | */ 62 | export default function(state = defaultUser, action) { 63 | switch (action.type) { 64 | case GET_USER: 65 | return action.user 66 | case REMOVE_USER: 67 | return defaultUser 68 | default: 69 | return state 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /client/store/user.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe beforeEach afterEach it */ 2 | 3 | import {expect} from 'chai' 4 | import {me, logout} from './user' 5 | import axios from 'axios' 6 | import MockAdapter from 'axios-mock-adapter' 7 | import configureMockStore from 'redux-mock-store' 8 | import thunkMiddleware from 'redux-thunk' 9 | import history from '../history' 10 | 11 | const middlewares = [thunkMiddleware] 12 | const mockStore = configureMockStore(middlewares) 13 | 14 | describe('thunk creators', () => { 15 | let store 16 | let mockAxios 17 | 18 | const initialState = {user: {}} 19 | 20 | beforeEach(() => { 21 | mockAxios = new MockAdapter(axios) 22 | store = mockStore(initialState) 23 | }) 24 | 25 | afterEach(() => { 26 | mockAxios.restore() 27 | store.clearActions() 28 | }) 29 | 30 | describe('me', () => { 31 | it('eventually dispatches the GET USER action', async () => { 32 | const fakeUser = {email: 'Cody'} 33 | mockAxios.onGet('/auth/me').replyOnce(200, fakeUser) 34 | await store.dispatch(me()) 35 | const actions = store.getActions() 36 | expect(actions[0].type).to.be.equal('GET_USER') 37 | expect(actions[0].user).to.be.deep.equal(fakeUser) 38 | }) 39 | }) 40 | 41 | describe('logout', () => { 42 | it('logout: eventually dispatches the REMOVE_USER action', async () => { 43 | mockAxios.onPost('/auth/logout').replyOnce(204) 44 | await store.dispatch(logout()) 45 | const actions = store.getActions() 46 | expect(actions[0].type).to.be.equal('REMOVE_USER') 47 | expect(history.location.pathname).to.be.equal('/login') 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boilermaker", 3 | "version": "2.0.0", 4 | "description": "Some boilerplate code to get you started - get shakin'!", 5 | "engines": { 6 | "node": "~12.11.1" 7 | }, 8 | "main": "index.js", 9 | "scripts": { 10 | "build-client": "webpack", 11 | "build-client-watch": "webpack -w", 12 | "deploy": "script/deploy", 13 | "heroku-token": "script/encrypt-heroku-auth-token.js", 14 | "lint": "eslint ./ --ignore-path .gitignore", 15 | "lint-fix": "npm run lint -- --fix", 16 | "prepare": "if [ -d .git ]; then npm-merge-driver install; fi", 17 | "prettify": "prettier --write \"**/*.{js,jsx,json,css,scss,md}\"", 18 | "postinstall": "touch secrets.js", 19 | "seed": "node script/seed.js", 20 | "start": "node server", 21 | "start-dev": "NODE_ENV='development' npm run build-client-watch & NODE_ENV='development' npm run start-server", 22 | "start-server": "nodemon server -e html,js,scss --ignore public --ignore client", 23 | "test": "NODE_ENV='test' mocha \"./server/**/*.spec.js\" \"./client/**/*.spec.js\" \"./script/**/*.spec.js\" --require @babel/polyfill --require @babel/register" 24 | }, 25 | "husky": { 26 | "hooks": { 27 | "pre-commit": "lint-staged" 28 | } 29 | }, 30 | "lint-staged": { 31 | "*.{js,jsx}": [ 32 | "prettier --write", 33 | "npm run lint-fix", 34 | "git add" 35 | ], 36 | "*.{css,scss,json,md}": [ 37 | "prettier --write", 38 | "git add" 39 | ] 40 | }, 41 | "author": "Fullstack Academy of Code", 42 | "license": "MIT", 43 | "dependencies": { 44 | "axios": "^0.19.0", 45 | "compression": "^1.7.3", 46 | "connect-session-sequelize": "^6.0.0", 47 | "express": "^4.16.4", 48 | "express-session": "^1.15.1", 49 | "history": "^4.9.0", 50 | "morgan": "^1.9.1", 51 | "passport": "^0.4.0", 52 | "passport-google-oauth": "^2.0.0", 53 | "pg": "^8.3.2", 54 | "pg-hstore": "^2.3.2", 55 | "prop-types": "^15.7.2", 56 | "react": "^16.8.6", 57 | "react-dom": "^16.8.6", 58 | "react-redux": "^7.0.1", 59 | "react-router-dom": "^5.0.0", 60 | "redux": "^4.0.1", 61 | "redux-logger": "^3.0.6", 62 | "redux-thunk": "^2.3.0", 63 | "sequelize": "^5.3.1", 64 | "socket.io": "^2.2.0" 65 | }, 66 | "devDependencies": { 67 | "@babel/core": "^7.4.3", 68 | "@babel/plugin-proposal-class-properties": "7.4.0", 69 | "@babel/plugin-proposal-decorators": "7.4.0", 70 | "@babel/plugin-proposal-export-namespace-from": "7.2.0", 71 | "@babel/plugin-proposal-function-sent": "7.2.0", 72 | "@babel/plugin-proposal-json-strings": "7.2.0", 73 | "@babel/plugin-proposal-numeric-separator": "7.2.0", 74 | "@babel/plugin-proposal-throw-expressions": "7.2.0", 75 | "@babel/plugin-syntax-dynamic-import": "7.2.0", 76 | "@babel/plugin-syntax-import-meta": "7.2.0", 77 | "@babel/polyfill": "^7.4.3", 78 | "@babel/preset-env": "^7.4.3", 79 | "@babel/preset-react": "^7.0.0", 80 | "@babel/register": "^7.4.0", 81 | "axios-mock-adapter": "^1.16.0", 82 | "babel-eslint": "^10.0.1", 83 | "babel-loader": "^8.0.5", 84 | "chai": "^4.2.0", 85 | "enzyme": "^3.9.0", 86 | "enzyme-adapter-react-16": "^1.12.1", 87 | "eslint": "^5.16.0", 88 | "eslint-config-fullstack": "^6.0.0", 89 | "eslint-config-prettier": "^4.1.0", 90 | "eslint-plugin-react": "^7.12.4", 91 | "git-url-parse": "^11.1.2", 92 | "husky": "^1.3.1", 93 | "lint-staged": "^8.1.5", 94 | "mocha": "^6.1.4", 95 | "nodemon": "^1.18.3", 96 | "npm-merge-driver": "^2.3.5", 97 | "prettier": "1.11.1", 98 | "react-test-renderer": "^16.4.2", 99 | "redux-devtools-extension": "^2.13.5", 100 | "redux-mock-store": "^1.5.3", 101 | "simple-git": "^1.121.0", 102 | "supertest": "^4.0.2", 103 | "webpack": "^4.16.4", 104 | "webpack-cli": "^3.1.0", 105 | "yaml": "^1.6.0" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FullstackAcademy/boilermaker/c8d3ece525c31a27330233fd6cc01e40aa2b798a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Boilermaker 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | 5 | a { 6 | text-decoration: none; 7 | } 8 | 9 | label { 10 | display: block; 11 | } 12 | 13 | nav a { 14 | display: inline-block; 15 | margin: 1em; 16 | } 17 | 18 | form div { 19 | margin: 1em; 20 | display: inline-block; 21 | } 22 | -------------------------------------------------------------------------------- /script/deploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Hello, welcome to a bash script. 4 | 5 | # This bash script deploys your boilermaker application. 6 | 7 | # On the terminal you run individual 8 | # bash commands, and this file strings a bunch of commands together. 9 | 10 | # The first line of this file, or the `hashbang`, tells the system to 11 | # execute the text of this file as a bash program. 12 | 13 | # We want this entire script to exit if any single line fails. 14 | # So we set the `-e` flag. 15 | set -e 16 | 17 | # If our deploy fails partway through we want to clean up after ourselves. 18 | # This next block is like a try/catch for our entire script. 19 | 20 | # We trap any program EXIT and run this function. 21 | # Whether the deploy succeeds or fails, we'll clean up the deploy branch. 22 | 23 | function cleanup_at_exit { 24 | # return to your master branch 25 | git checkout master 26 | 27 | # remove the deploy branch 28 | git branch -D deploy 29 | } 30 | trap cleanup_at_exit EXIT 31 | 32 | # checks out a new branch called "deploy". Note that the name "deploy" here isn't magical, 33 | # but it needs to match the name of the branch we specify when we push to our heroku remote. 34 | git checkout -b deploy 35 | 36 | # webpack will run in "production mode" 37 | webpack -p 38 | 39 | # "force" add the otherwise gitignored build files 40 | git add -f public/bundle.js public/bundle.js.map 41 | 42 | # create a commit, even if nothing changed 43 | git commit --allow-empty -m 'Deploying' 44 | 45 | # push your local "deploy" branch to the "master" branch on heroku 46 | git push --force heroku deploy:master 47 | -------------------------------------------------------------------------------- /script/encrypt-heroku-auth-token.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {spawn} = require('child_process') 4 | const fs = require('fs') 5 | 6 | const axios = require('axios') 7 | const GitUrlParse = require('git-url-parse') 8 | const simpleGit = require('simple-git')() 9 | const YAML = require('yaml') 10 | 11 | /* Specific message contents stored as constants */ 12 | 13 | const keyComments = require('./keyComments.json') 14 | 15 | const idempotenceMessage = `It appears that your token has been encrypted. 16 | To run this script again, delete the \`before_deploy\` and \`deploy\` keys 17 | from the .travis.yml file.` 18 | 19 | const successMessage = `Complete! Run \`git diff .travis.yml\` to check.` 20 | 21 | /* Clean up system state changes. */ 22 | const clean = () => { 23 | const externalFiles = ['.tmp.key.pem', '.tmp.token.txt', '.tmp.token.enc'] 24 | externalFiles.forEach(file => { 25 | if (fs.existsSync(file)) fs.unlinkSync(file) 26 | }) 27 | } 28 | 29 | /* Get a specific git remote URL. */ 30 | const getRemoteURL = (name, remotes) => { 31 | try { 32 | return remotes.filter(remote => remote.name === name)[0].refs.fetch 33 | } catch (err) { 34 | console.log( 35 | `It appears that the remote ${name} does not exist.`, 36 | `Here is the full error:`, 37 | err 38 | ) 39 | } 40 | } 41 | 42 | /* Run a command and return its stdout. */ 43 | const getOutputFromCommand = async (command, args) => { 44 | const response = await new Promise((resolve, reject) => { 45 | const process = spawn(command, args) 46 | 47 | const stdout = [] 48 | const stderr = [] 49 | 50 | process.stdout.on('data', data => { 51 | stdout.push(data) 52 | }) 53 | 54 | process.stderr.on('data', data => { 55 | stderr.push(data) 56 | }) 57 | 58 | process.on('close', code => { 59 | if (code) throw new Error(reject(stderr)) 60 | resolve(stdout) 61 | }) 62 | }) 63 | return response 64 | } 65 | 66 | /* Use git remote URLs to get app identifiers. */ 67 | const getNamesFromGit = () => 68 | new Promise((resolve, reject) => 69 | simpleGit.getRemotes(true, (err, res) => { 70 | if (err) throw new Error(reject(err)) 71 | resolve({ 72 | fullName: GitUrlParse(getRemoteURL('origin', res)).full_name, 73 | appName: GitUrlParse(getRemoteURL('heroku', res)).name 74 | }) 75 | }) 76 | ) 77 | 78 | /* Use the openssl command to encrypt an authentication token. */ 79 | const encryptHerokuToken = async () => { 80 | await getOutputFromCommand('openssl', [ 81 | 'rsautl', 82 | '-encrypt', 83 | '-pubin', 84 | '-inkey', 85 | '.tmp.key.pem', 86 | '-in', 87 | '.tmp.token.txt', 88 | '-out', 89 | '.tmp.token.enc' 90 | ]) 91 | } 92 | 93 | /* Write the encrypted key, and other values, to the .travis.yml file. */ 94 | const updateTravisYAML = (app, key) => { 95 | const travis = fs.readFileSync('.travis.yml', 'utf8') 96 | const doc = YAML.parseDocument(travis) 97 | if (doc.has('before_deploy')) { 98 | return console.log(idempotenceMessage) 99 | } 100 | doc.set('before_deploy', ['rm -rf node_modules']) 101 | doc.set( 102 | 'deploy', 103 | YAML.createNode({ 104 | skip_cleanup: true, //eslint-disable-line 105 | provider: 'heroku', 106 | app: app, 107 | api_key: {secure: key} //eslint-disable-line 108 | }) 109 | ) 110 | doc.contents.items.filter(item => item.key in keyComments).forEach(item => { 111 | item.comment = keyComments[item.key] 112 | if (item.key === 'deploy') { 113 | item.value.items.forEach(item_ => { 114 | item_.commentBefore = keyComments[item_.key] 115 | }) 116 | } 117 | }) 118 | doc.comment = '' 119 | fs.writeFileSync('.travis.yml', doc.toString()) 120 | return true 121 | } 122 | 123 | const main = async () => { 124 | const verbose = process.argv.hasOwnProperty(2) 125 | const {fullName, appName} = await getNamesFromGit() 126 | 127 | /* Get Heroku authentication token from the Heroku CLI. */ 128 | const herokuTokenOut = await getOutputFromCommand('heroku', ['auth:token']) 129 | const herokuTokenStr = herokuTokenOut.toString('utf-8') 130 | const herokuToken = herokuTokenStr.slice(0, herokuTokenStr.length - 1) 131 | if (verbose) console.log('Received Heroku token', herokuToken.toString()) 132 | 133 | /* Download the repo's public key supplied by Travis. */ 134 | const travisURL = `https://api.travis-ci.org/repos/${fullName}/key` 135 | const travisResponse = await axios.get(travisURL) 136 | const key = travisResponse.data.key 137 | const keyBuffer = Buffer.from(key, 'utf-8') 138 | if (verbose) console.log('Received Travis pubkey:\n', keyBuffer.toString()) 139 | 140 | /* Write files for use with openssl */ 141 | fs.writeFileSync('.tmp.key.pem', key) 142 | fs.writeFileSync('.tmp.token.txt', herokuToken) 143 | 144 | /* Encrypt the Heroku token and save it in the .tmp.token.enc file. */ 145 | await encryptHerokuToken() 146 | 147 | /* Encode the encrypted data in base64. */ 148 | const keyBase64 = fs.readFileSync('.tmp.token.enc').toString('base64') 149 | if (verbose) console.log('Encrypted key base 64 encoded:', keyBase64) 150 | 151 | /* Delete temporary files. */ 152 | clean() 153 | 154 | /* Add the encrypted key to the .travis.yml file. */ 155 | const update = updateTravisYAML(appName, keyBase64) 156 | if (update) console.log(successMessage) 157 | 158 | /* Clean up in the case of unspecified errors. */ 159 | process.on('uncaughtException', () => { 160 | clean() 161 | if (verbose) console.log('Cleaned up on error!') 162 | process.exit(1) 163 | }) 164 | 165 | process.on('unhandledRejection', () => { 166 | clean() 167 | if (verbose) console.log('Cleaned up on error!') 168 | process.exit(1) 169 | }) 170 | } 171 | 172 | if (require.main === module) { 173 | main() 174 | } 175 | -------------------------------------------------------------------------------- /script/keyComments.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_key": " the secure key indicates an encrypted value; see README", 3 | "app": " app should be your heroku app name; see README", 4 | "before_deploy": " omit node_modules, since we set skip_cleanup below", 5 | "deploy": " see README for details on these keys", 6 | "skip_cleanup": " prevents travis from deleting the build" 7 | } 8 | -------------------------------------------------------------------------------- /script/seed.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('../server/db') 4 | const {User} = require('../server/db/models') 5 | 6 | async function seed() { 7 | await db.sync({force: true}) 8 | console.log('db synced!') 9 | 10 | const users = await Promise.all([ 11 | User.create({email: 'cody@email.com', password: '123'}), 12 | User.create({email: 'murphy@email.com', password: '123'}) 13 | ]) 14 | 15 | console.log(`seeded ${users.length} users`) 16 | console.log(`seeded successfully`) 17 | } 18 | 19 | // We've separated the `seed` function from the `runSeed` function. 20 | // This way we can isolate the error handling and exit trapping. 21 | // The `seed` function is concerned only with modifying the database. 22 | async function runSeed() { 23 | console.log('seeding...') 24 | try { 25 | await seed() 26 | } catch (err) { 27 | console.error(err) 28 | process.exitCode = 1 29 | } finally { 30 | console.log('closing db connection') 31 | await db.close() 32 | console.log('db connection closed') 33 | } 34 | } 35 | 36 | // Execute the `seed` function, IF we ran this module directly (`node seed`). 37 | // `Async` functions always return a promise, so we can use `catch` to handle 38 | // any errors that might occur inside of `seed`. 39 | if (module === require.main) { 40 | runSeed() 41 | } 42 | 43 | // we export the seed function for testing purposes (see `./seed.spec.js`) 44 | module.exports = seed 45 | -------------------------------------------------------------------------------- /script/seed.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe it */ 3 | 4 | const seed = require('./seed') 5 | 6 | describe('seed script', () => { 7 | it('completes successfully', seed) 8 | }) 9 | -------------------------------------------------------------------------------- /server/api/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | module.exports = router 3 | 4 | router.use('/users', require('./users')) 5 | 6 | router.use((req, res, next) => { 7 | const error = new Error('Not Found') 8 | error.status = 404 9 | next(error) 10 | }) 11 | -------------------------------------------------------------------------------- /server/api/users.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const {User} = require('../db/models') 3 | module.exports = router 4 | 5 | router.get('/', async (req, res, next) => { 6 | try { 7 | const users = await User.findAll({ 8 | // explicitly select only the id and email fields - even though 9 | // users' passwords are encrypted, it won't help if we just 10 | // send everything to anyone who asks! 11 | attributes: ['id', 'email'] 12 | }) 13 | res.json(users) 14 | } catch (err) { 15 | next(err) 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /server/api/users.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe beforeEach it */ 2 | 3 | const {expect} = require('chai') 4 | const request = require('supertest') 5 | const db = require('../db') 6 | const app = require('../index') 7 | const User = db.model('user') 8 | 9 | describe('User routes', () => { 10 | beforeEach(() => { 11 | return db.sync({force: true}) 12 | }) 13 | 14 | describe('/api/users/', () => { 15 | const codysEmail = 'cody@puppybook.com' 16 | 17 | beforeEach(() => { 18 | return User.create({ 19 | email: codysEmail 20 | }) 21 | }) 22 | 23 | it('GET /api/users', async () => { 24 | const res = await request(app) 25 | .get('/api/users') 26 | .expect(200) 27 | 28 | expect(res.body).to.be.an('array') 29 | expect(res.body[0].email).to.be.equal(codysEmail) 30 | }) 31 | }) // end describe('/api/users') 32 | }) // end describe('User routes') 33 | -------------------------------------------------------------------------------- /server/auth/google.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport') 2 | const router = require('express').Router() 3 | const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy 4 | const {User} = require('../db/models') 5 | module.exports = router 6 | 7 | /** 8 | * For OAuth keys and other secrets, your Node process will search 9 | * process.env to find environment variables. On your production server, 10 | * you will be able to set these environment variables with the appropriate 11 | * values. In development, a good practice is to keep a separate file with 12 | * these secrets that you only share with your team - it should NOT be tracked 13 | * by git! In this case, you may use a file called `secrets.js`, which will 14 | * set these environment variables like so: 15 | * 16 | * process.env.GOOGLE_CLIENT_ID = 'your google client id' 17 | * process.env.GOOGLE_CLIENT_SECRET = 'your google client secret' 18 | * process.env.GOOGLE_CALLBACK = '/your/google/callback' 19 | */ 20 | 21 | if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) { 22 | console.log('Google client ID / secret not found. Skipping Google OAuth.') 23 | } else { 24 | const googleConfig = { 25 | clientID: process.env.GOOGLE_CLIENT_ID, 26 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 27 | callbackURL: process.env.GOOGLE_CALLBACK 28 | } 29 | 30 | const strategy = new GoogleStrategy( 31 | googleConfig, 32 | (token, refreshToken, profile, done) => { 33 | const googleId = profile.id 34 | const email = profile.emails[0].value 35 | const imgUrl = profile.photos[0].value 36 | const firstName = profile.name.givenName 37 | const lastName = profile.name.familyName 38 | const fullName = profile.displayName 39 | 40 | User.findOrCreate({ 41 | where: {googleId}, 42 | defaults: {email, imgUrl, firstName, lastName, fullName} 43 | }) 44 | .then(([user]) => done(null, user)) 45 | .catch(done) 46 | } 47 | ) 48 | 49 | passport.use(strategy) 50 | 51 | router.get( 52 | '/', 53 | passport.authenticate('google', {scope: ['email', 'profile']}) 54 | ) 55 | 56 | router.get( 57 | '/callback', 58 | passport.authenticate('google', { 59 | successRedirect: '/home', 60 | failureRedirect: '/login' 61 | }) 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /server/auth/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const User = require('../db/models/user') 3 | module.exports = router 4 | 5 | router.post('/login', async (req, res, next) => { 6 | try { 7 | const user = await User.findOne({where: {email: req.body.email}}) 8 | if (!user) { 9 | console.log('No such user found:', req.body.email) 10 | res.status(401).send('Wrong username and/or password') 11 | } else if (!user.correctPassword(req.body.password)) { 12 | console.log('Incorrect password for user:', req.body.email) 13 | res.status(401).send('Wrong username and/or password') 14 | } else { 15 | req.login(user, err => (err ? next(err) : res.json(user))) 16 | } 17 | } catch (err) { 18 | next(err) 19 | } 20 | }) 21 | 22 | router.post('/signup', async (req, res, next) => { 23 | try { 24 | const user = await User.create(req.body) 25 | req.login(user, err => (err ? next(err) : res.json(user))) 26 | } catch (err) { 27 | if (err.name === 'SequelizeUniqueConstraintError') { 28 | res.status(401).send('User already exists') 29 | } else { 30 | next(err) 31 | } 32 | } 33 | }) 34 | 35 | router.post('/logout', (req, res) => { 36 | req.logout() 37 | req.session.destroy() 38 | res.redirect('/') 39 | }) 40 | 41 | router.get('/me', (req, res) => { 42 | res.json(req.user) 43 | }) 44 | 45 | router.use('/google', require('./google')) 46 | -------------------------------------------------------------------------------- /server/db/db.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize') 2 | const pkg = require('../../package.json') 3 | 4 | const databaseName = pkg.name + (process.env.NODE_ENV === 'test' ? '-test' : '') 5 | 6 | let config 7 | 8 | if (process.env.DATABASE_URL) { 9 | config = { 10 | logging: false, 11 | ssl: true, 12 | dialectOptions: { 13 | ssl: { 14 | require: true, 15 | rejectUnauthorized: false 16 | } 17 | } 18 | } 19 | } else { 20 | config = { 21 | logging: false 22 | } 23 | } 24 | 25 | const db = new Sequelize( 26 | process.env.DATABASE_URL || `postgres://localhost:5432/${databaseName}`, 27 | config 28 | ) 29 | 30 | module.exports = db 31 | 32 | // This is a global Mocha hook used for resource cleanup. 33 | // Otherwise, Mocha v4+ does not exit after tests. 34 | if (process.env.NODE_ENV === 'test') { 35 | after('close database connection', () => db.close()) 36 | } 37 | -------------------------------------------------------------------------------- /server/db/index.js: -------------------------------------------------------------------------------- 1 | const db = require('./db') 2 | 3 | // register models 4 | require('./models') 5 | 6 | module.exports = db 7 | -------------------------------------------------------------------------------- /server/db/models/index.js: -------------------------------------------------------------------------------- 1 | const User = require('./user') 2 | 3 | /** 4 | * If we had any associations to make, this would be a great place to put them! 5 | * ex. if we had another model called BlogPost, we might say: 6 | * 7 | * BlogPost.belongsTo(User) 8 | */ 9 | 10 | /** 11 | * We'll export all of our models here, so that any time a module needs a model, 12 | * we can just require it from 'db/models' 13 | * for example, we can say: const {User} = require('../db/models') 14 | * instead of: const User = require('../db/models/user') 15 | */ 16 | module.exports = { 17 | User 18 | } 19 | -------------------------------------------------------------------------------- /server/db/models/user.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const Sequelize = require('sequelize') 3 | const db = require('../db') 4 | 5 | const User = db.define('user', { 6 | email: { 7 | type: Sequelize.STRING, 8 | unique: true, 9 | allowNull: false 10 | }, 11 | password: { 12 | type: Sequelize.STRING, 13 | // Making `.password` act like a func hides it when serializing to JSON. 14 | // This is a hack to get around Sequelize's lack of a "private" option. 15 | get() { 16 | return () => this.getDataValue('password') 17 | } 18 | }, 19 | salt: { 20 | type: Sequelize.STRING, 21 | // Making `.salt` act like a function hides it when serializing to JSON. 22 | // This is a hack to get around Sequelize's lack of a "private" option. 23 | get() { 24 | return () => this.getDataValue('salt') 25 | } 26 | }, 27 | googleId: { 28 | type: Sequelize.STRING 29 | } 30 | }) 31 | 32 | module.exports = User 33 | 34 | /** 35 | * instanceMethods 36 | */ 37 | User.prototype.correctPassword = function(candidatePwd) { 38 | return User.encryptPassword(candidatePwd, this.salt()) === this.password() 39 | } 40 | 41 | /** 42 | * classMethods 43 | */ 44 | User.generateSalt = function() { 45 | return crypto.randomBytes(16).toString('base64') 46 | } 47 | 48 | User.encryptPassword = function(plainText, salt) { 49 | return crypto 50 | .createHash('RSA-SHA256') 51 | .update(plainText) 52 | .update(salt) 53 | .digest('hex') 54 | } 55 | 56 | /** 57 | * hooks 58 | */ 59 | const setSaltAndPassword = user => { 60 | if (user.changed('password')) { 61 | user.salt = User.generateSalt() 62 | user.password = User.encryptPassword(user.password(), user.salt()) 63 | } 64 | } 65 | 66 | User.beforeCreate(setSaltAndPassword) 67 | User.beforeUpdate(setSaltAndPassword) 68 | User.beforeBulkCreate(users => { 69 | users.forEach(setSaltAndPassword) 70 | }) 71 | -------------------------------------------------------------------------------- /server/db/models/user.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe beforeEach it */ 2 | 3 | const {expect} = require('chai') 4 | const db = require('../index') 5 | const User = db.model('user') 6 | 7 | describe('User model', () => { 8 | beforeEach(() => { 9 | return db.sync({force: true}) 10 | }) 11 | 12 | describe('instanceMethods', () => { 13 | describe('correctPassword', () => { 14 | let cody 15 | 16 | beforeEach(async () => { 17 | cody = await User.create({ 18 | email: 'cody@puppybook.com', 19 | password: 'bones' 20 | }) 21 | }) 22 | 23 | it('returns true if the password is correct', () => { 24 | expect(cody.correctPassword('bones')).to.be.equal(true) 25 | }) 26 | 27 | it('returns false if the password is incorrect', () => { 28 | expect(cody.correctPassword('bonez')).to.be.equal(false) 29 | }) 30 | }) // end describe('correctPassword') 31 | }) // end describe('instanceMethods') 32 | }) // end describe('User model') 33 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const express = require('express') 3 | const morgan = require('morgan') 4 | const compression = require('compression') 5 | const session = require('express-session') 6 | const passport = require('passport') 7 | const SequelizeStore = require('connect-session-sequelize')(session.Store) 8 | const db = require('./db') 9 | const sessionStore = new SequelizeStore({db}) 10 | const PORT = process.env.PORT || 8080 11 | const app = express() 12 | const socketio = require('socket.io') 13 | module.exports = app 14 | 15 | // This is a global Mocha hook, used for resource cleanup. 16 | // Otherwise, Mocha v4+ never quits after tests. 17 | if (process.env.NODE_ENV === 'test') { 18 | after('close the session store', () => sessionStore.stopExpiringSessions()) 19 | } 20 | 21 | /** 22 | * In your development environment, you can keep all of your 23 | * app's secret API keys in a file called `secrets.js`, in your project 24 | * root. This file is included in the .gitignore - it will NOT be tracked 25 | * or show up on Github. On your production server, you can add these 26 | * keys as environment variables, so that they can still be read by the 27 | * Node process on process.env 28 | */ 29 | if (process.env.NODE_ENV !== 'production') require('../secrets') 30 | 31 | // passport registration 32 | passport.serializeUser((user, done) => done(null, user.id)) 33 | 34 | passport.deserializeUser(async (id, done) => { 35 | try { 36 | const user = await db.models.user.findByPk(id) 37 | done(null, user) 38 | } catch (err) { 39 | done(err) 40 | } 41 | }) 42 | 43 | const createApp = () => { 44 | // logging middleware 45 | app.use(morgan('dev')) 46 | 47 | // body parsing middleware 48 | app.use(express.json()) 49 | app.use(express.urlencoded({extended: true})) 50 | 51 | // compression middleware 52 | app.use(compression()) 53 | 54 | // session middleware with passport 55 | app.use( 56 | session({ 57 | secret: process.env.SESSION_SECRET || 'my best friend is Cody', 58 | store: sessionStore, 59 | resave: false, 60 | saveUninitialized: false 61 | }) 62 | ) 63 | app.use(passport.initialize()) 64 | app.use(passport.session()) 65 | 66 | // auth and api routes 67 | app.use('/auth', require('./auth')) 68 | app.use('/api', require('./api')) 69 | 70 | // static file-serving middleware 71 | app.use(express.static(path.join(__dirname, '..', 'public'))) 72 | 73 | // any remaining requests with an extension (.js, .css, etc.) send 404 74 | app.use((req, res, next) => { 75 | if (path.extname(req.path).length) { 76 | const err = new Error('Not found') 77 | err.status = 404 78 | next(err) 79 | } else { 80 | next() 81 | } 82 | }) 83 | 84 | // sends index.html 85 | app.use('*', (req, res) => { 86 | res.sendFile(path.join(__dirname, '..', 'public/index.html')) 87 | }) 88 | 89 | // error handling endware 90 | app.use((err, req, res, next) => { 91 | console.error(err) 92 | console.error(err.stack) 93 | res.status(err.status || 500).send(err.message || 'Internal server error.') 94 | }) 95 | } 96 | 97 | const startListening = () => { 98 | // start listening (and create a 'server' object representing our server) 99 | const server = app.listen(PORT, () => 100 | console.log(`Mixing it up on port ${PORT}`) 101 | ) 102 | 103 | // set up our socket control center 104 | const io = socketio(server) 105 | require('./socket')(io) 106 | } 107 | 108 | const syncDb = () => db.sync() 109 | 110 | async function bootApp() { 111 | await sessionStore.sync() 112 | await syncDb() 113 | await createApp() 114 | await startListening() 115 | } 116 | // This evaluates as true when this file is run directly from the command line, 117 | // i.e. when we say 'node server/index.js' (or 'nodemon server/index.js', or 'nodemon server', etc) 118 | // It will evaluate false when this module is required by another module - for example, 119 | // if we wanted to require our app in a test spec 120 | if (require.main === module) { 121 | bootApp() 122 | } else { 123 | createApp() 124 | } 125 | -------------------------------------------------------------------------------- /server/socket/index.js: -------------------------------------------------------------------------------- 1 | module.exports = io => { 2 | io.on('connection', socket => { 3 | console.log(`A socket connection to the server has been made: ${socket.id}`) 4 | 5 | socket.on('disconnect', () => { 6 | console.log(`Connection ${socket.id} has left the building`) 7 | }) 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const isDev = process.env.NODE_ENV === 'development' 2 | 3 | module.exports = { 4 | mode: isDev ? 'development' : 'production', 5 | entry: [ 6 | '@babel/polyfill', // enables async-await 7 | './client/index.js' 8 | ], 9 | output: { 10 | path: __dirname, 11 | filename: './public/bundle.js' 12 | }, 13 | resolve: { 14 | extensions: ['.js', '.jsx'] 15 | }, 16 | devtool: 'source-map', 17 | watchOptions: { 18 | ignored: /node_modules/ 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.jsx?$/, 24 | exclude: /node_modules/, 25 | loader: 'babel-loader' 26 | } 27 | ] 28 | } 29 | } 30 | --------------------------------------------------------------------------------