23 | )
24 |
25 | render(
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | ,
35 | document.getElementById('main')
36 | )
37 |
--------------------------------------------------------------------------------
/app/reducers/auth.jsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const reducer = (state=null, action) => {
4 | switch (action.type) {
5 | case AUTHENTICATED:
6 | return action.user
7 | }
8 | return state
9 | }
10 |
11 | const AUTHENTICATED = 'AUTHENTICATED'
12 | export const authenticated = user => ({
13 | type: AUTHENTICATED, user
14 | })
15 |
16 | export const login = (username, password) =>
17 | dispatch =>
18 | axios.post('/api/auth/login/local',
19 | {username, password})
20 | .then(() => dispatch(whoami()))
21 | .catch(() => dispatch(whoami()))
22 |
23 | export const logout = () =>
24 | dispatch =>
25 | axios.post('/api/auth/logout')
26 | .then(() => dispatch(whoami()))
27 | .catch(() => dispatch(whoami()))
28 |
29 | export const whoami = () =>
30 | dispatch =>
31 | axios.get('/api/auth/whoami')
32 | .then(response => {
33 | const user = response.data
34 | dispatch(authenticated(user))
35 | })
36 | .catch(failed => dispatch(authenticated(null)))
37 |
38 | export default reducer
39 |
--------------------------------------------------------------------------------
/server/users.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const db = require('APP/db')
4 | const User = db.model('users')
5 |
6 | const {mustBeLoggedIn, forbidden} = require('./auth.filters')
7 |
8 | module.exports = require('express').Router()
9 | .get('/',
10 | // The forbidden middleware will fail *all* requests to list users.
11 | // Remove it if you want to allow anyone to list all users on the site.
12 | //
13 | // If you want to only let admins list all the users, then you'll
14 | // have to add a role column to the users table to support
15 | // the concept of admin users.
16 | forbidden('listing users is not allowed'),
17 | (req, res, next) =>
18 | User.findAll()
19 | .then(users => res.json(users))
20 | .catch(next))
21 | .post('/',
22 | (req, res, next) =>
23 | User.create(req.body)
24 | .then(user => res.status(201).json(user))
25 | .catch(next))
26 | .get('/:id',
27 | mustBeLoggedIn,
28 | (req, res, next) =>
29 | User.findById(req.params.id)
30 | .then(user => res.json(user))
31 | .catch(next))
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2017 Fullstack Academy of Code
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const LiveReloadPlugin = require('webpack-livereload-plugin')
4 | , devMode = require('.').isDevelopment
5 |
6 | /**
7 | * Fast source maps rebuild quickly during development, but only give a link
8 | * to the line where the error occurred. The stack trace will show the bundled
9 | * code, not the original code. Keep this on `false` for slower builds but
10 | * usable stack traces. Set to `true` if you want to speed up development.
11 | */
12 |
13 | , USE_FAST_SOURCE_MAPS = false
14 |
15 | module.exports = {
16 | entry: './app/main.jsx',
17 | output: {
18 | path: __dirname,
19 | filename: './public/bundle.js'
20 | },
21 | context: __dirname,
22 | devtool: devMode && USE_FAST_SOURCE_MAPS
23 | ? 'cheap-module-eval-source-map'
24 | : 'source-map',
25 | resolve: {
26 | extensions: ['.js', '.jsx', '.json', '*']
27 | },
28 | module: {
29 | rules: [{
30 | test: /jsx?$/,
31 | exclude: /(node_modules|bower_components)/,
32 | use: [{
33 | loader: 'babel-loader',
34 | options: {
35 | presets: ['react', 'es2015', 'stage-2']
36 | }
37 | }]
38 | }]
39 | },
40 | plugins: devMode
41 | ? [new LiveReloadPlugin({appendScriptTag: true})]
42 | : []
43 | }
44 |
--------------------------------------------------------------------------------
/app/components/Jokes.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import chai, {expect} from 'chai'
3 | chai.use(require('chai-enzyme')())
4 |
5 | import {shallow} from 'enzyme'
6 |
7 | import Jokes from './Jokes'
8 |
9 | /* global describe it beforeEach */
10 | describe('', () => {
11 | const joke = {
12 | q: 'Why did the skeleton write tests?',
13 | a: 'To see if she did anything bone-headed.',
14 | }
15 |
16 | let root
17 | beforeEach('render the root', () =>
18 | root = shallow()
19 | )
20 |
21 | it('shows a joke', () => {
22 | root.setState({ joke, answered: false })
23 | expect(root.find('h1')).to.have.length(1)
24 | expect(root.find('h1').text()).equal(joke.q)
25 | })
26 |
27 | it("doesn't show the answer when state.answered=false", () => {
28 | root.setState({ joke, answered: false })
29 | expect(root.find('h2')).to.have.length(0)
30 | })
31 |
32 | it('shows the answer when state.answered=true', () => {
33 | root.setState({ joke, answered: true })
34 | expect(root.find('h2')).to.have.length(1)
35 | expect(root.find('h2').text()).to.equal(joke.a)
36 | })
37 |
38 | it('when tapped, sets state.answered=true', () => {
39 | root.setState({ joke, answered: false })
40 | root.simulate('click')
41 | expect(root.state().answered).to.be.true
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/server/users.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest')
2 | , {expect} = require('chai')
3 | , db = require('APP/db')
4 | , app = require('./start')
5 |
6 | /* global describe it before afterEach */
7 |
8 | describe('/api/users', () => {
9 | before('Await database sync', () => db.didSync)
10 | afterEach('Clear the tables', () => db.truncate({ cascade: true }))
11 |
12 | describe('GET /:id', () =>
13 | describe('when not logged in', () =>
14 | it('fails with a 401 (Unauthorized)', () =>
15 | request(app)
16 | .get(`/api/users/1`)
17 | .expect(401)
18 | )))
19 |
20 | describe('POST', () =>
21 | describe('when not logged in', () => {
22 | it('creates a user', () =>
23 | request(app)
24 | .post('/api/users')
25 | .send({
26 | email: 'beth@secrets.org',
27 | password: '12345'
28 | })
29 | .expect(201))
30 |
31 | it('redirects to the user it just made', () =>
32 | request(app)
33 | .post('/api/users')
34 | .send({
35 | email: 'eve@interloper.com',
36 | password: '23456',
37 | })
38 | .redirects(1)
39 | .then(res => expect(res.body).to.contain({
40 | email: 'eve@interloper.com'
41 | })))
42 | }))
43 | })
44 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: "eslint-config-standard",
3 | root: true,
4 | parser: "babel-eslint",
5 | parserOptions: {
6 | sourceType: "module",
7 | ecmaVersion: 8
8 | },
9 | ecmaFeatures: {
10 | jsx: true,
11 | },
12 | plugins: ['react'],
13 | rules: {
14 | "space-before-function-paren": ["error", "never"],
15 | "prefer-const": "warn",
16 | "comma-dangle": ["error", "only-multiline"],
17 | "space-infix-ops": "off", // Until eslint #7489 lands
18 | "new-cap": "off",
19 | "no-unused-vars": ["error", { "varsIgnorePattern": "^_" }],
20 | "no-return-assign": "off",
21 | "no-unused-expressions": "off",
22 | "one-var": "off",
23 | "new-parens": "off",
24 | "indent": ["error", 2, {SwitchCase: 0}],
25 | "arrow-body-style": ["warn", "as-needed"],
26 |
27 | "no-unused-vars": "off",
28 | "react/jsx-uses-react": "error",
29 | "react/jsx-uses-vars": "error",
30 | "react/react-in-jsx-scope": "error",
31 |
32 | "import/first": "off",
33 |
34 | // This rule enforces a comma-first style, such as
35 | // npm uses. I think it's great, but it can look a bit weird,
36 | // so we're leaving it off for now (although stock Bones passes
37 | // the linter with it on). If you decide you want to enforce
38 | // this rule, change "off" to "error".
39 | "comma-style": ["off", "first", {
40 | exceptions: {
41 | ArrayExpression: true,
42 | ObjectExpression: true,
43 | }
44 | }],
45 | },
46 | }
47 |
--------------------------------------------------------------------------------
/app/components/WhoAmI.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import chai, {expect} from 'chai'
3 | chai.use(require('chai-enzyme')())
4 | import {shallow} from 'enzyme'
5 | import {spy} from 'sinon'
6 | chai.use(require('sinon-chai'))
7 | import {createStore} from 'redux'
8 |
9 | import WhoAmIContainer, {WhoAmI} from './WhoAmI'
10 |
11 | /* global describe it beforeEach */
12 | describe('', () => {
13 | const user = {
14 | name: 'Dr. Bones',
15 | }
16 | const logout = spy()
17 | let root
18 | beforeEach('render the root', () =>
19 | root = shallow()
20 | )
21 |
22 | it('greets the user', () => {
23 | expect(root.text()).to.contain(user.name)
24 | })
25 |
26 | it('has a logout button', () => {
27 | expect(root.find('button.logout')).to.have.length(1)
28 | })
29 |
30 | it('calls props.logout when logout is tapped', () => {
31 | root.find('button.logout').simulate('click')
32 | expect(logout).to.have.been.called
33 | })
34 | })
35 |
36 | describe("'s connection", () => {
37 | const state = {
38 | auth: {name: 'Dr. Bones'}
39 | }
40 |
41 | let root, store, dispatch
42 | beforeEach('create store and render the root', () => {
43 | store = createStore(state => state, state)
44 | dispatch = spy(store, 'dispatch')
45 | root = shallow()
46 | })
47 |
48 | it('gets prop.user from state.auth', () => {
49 | expect(root.find(WhoAmI)).to.have.prop('user').eql(state.auth)
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/db/models/user.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | // bcrypt docs: https://www.npmjs.com/package/bcrypt
4 | const bcrypt = require('bcryptjs')
5 | , {STRING, VIRTUAL} = require('sequelize')
6 |
7 | module.exports = db => db.define('users', {
8 | name: STRING,
9 | email: {
10 | type: STRING,
11 | validate: {
12 | isEmail: true,
13 | notEmpty: true,
14 | }
15 | },
16 |
17 | // We support oauth, so users may or may not have passwords.
18 | password_digest: STRING, // This column stores the hashed password in the DB, via the beforeCreate/beforeUpdate hooks
19 | password: VIRTUAL // Note that this is a virtual, and not actually stored in DB
20 | }, {
21 | indexes: [{fields: ['email'], unique: true}],
22 | hooks: {
23 | beforeCreate: setEmailAndPassword,
24 | beforeUpdate: setEmailAndPassword,
25 | },
26 | defaultScope: {
27 | attributes: {exclude: ['password_digest']}
28 | },
29 | instanceMethods: {
30 | // This method is a Promisified bcrypt.compare
31 | authenticate(plaintext) {
32 | return bcrypt.compare(plaintext, this.password_digest)
33 | }
34 | }
35 | })
36 |
37 | module.exports.associations = (User, {OAuth, Thing, Favorite}) => {
38 | User.hasOne(OAuth)
39 | User.belongsToMany(Thing, {as: 'favorites', through: Favorite})
40 | }
41 |
42 | function setEmailAndPassword(user) {
43 | user.email = user.email && user.email.toLowerCase()
44 | if (!user.password) return Promise.resolve(user)
45 |
46 | return bcrypt.hash(user.get('password'), 10)
47 | .then(hash => user.set('password_digest', hash))
48 | }
49 |
--------------------------------------------------------------------------------
/bin/build-branch.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Paths to add to the deployment branch.
4 | #
5 | # These paths will be added with git add -f, to include build artifacts
6 | # we normally ignore in the branch we push to heroku.
7 | build_paths="public"
8 |
9 | # colors
10 | red='\033[0;31m'
11 | blue='\033[0;34m'
12 | off='\033[0m'
13 |
14 | echoed() {
15 | echo "${blue}${*}${off}"
16 | $*
17 | }
18 |
19 | if [[ $(git status --porcelain 2> /dev/null | grep -v '$\?\?' | tail -n1) != "" ]]; then
20 | echo "${red}Uncommitted changes would be lost. Commit or stash these changes:${off}"
21 | git status
22 | exit 1
23 | fi
24 |
25 | # Our branch name is build/commit-sha-hash
26 | version="$(git log -n1 --pretty=format:%H)"
27 | branch_name="build/${version}"
28 |
29 |
30 | function create_build_branch() {
31 | git branch "${branch_name}"
32 | git checkout "${branch_name}"
33 | return 0
34 | }
35 |
36 | function commit_build_artifacts() {
37 | # Add our build paths. -f means "even if it's in .gitignore'".
38 | git add -f "${build_paths}"
39 |
40 | # Commit the build artifacts on the branch.
41 | git commit -a -m "Built ${version} on $(date)."
42 |
43 | # Always succeed.
44 | return 0
45 | }
46 |
47 | # We expect to be sourced by some file that defines a deploy
48 | # function. If deploy() isn't defined, define a stub function.
49 | if [[ -z $(type -t deploy) ]]; then
50 | function deploy() {
51 | echo '(No deployment step defined.)'
52 | return 0
53 | }
54 | fi
55 |
56 | (
57 | create_build_branch &&
58 | echoed yarn &&
59 | echoed npm run build &&
60 | commit_build_artifacts &&
61 | deploy
62 |
63 | # Regardless of whether we succeeded or failed, go back to
64 | # the previous branch.
65 | git checkout -
66 | )
67 |
--------------------------------------------------------------------------------
/app/components/Login.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import chai, {expect} from 'chai'
3 | chai.use(require('chai-enzyme')())
4 | import {shallow} from 'enzyme'
5 | import {spy} from 'sinon'
6 | chai.use(require('sinon-chai'))
7 |
8 | import {Login} from './Login'
9 |
10 | /* global describe it beforeEach */
11 | describe('', () => {
12 | let root
13 | beforeEach('render the root', () =>
14 | root = shallow()
15 | )
16 |
17 | it('shows a login form', () => {
18 | expect(root.find('input[name="username"]')).to.have.length(1)
19 | expect(root.find('input[name="password"]')).to.have.length(1)
20 | })
21 |
22 | it('shows a password field', () => {
23 | const pw = root.find('input[name="password"]')
24 | expect(pw).to.have.length(1)
25 | expect(pw.at(0)).to.have.attr('type').equals('password')
26 | })
27 |
28 | it('has a login button', () => {
29 | const submit = root.find('input[type="submit"]')
30 | expect(submit).to.have.length(1)
31 | })
32 |
33 | describe('when submitted', () => {
34 | const login = spy()
35 | const root = shallow()
36 | const submitEvent = {
37 | preventDefault: spy(),
38 | target: {
39 | username: {value: 'bones@example.com'},
40 | password: {value: '12345'},
41 | }
42 | }
43 |
44 | beforeEach('submit', () => {
45 | login.reset()
46 | submitEvent.preventDefault.reset()
47 | root.simulate('submit', submitEvent)
48 | })
49 |
50 | it('calls props.login with credentials', () => {
51 | expect(login).to.have.been.calledWith(
52 | submitEvent.target.username.value,
53 | submitEvent.target.password.value,
54 | )
55 | })
56 |
57 | it('calls preventDefault', () => {
58 | expect(submitEvent.preventDefault).to.have.been.called
59 | })
60 | })
61 | })
62 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const {resolve} = require('path')
4 | , chalk = require('chalk')
5 | , pkg = require('./package.json')
6 | , debug = require('debug')(`${pkg.name}:boot`)
7 |
8 | , nameError =
9 | `*******************************************************************
10 | You need to give your app a proper name.
11 |
12 | The package name
13 |
14 | ${pkg.name}
15 |
16 | isn't valid. If you don't change it, things won't work right.
17 |
18 | Please change it in ${__dirname}/package.json
19 | ~ xoxo, bones
20 | ********************************************************************`
21 |
22 | const reasonableName = /^[a-z0-9\-_]+$/
23 | // RegExp.test docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test
24 | if (!reasonableName.test(pkg.name)) {
25 | console.error(chalk.red(nameError))
26 | }
27 |
28 | // This will load a secrets file from
29 | //
30 | // ~/.your_app_name.env.js
31 | // or ~/.your_app_name.env.json
32 | //
33 | // and add it to the environment.
34 | // Note that this needs to be in your home directory, not the project's root directory
35 | const env = process.env
36 | , secretsFile = resolve(require('homedir')(), `.${pkg.name}.env`)
37 |
38 | try {
39 | Object.assign(env, require(secretsFile))
40 | } catch (error) {
41 | debug('%s: %s', secretsFile, error.message)
42 | debug('%s: env file not found or invalid, moving on', secretsFile)
43 | }
44 |
45 | module.exports = {
46 | get name() { return pkg.name },
47 | get isTesting() { return !!global.it },
48 | get isProduction() {
49 | return env.NODE_ENV === 'production'
50 | },
51 | get isDevelopment() {
52 | return env.NODE_ENV === 'development'
53 | },
54 | get baseUrl() {
55 | return env.BASE_URL || `http://localhost:${module.exports.port}`
56 | },
57 | get port() {
58 | return env.PORT || 1337
59 | },
60 | get root() {
61 | return __dirname
62 | },
63 | package: pkg,
64 | env,
65 | }
66 |
--------------------------------------------------------------------------------
/bin/mkapplink.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict'
4 |
5 | // 'bin/setup' is a symlink pointing to this file, which makes a
6 | // symlink in your project's main node_modules folder that points to
7 | // the root of your project's directory.
8 |
9 | const chalk = require('chalk')
10 | , fs = require('fs')
11 | , {resolve} = require('path')
12 |
13 | , appLink = resolve(__dirname, '..', 'node_modules', 'APP')
14 |
15 | , symlinkError = error =>
16 | `*******************************************************************
17 | ${appLink} must point to '..'
18 |
19 | This symlink lets you require('APP/some/path') rather than
20 | ../../../some/path
21 |
22 | I tried to create it, but got this error:
23 | ${error.message}
24 |
25 | You might try this:
26 |
27 | rm ${appLink}
28 |
29 | Then run me again.
30 |
31 | ~ xoxo, bones
32 | ********************************************************************`
33 |
34 | function makeAppSymlink() {
35 | console.log(`Linking '${appLink}' to '..'`)
36 | try {
37 | // fs.unlinkSync docs: https://nodejs.org/api/fs.html#fs_fs_unlinksync_path
38 | try { fs.unlinkSync(appLink) } catch (swallowed) { }
39 | // fs.symlinkSync docs: https://nodejs.org/api/fs.html#fs_fs_symlinksync_target_path_type
40 | const linkType = process.platform === 'win32' ? 'junction' : 'dir'
41 | fs.symlinkSync('..', appLink, linkType)
42 | } catch (error) {
43 | console.error(chalk.red(symlinkError(error)))
44 | // process.exit docs: https://nodejs.org/api/process.html#process_process_exit_code
45 | process.exit(1)
46 | }
47 | console.log(`Ok, created ${appLink}`)
48 | }
49 |
50 | function ensureAppSymlink() {
51 | try {
52 | // readlinkSync docs: https://nodejs.org/api/fs.html#fs_fs_readlinksync_path_options
53 | const currently = fs.readlinkSync(appLink)
54 | if (currently !== '..') {
55 | throw new Error(`${appLink} is pointing to '${currently}' rather than '..'`)
56 | }
57 | } catch (error) {
58 | makeAppSymlink()
59 | }
60 | }
61 |
62 | if (module === require.main) {
63 | ensureAppSymlink()
64 | }
65 |
--------------------------------------------------------------------------------
/db/models/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | // Require our models. Running each module registers the model into sequelize
4 | // so any other part of the application could call sequelize.model('User')
5 | // to get access to the User model.
6 |
7 | const app = require('APP')
8 | , debug = require('debug')(`${app.name}:models`)
9 | // Our model files export functions that take a database and return
10 | // a model. We call these functions "meta models" (they are models of
11 | // models).
12 | //
13 | // This lets us avoid cyclic dependencies, which can be hard to reason
14 | // about.
15 | , metaModels = {
16 | OAuth: require('./oauth'),
17 | User: require('./user'),
18 | Thing: require('./thing'),
19 | Favorite: require('./favorite'),
20 | // ---------- Add new models here ----------
21 | }
22 | , {mapValues} = require('lodash')
23 |
24 | module.exports = db => {
25 | // Create actual model classes by calling each meta model with the
26 | // database.
27 | const models = mapValues(metaModels, defineModel => defineModel(db))
28 |
29 | /*
30 | At this point, all our models have been created. We just need to
31 | create the associations between them.
32 |
33 | We pass the responsibility for this onto the models themselves:
34 | If they export an `associations` method, we'll call it, passing
35 | in all the models that have been defined.
36 |
37 | This lets us move the association logic to the model files,
38 | so all the knowledge about the structure of each model remains
39 | self-contained.
40 |
41 | The Sequelize docs suggest a similar setup:
42 |
43 | https://github.com/sequelize/express-example#sequelize-setup
44 | */
45 | Object.keys(metaModels)
46 | .forEach(name => {
47 | const {associations} = metaModels[name]
48 | if (typeof associations === 'function') {
49 | debug('associating model %s', name)
50 | // Metamodel::associations(self: Model, others: {[name: String]: Model}) -> ()
51 | //
52 | // Associate self with others.
53 | associations.call(metaModels[name], models[name], models)
54 | }
55 | })
56 |
57 | return models
58 | }
59 |
--------------------------------------------------------------------------------
/dev.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Concurrently run our various dev tasks.
3 | *
4 | * Usage: node dev
5 | **/
6 |
7 | const app = require('.')
8 | , chalk = require('chalk'), {bold} = chalk
9 | , {red, green, blue, cyan, yellow} = bold
10 | , dev = module.exports = () => run({
11 | server: task(app.package.scripts['start-watch'], {color: blue}),
12 | build: task(app.package.scripts['build-watch'], {color: green}),
13 | lint: task(app.package.scripts['lint-watch'], {color: cyan}),
14 | test: task(app.package.scripts['test-watch'], {color: yellow})
15 | })
16 |
17 | const taskEnvironment = (path=require('path')) => {
18 | const env = {}
19 | for (const key in process.env) {
20 | env[key] = process.env[key]
21 | }
22 | Object.assign(env, {
23 | NODE_ENV: 'development',
24 | PATH: [ path.join(app.root, 'node_modules', '.bin')
25 | , process.env.PATH ].join(path.delimiter)
26 | })
27 | return env
28 | }
29 |
30 | function run(tasks) {
31 | Object.keys(tasks)
32 | .map(name => tasks[name](name))
33 | }
34 |
35 | function task(command, {
36 | spawn=require('child_process').spawn,
37 | path=require('path'),
38 | color
39 | }={}) {
40 | return name => {
41 | const stdout = log({name, color}, process.stdout)
42 | , stderr = log({name, color, text: red}, process.stderr)
43 | , proc = spawn(command, {
44 | shell: true,
45 | stdio: 'pipe',
46 | env: taskEnvironment(),
47 | }).on('error', stderr)
48 | .on('exit', (code, signal) => {
49 | stderr(`Exited with code ${code}`)
50 | if (signal) stderr(`Exited with signal ${signal}`)
51 | })
52 | proc.stdout.on('data', stdout)
53 | proc.stderr.on('data', stderr)
54 | }
55 | }
56 |
57 | function log({
58 | name,
59 | ts=timestamp,
60 | color=none,
61 | text=none,
62 | }, out=process.stdout) {
63 | return data => data.toString()
64 | // Strip out screen-clearing control sequences, which really
65 | // muck up the output.
66 | .replace('\u001b[2J', '')
67 | .replace('\u001b[1;3H', '')
68 | .split('\n')
69 | .forEach(line => out.write(`${color(`${ts()} ${name} \t⎹ `)}${text(line)}\n`))
70 | }
71 |
72 | const dateformat = require('dateformat')
73 | function timestamp() {
74 | return dateformat('yyyy-mm-dd HH:MM:ss (Z)')
75 | }
76 |
77 | function none(x) { return x }
78 |
79 | if (module === require.main) { dev() }
80 |
--------------------------------------------------------------------------------
/server/auth.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest')
2 | const {expect} = require('chai')
3 | const db = require('APP/db'), {User} = db
4 | const app = require('./start')
5 |
6 | const alice = {
7 | username: 'alice@secrets.org',
8 | password: '12345'
9 | }
10 |
11 | /* global describe it before afterEach beforeEach */
12 | describe('/api/auth', () => {
13 | before('Await database sync', () => db.didSync)
14 | afterEach('Clear the tables', () => db.truncate({ cascade: true }))
15 |
16 | beforeEach('create a user', () =>
17 | User.create({
18 | email: alice.username,
19 | password: alice.password
20 | })
21 | )
22 |
23 | describe('POST /login/local (username, password)', () => {
24 | it('succeeds with a valid username and password', () =>
25 | request(app)
26 | .post('/api/auth/login/local')
27 | .send(alice)
28 | .expect(302)
29 | .expect('Set-Cookie', /session=.*/)
30 | .expect('Location', '/')
31 | )
32 |
33 | it('fails with an invalid username and password', () =>
34 | request(app)
35 | .post('/api/auth/login/local')
36 | .send({username: alice.username, password: 'wrong'})
37 | .expect(401)
38 | )
39 | })
40 |
41 | describe('GET /whoami', () => {
42 | describe('when not logged in', () =>
43 | it('responds with an empty object', () =>
44 | request(app).get('/api/auth/whoami')
45 | .expect(200)
46 | .then(res => expect(res.body).to.eql({}))
47 | ))
48 |
49 | describe('when logged in', () => {
50 | // supertest agents persist cookies
51 | const agent = request.agent(app)
52 |
53 | beforeEach('log in', () => agent
54 | .post('/api/auth/login/local')
55 | .send(alice))
56 |
57 | it('responds with the currently logged in user', () =>
58 | agent.get('/api/auth/whoami')
59 | .set('Accept', 'application/json')
60 | .expect(200)
61 | .then(res => expect(res.body).to.contain({
62 | email: alice.username
63 | }))
64 | )
65 | })
66 | })
67 |
68 | describe('POST /logout', () =>
69 | describe('when logged in', () => {
70 | const agent = request.agent(app)
71 |
72 | beforeEach('log in', () => agent
73 | .post('/api/auth/login/local')
74 | .send(alice))
75 |
76 | it('logs you out and redirects to whoami', () => agent
77 | .post('/api/auth/logout')
78 | .expect(302)
79 | .expect('Location', '/api/auth/whoami')
80 | .then(() =>
81 | agent.get('/api/auth/whoami')
82 | .expect(200)
83 | .then(rsp => expect(rsp.body).eql({}))
84 | )
85 | )
86 | })
87 | )
88 | })
89 |
--------------------------------------------------------------------------------
/db/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const app = require('APP')
3 | , debug = require('debug')(`${app.name}:db`) // DEBUG=your_app_name:db
4 | , chalk = require('chalk')
5 | , Sequelize = require('sequelize')
6 |
7 | , name = (app.env.DATABASE_NAME || app.name) +
8 | (app.isTesting ? '_test' : '')
9 | , url = app.env.DATABASE_URL || `postgres://localhost:5432/${name}`
10 |
11 | debug(chalk.yellow(`Opening database connection to ${url}`))
12 | const db = module.exports = new Sequelize(url, {
13 | logging: require('debug')('sql'), // export DEBUG=sql in the environment to
14 | // get SQL queries
15 | define: {
16 | underscored: true, // use snake_case rather than camelCase column names.
17 | // these are easier to work with in psql.
18 | freezeTableName: true, // don't change table names from the one specified
19 | timestamps: true, // automatically include timestamp columns
20 | }
21 | })
22 |
23 | // Initialize all our models and assign them as properties
24 | // on the database object.
25 | //
26 | // This lets us use destructuring to get at them like so:
27 | //
28 | // const {User, Product} = require('APP/db')
29 | //
30 | Object.assign(db, require('./models')(db),
31 | // We'll also make createAndSync available. It's sometimes useful in tests.
32 | {createAndSync})
33 |
34 | // After defining all the models, sync the database.
35 | // Notice that didSync *is* a Promise, rather than being a function that returns
36 | // a Promise. It holds the state of this initial sync.
37 | db.didSync = db.createAndSync()
38 |
39 | // sync the db, creating it if necessary
40 | function createAndSync(force=app.isTesting, retries=0, maxRetries=5) {
41 | return db.sync({force})
42 | .then(() => debug(`Synced models to db ${url}`))
43 | .catch(fail => {
44 | // Don't do this auto-create nonsense in prod, or
45 | // if we've retried too many times.
46 | if (app.isProduction || retries > maxRetries) {
47 | console.error(chalk.red(`********** database error ***********`))
48 | console.error(chalk.red(` Couldn't connect to ${url}`))
49 | console.error()
50 | console.error(chalk.red(fail))
51 | console.error(chalk.red(`*************************************`))
52 | return
53 | }
54 | // Otherwise, do this autocreate nonsense
55 | debug(`${retries ? `[retry ${retries}]` : ''} Creating database ${name}...`)
56 | return new Promise(resolve =>
57 | // 'child_process.exec' docs: https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback
58 | require('child_process').exec(`createdb "${name}"`, resolve)
59 | ).then(() => createAndSync(true, retries + 1))
60 | })
61 | }
62 |
--------------------------------------------------------------------------------
/db/models/oauth.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const app = require('APP')
4 | , debug = require('debug')(`${app.name}:oauth`)
5 | , {STRING, JSON} = require('sequelize')
6 |
7 | module.exports = db => {
8 | const OAuth = db.define('oauths', {
9 | uid: STRING,
10 | provider: STRING,
11 |
12 | // OAuth v2 fields
13 | accessToken: STRING,
14 | refreshToken: STRING,
15 |
16 | // OAuth v1 fields
17 | token: STRING,
18 | tokenSecret: STRING,
19 |
20 | // The whole profile as JSON
21 | profileJson: JSON,
22 | }, {
23 | // Further reading on indexes:
24 | // 1. Sequelize and indexes: http://docs.sequelizejs.com/en/2.0/docs/models-definition/#indexes
25 | // 2. Postgres documentation: https://www.postgresql.org/docs/9.1/static/indexes.html
26 | indexes: [{fields: ['uid'], unique: true}],
27 | })
28 |
29 | // OAuth.V2 is a default argument for the OAuth.setupStrategy method - it's our callback function that will execute when the user has successfully logged in
30 | OAuth.V2 = (accessToken, refreshToken, profile, done) =>
31 | OAuth.findOrCreate({
32 | where: {
33 | provider: profile.provider,
34 | uid: profile.id,
35 | }
36 | })
37 | .spread(oauth => {
38 | debug(profile)
39 | debug('provider:%s will log in user:{name=%s uid=%s}',
40 | profile.provider,
41 | profile.displayName,
42 | profile.id
43 | )
44 | oauth.profileJson = profile
45 | oauth.accessToken = accessToken
46 |
47 | // db.Promise.props is a Bluebird.js method; basically like "all" but for an object whose properties might contain promises.
48 | // Docs: http://bluebirdjs.com/docs/api/promise.props.html
49 | return db.Promise.props({
50 | oauth,
51 | user: oauth.getUser(),
52 | _saveProfile: oauth.save(),
53 | })
54 | })
55 | .then(({ oauth, user }) => user ||
56 | OAuth.User.create({
57 | name: profile.displayName,
58 | })
59 | .then(user => db.Promise.props({
60 | user,
61 | _setOauthUser: oauth.setUser(user)
62 | }))
63 | .then(({user}) => user)
64 | )
65 | .then(user => done(null, user))
66 | .catch(done)
67 |
68 | // setupStrategy is a wrapper around passport.use, and is called in authentication routes in server/auth.js
69 | OAuth.setupStrategy =
70 | ({
71 | provider,
72 | strategy,
73 | config,
74 | oauth=OAuth.V2,
75 | passport
76 | }) => {
77 | const undefinedKeys = Object.keys(config)
78 | .map(k => config[k])
79 | .filter(value => typeof value === 'undefined')
80 | if (undefinedKeys.length) {
81 | for (const key in config) {
82 | if (!config[key]) debug('provider:%s: needs environment var %s', provider, key)
83 | }
84 | debug('provider:%s will not initialize', provider)
85 | return
86 | }
87 |
88 | debug('initializing provider:%s', provider)
89 |
90 | passport.use(new strategy(config, oauth))
91 | }
92 |
93 | return OAuth
94 | }
95 |
96 | module.exports.associations = (OAuth, {User}) => {
97 | // Create a static association between the OAuth and User models.
98 | // This lets us refer to OAuth.User above, when we need to create
99 | // a user.
100 | OAuth.User = User
101 | OAuth.belongsTo(User)
102 | }
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hi, I'm bones
2 |
3 | I'm a happy little skeleton. You can clone me to use as a starter on your projects!
4 | I have React, Redux, Sequelize, and Express all just rattling around in here ready
5 | to go.
6 |
7 | ## I need node >= 6.7.0
8 |
9 | If you don't have it, I'll complain and tell you how to install it.
10 |
11 | ## 1. Make me into something!
12 |
13 | We recommend that you **clone**, not fork, this repo – unless your intention is
14 | to develop Bones proper instead of using Bones as the starting point for your
15 | own application.
16 |
17 | Start by doing either of the following:
18 |
19 | * Create a GitHub repo and clone it, or
20 | * `git init` in an empty directory on your machine.
21 |
22 | After you have a repo on your machine:
23 |
24 | ```sh
25 | git remote add bones https://github.com/FullstackAcademy/bones.git
26 | git fetch bones
27 | git merge bones/master
28 | ```
29 |
30 | And then you'll have me! If I change – which I probably will – you can get the most recent
31 | version by doing this again:
32 |
33 | ```sh
34 | git fetch bones
35 | git merge bones/master
36 | ```
37 |
38 | ## 2. I need a name.
39 |
40 | I don't have a name. I think I used to have one, but it turned to dust right along with my
41 | heart and liver and pituitary gland and all that stuff.
42 |
43 | Anyway, I'll need one. Give me a name in `package.json`.
44 |
45 | ## 3. Start my dusty heart
46 |
47 | Short and sweet:
48 |
49 | ```sh
50 | npm install
51 | npm run dev
52 | ```
53 |
54 | The `dev` script sets `NODE_ENV` to "development", runs the build script in watch mode, and
55 | starts the server with `nodemon`. Build vs server logs are separated by a prefix. If you prefer
56 | to run the server and build processes separately, you can instead do:
57 |
58 | ```sh
59 | npm run start-dev
60 | ```
61 |
62 | ```sh
63 | npm run build-dev
64 | ```
65 |
66 | In two separate terminals. The vanilla `npm start` is for production — you won't use it in development!
67 |
68 | ## My anatomy
69 |
70 | `/app` has the React/Redux setup. `main.jsx` is the entry point.
71 |
72 | `/db` has the Sequelize models and database setup. It'll create the database for you if it doesn't exist,
73 | assuming you're using postgres.
74 |
75 | `/server` has the Express server and routes. `start.js` is the entry point.
76 |
77 | `/bin` has scripts. (Right now it has *one* script that creates a useful symlink.)
78 |
79 | ## Conventions
80 |
81 | I use `require` and `module.exports` in `.js` files.
82 |
83 | I use `import` and `export` in `.jsx` files, unless `require` makes for cleaner code.
84 |
85 | I use two spaces, no semi-colons, and trailing commas where possible. I'll
86 | have a linter someday soon.
87 |
88 | ## Quick Heroku deployment
89 |
90 | 1. Set up the [Heroku command line tools](https://devcenter.heroku.com/articles/heroku-cli) and install [Yarn](https://yarnpkg.com/en/) if you haven't already (`npm install -g yarn`)
91 | 2. `heroku login`
92 | 3. Add a git remote for heroku:
93 | - **If you're creating a new app...**
94 | 1. `heroku create` or `heroku create your-app-name` if you have a name in mind.
95 | 2. `heroku addons:create heroku-postgresql:hobby-dev` to add postgres
96 | 3. `npm run deploy-heroku`. This will create a new branch and compile and commit your frontend JS to it, then push that branch to Heroku.
97 | 4. `heroku run npm run seed` to seed the database
98 |
99 | - **If you already have a Heroku app...**
100 | 1. `heroku git:remote your-app-name` You'll need to be a collaborator on the app.
101 |
102 | Afterwards,
103 | - *To deploy:* `npm run deploy-heroku`
104 | - *To re-seed:* `heroku run npm run seed`
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "__BONES_NEEDS_A_NAME__",
3 | "version": "0.0.1",
4 | "description": "A happy little skeleton.",
5 | "main": "index.js",
6 | "engines": {
7 | "node": ">= 7.0.0"
8 | },
9 | "scripts": {
10 | "dev": "node dev",
11 | "validate": "check-node-version --node '>= 7.0.0'",
12 | "setup": "./bin/setup",
13 | "prep": "npm run validate && npm run setup",
14 | "postinstall": "npm run prep",
15 | "build": "webpack",
16 | "build-watch": "npm run build -- -w",
17 | "build-dev": "cross-env NODE_ENV=development npm run build-watch",
18 | "build-branch": "bin/build-branch.sh",
19 | "start": "node server/start.js",
20 | "start-watch": "nodemon server/start.js --watch server --watch db --watch index.js --watch package.json",
21 | "start-dev": "cross-env NODE_ENV=development npm run start-watch",
22 | "test": "mocha --compilers js:babel-register --watch-extensions js,jsx app/**/*.test.js app/**/*.test.jsx server/**/*.test.js db/**/*.test.js",
23 | "test-watch": "npm run test -- --watch --reporter=min",
24 | "seed": "node db/seed.js",
25 | "deploy-heroku": "bin/deploy-heroku.sh",
26 | "lint": "esw . --ignore-path .gitignore --ext '.js,.jsx'",
27 | "lint-watch": "npm run lint -- -w",
28 | "prepush": "npm run lint && npm run test"
29 | },
30 | "repository": {
31 | "type": "git",
32 | "url": "git+https://github.com/queerviolet/bones.git"
33 | },
34 | "keywords": [
35 | "react",
36 | "redux",
37 | "skeleton"
38 | ],
39 | "author": "Ashi Krishnan ",
40 | "license": "MIT",
41 | "bugs": {
42 | "url": "https://github.com/queerviolet/bones/issues"
43 | },
44 | "homepage": "https://github.com/queerviolet/bones#readme",
45 | "dependencies": {
46 | "axios": "^0.15.2",
47 | "babel-preset-stage-2": "^6.18.0",
48 | "bcryptjs": "^2.4.0",
49 | "body-parser": "^1.15.2",
50 | "chai-enzyme": "^0.5.2",
51 | "chalk": "^1.1.3",
52 | "check-node-version": "^1.1.2",
53 | "concurrently": "^3.1.0",
54 | "cookie-session": "^2.0.0-alpha.1",
55 | "enzyme": "^2.5.1",
56 | "express": "^4.14.0",
57 | "finalhandler": "^1.0.0",
58 | "homedir": "^0.6.0",
59 | "passport": "^0.3.2",
60 | "passport-facebook": "^2.1.1",
61 | "passport-github2": "^0.1.10",
62 | "passport-google-oauth": "^1.0.0",
63 | "passport-local": "^1.0.0",
64 | "pg": "^6.1.0",
65 | "pretty-error": "^2.0.2",
66 | "react": "^15.3.2",
67 | "react-dom": "^15.3.2",
68 | "react-redux": "^4.4.5",
69 | "react-router": "^3.0.0",
70 | "redux": "^3.6.0",
71 | "redux-devtools-extension": "^2.13.0",
72 | "redux-logger": "^2.7.0",
73 | "redux-thunk": "^2.1.0",
74 | "sequelize": "^3.24.6",
75 | "sinon": "^1.17.6",
76 | "sinon-chai": "^2.8.0"
77 | },
78 | "devDependencies": {
79 | "babel": "^6.5.2",
80 | "babel-core": "^6.18.0",
81 | "babel-eslint": "^7.2.2",
82 | "babel-loader": "^6.2.7",
83 | "babel-preset-es2015": "^6.18.0",
84 | "babel-preset-react": "^6.16.0",
85 | "chai": "^3.5.0",
86 | "cross-env": "^3.1.4",
87 | "dateformat": "^2.0.0",
88 | "eslint": "^3.19.0",
89 | "eslint-config-standard": "^10.2.1",
90 | "eslint-plugin-import": "^2.2.0",
91 | "eslint-plugin-node": "^4.2.2",
92 | "eslint-plugin-promise": "^3.5.0",
93 | "eslint-plugin-react": "^6.10.3",
94 | "eslint-plugin-standard": "^3.0.1",
95 | "eslint-watch": "^3.1.0",
96 | "husky": "^0.13.3",
97 | "mocha": "^3.1.2",
98 | "nodemon": "^1.11.0",
99 | "supertest": "^3.0.0",
100 | "volleyball": "^1.4.1",
101 | "webpack": "^2.2.1",
102 | "webpack-livereload-plugin": "^0.10.0"
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/server/start.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const express = require('express')
5 | const bodyParser = require('body-parser')
6 | const {resolve} = require('path')
7 | const passport = require('passport')
8 | const PrettyError = require('pretty-error')
9 | const finalHandler = require('finalhandler')
10 | // PrettyError docs: https://www.npmjs.com/package/pretty-error
11 |
12 | // Bones has a symlink from node_modules/APP to the root of the app.
13 | // That means that we can require paths relative to the app root by
14 | // saying require('APP/whatever').
15 | //
16 | // This next line requires our root index.js:
17 | const pkg = require('APP')
18 |
19 | const app = express()
20 |
21 | if (!pkg.isProduction && !pkg.isTesting) {
22 | // Logging middleware (dev only)
23 | app.use(require('volleyball'))
24 | }
25 |
26 | // Pretty error prints errors all pretty.
27 | const prettyError = new PrettyError
28 |
29 | // Skip events.js and http.js and similar core node files.
30 | prettyError.skipNodeFiles()
31 |
32 | // Skip all the trace lines about express' core and sub-modules.
33 | prettyError.skipPackage('express')
34 |
35 | module.exports = app
36 | // Session middleware - compared to express-session (which is what's used in the Auther workshop), cookie-session stores sessions in a cookie, rather than some other type of session store.
37 | // Cookie-session docs: https://www.npmjs.com/package/cookie-session
38 | .use(require('cookie-session')({
39 | name: 'session',
40 | keys: [process.env.SESSION_SECRET || 'an insecure secret key'],
41 | }))
42 |
43 | // Body parsing middleware
44 | .use(bodyParser.urlencoded({ extended: true }))
45 | .use(bodyParser.json())
46 |
47 | // Authentication middleware
48 | .use(passport.initialize())
49 | .use(passport.session())
50 |
51 | // Serve static files from ../public
52 | .use(express.static(resolve(__dirname, '..', 'public')))
53 |
54 | // Serve our api - ./api also requires in ../db, which syncs with our database
55 | .use('/api', require('./api'))
56 |
57 | // any requests with an extension (.js, .css, etc.) turn into 404
58 | .use((req, res, next) => {
59 | if (path.extname(req.path).length) {
60 | const err = new Error('Not found')
61 | err.status = 404
62 | next(err)
63 | } else {
64 | next()
65 | }
66 | })
67 |
68 | // Send index.html for anything else.
69 | .get('/*', (_, res) => res.sendFile(resolve(__dirname, '..', 'public', 'index.html')))
70 |
71 | // Error middleware interceptor, delegates to same handler Express uses.
72 | // https://github.com/expressjs/express/blob/master/lib/application.js#L162
73 | // https://github.com/pillarjs/finalhandler/blob/master/index.js#L172
74 | .use((err, req, res, next) => {
75 | console.error(prettyError.render(err))
76 | finalHandler(req, res)(err)
77 | })
78 |
79 | if (module === require.main) {
80 | // Start listening only if we're the main module.
81 | //
82 | // https://nodejs.org/api/modules.html#modules_accessing_the_main_module
83 | const server = app.listen(
84 | pkg.port,
85 | () => {
86 | console.log(`--- Started HTTP Server for ${pkg.name} ---`)
87 | const { address, port } = server.address()
88 | const host = address === '::' ? 'localhost' : address
89 | const urlSafeHost = host.includes(':') ? `[${host}]` : host
90 | console.log(`Listening on http://${urlSafeHost}:${port}`)
91 | }
92 | )
93 | }
94 |
95 | // This check on line 64 is only starting the server if this file is being run directly by Node, and not required by another file.
96 | // Bones does this for testing reasons. If we're running our app in development or production, we've run it directly from Node using 'npm start'.
97 | // If we're testing, then we don't actually want to start the server; 'module === require.main' will luckily be false in that case, because we would be requiring in this file in our tests rather than running it directly.
98 |
--------------------------------------------------------------------------------
/db/seed.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const db = require('APP/db')
4 | , {User, Thing, Favorite, Promise} = db
5 | , {mapValues} = require('lodash')
6 |
7 | function seedEverything() {
8 | const seeded = {
9 | users: users(),
10 | things: things(),
11 | }
12 |
13 | seeded.favorites = favorites(seeded)
14 |
15 | return Promise.props(seeded)
16 | }
17 |
18 | const users = seed(User, {
19 | god: {
20 | email: 'god@example.com',
21 | name: 'So many names',
22 | password: '1234',
23 | },
24 | barack: {
25 | name: 'Barack Obama',
26 | email: 'barack@example.gov',
27 | password: '1234'
28 | },
29 | })
30 |
31 | const things = seed(Thing, {
32 | surfing: {name: 'surfing'},
33 | smiting: {name: 'smiting'},
34 | puppies: {name: 'puppies'},
35 | })
36 |
37 | const favorites = seed(Favorite,
38 | // We're specifying a function here, rather than just a rows object.
39 | // Using a function lets us receive the previously-seeded rows (the seed
40 | // function does this wiring for us).
41 | //
42 | // This lets us reference previously-created rows in order to create the join
43 | // rows. We can reference them by the names we used above (which is why we used
44 | // Objects above, rather than just arrays).
45 | ({users, things}) => ({
46 | // The easiest way to seed associations seems to be to just create rows
47 | // in the join table.
48 | 'obama loves surfing': {
49 | user_id: users.barack.id, // users.barack is an instance of the User model
50 | // that we created in the user seed above.
51 | // The seed function wires the promises so that it'll
52 | // have been created already.
53 | thing_id: things.surfing.id // Same thing for things.
54 | },
55 | 'god is into smiting': {
56 | user_id: users.god.id,
57 | thing_id: things.smiting.id
58 | },
59 | 'obama loves puppies': {
60 | user_id: users.barack.id,
61 | thing_id: things.puppies.id
62 | },
63 | 'god loves puppies': {
64 | user_id: users.god.id,
65 | thing_id: things.puppies.id
66 | },
67 | })
68 | )
69 |
70 | if (module === require.main) {
71 | db.didSync
72 | .then(() => db.sync({force: true}))
73 | .then(seedEverything)
74 | .finally(() => process.exit(0))
75 | }
76 |
77 | class BadRow extends Error {
78 | constructor(key, row, error) {
79 | super(error)
80 | this.cause = error
81 | this.row = row
82 | this.key = key
83 | }
84 |
85 | toString() {
86 | return `[${this.key}] ${this.cause} while creating ${JSON.stringify(this.row, 0, 2)}`
87 | }
88 | }
89 |
90 | // seed(Model: Sequelize.Model, rows: Function|Object) ->
91 | // (others?: {...Function|Object}) -> Promise
92 | //
93 | // Takes a model and either an Object describing rows to insert,
94 | // or a function that when called, returns rows to insert. returns
95 | // a function that will seed the DB when called and resolve with
96 | // a Promise of the object of all seeded rows.
97 | //
98 | // The function form can be used to initialize rows that reference
99 | // other models.
100 | function seed(Model, rows) {
101 | return (others={}) => {
102 | if (typeof rows === 'function') {
103 | rows = Promise.props(
104 | mapValues(others,
105 | other =>
106 | // Is other a function? If so, call it. Otherwise, leave it alone.
107 | typeof other === 'function' ? other() : other)
108 | ).then(rows)
109 | }
110 |
111 | return Promise.resolve(rows)
112 | .then(rows => Promise.props(
113 | Object.keys(rows)
114 | .map(key => {
115 | const row = rows[key]
116 | return {
117 | key,
118 | value: Promise.props(row)
119 | .then(row => Model.create(row)
120 | .catch(error => { throw new BadRow(key, row, error) })
121 | )
122 | }
123 | }).reduce(
124 | (all, one) => Object.assign({}, all, {[one.key]: one.value}),
125 | {}
126 | )
127 | )
128 | )
129 | .then(seeded => {
130 | console.log(`Seeded ${Object.keys(seeded).length} ${Model.name} OK`)
131 | return seeded
132 | }).catch(error => {
133 | console.error(`Error seeding ${Model.name}: ${error} \n${error.stack}`)
134 | })
135 | }
136 | }
137 |
138 | module.exports = Object.assign(seed, {users, things, favorites})
139 |
--------------------------------------------------------------------------------
/server/auth.js:
--------------------------------------------------------------------------------
1 | const app = require('APP'), {env} = app
2 | const debug = require('debug')(`${app.name}:auth`)
3 | const passport = require('passport')
4 |
5 | const {User, OAuth} = require('APP/db')
6 | const auth = require('express').Router()
7 |
8 | /*************************
9 | * Auth strategies
10 | *
11 | * The OAuth model knows how to configure Passport middleware.
12 | * To enable an auth strategy, ensure that the appropriate
13 | * environment variables are set.
14 | *
15 | * You can do it on the command line:
16 | *
17 | * FACEBOOK_CLIENT_ID=abcd FACEBOOK_CLIENT_SECRET=1234 npm run dev
18 | *
19 | * Or, better, you can create a ~/.$your_app_name.env.json file in
20 | * your home directory, and set them in there:
21 | *
22 | * {
23 | * FACEBOOK_CLIENT_ID: 'abcd',
24 | * FACEBOOK_CLIENT_SECRET: '1234',
25 | * }
26 | *
27 | * Concentrating your secrets this way will make it less likely that you
28 | * accidentally push them to Github, for example.
29 | *
30 | * When you deploy to production, you'll need to set up these environment
31 | * variables with your hosting provider.
32 | **/
33 |
34 | // Facebook needs the FACEBOOK_CLIENT_ID and FACEBOOK_CLIENT_SECRET
35 | // environment variables.
36 | OAuth.setupStrategy({
37 | provider: 'facebook',
38 | strategy: require('passport-facebook').Strategy,
39 | config: {
40 | clientID: env.FACEBOOK_CLIENT_ID,
41 | clientSecret: env.FACEBOOK_CLIENT_SECRET,
42 | callbackURL: `${app.baseUrl}/api/auth/login/facebook`,
43 | },
44 | passport
45 | })
46 |
47 | // Google needs the GOOGLE_CLIENT_SECRET AND GOOGLE_CLIENT_ID
48 | // environment variables.
49 | OAuth.setupStrategy({
50 | provider: 'google',
51 | strategy: require('passport-google-oauth').OAuth2Strategy,
52 | config: {
53 | clientID: env.GOOGLE_CLIENT_ID,
54 | clientSecret: env.GOOGLE_CLIENT_SECRET,
55 | callbackURL: `${app.baseUrl}/api/auth/login/google`,
56 | },
57 | passport
58 | })
59 |
60 | // Github needs the GITHUB_CLIENT_ID AND GITHUB_CLIENT_SECRET
61 | // environment variables.
62 | OAuth.setupStrategy({
63 | provider: 'github',
64 | strategy: require('passport-github2').Strategy,
65 | config: {
66 | clientID: env.GITHUB_CLIENT_ID,
67 | clientSecret: env.GITHUB_CLIENT_SECRET,
68 | callbackURL: `${app.baseUrl}/api/auth/login/github`,
69 | },
70 | passport
71 | })
72 |
73 | // Other passport configuration:
74 | // Passport review in the Week 6 Concept Review:
75 | // https://docs.google.com/document/d/1MHS7DzzXKZvR6MkL8VWdCxohFJHGgdms71XNLIET52Q/edit?usp=sharing
76 | passport.serializeUser((user, done) => {
77 | done(null, user.id)
78 | })
79 |
80 | passport.deserializeUser(
81 | (id, done) => {
82 | debug('will deserialize user.id=%d', id)
83 | User.findById(id)
84 | .then(user => {
85 | if (!user) debug('deserialize retrieved null user for id=%d', id)
86 | else debug('deserialize did ok user.id=%d', id)
87 | done(null, user)
88 | })
89 | .catch(err => {
90 | debug('deserialize did fail err=%s', err)
91 | done(err)
92 | })
93 | }
94 | )
95 |
96 | // require.('passport-local').Strategy => a function we can use as a constructor, that takes in a callback
97 | passport.use(new (require('passport-local').Strategy)(
98 | (email, password, done) => {
99 | debug('will authenticate user(email: "%s")', email)
100 | User.findOne({
101 | where: {email},
102 | attributes: {include: ['password_digest']}
103 | })
104 | .then(user => {
105 | if (!user) {
106 | debug('authenticate user(email: "%s") did fail: no such user', email)
107 | return done(null, false, { message: 'Login incorrect' })
108 | }
109 | return user.authenticate(password)
110 | .then(ok => {
111 | if (!ok) {
112 | debug('authenticate user(email: "%s") did fail: bad password')
113 | return done(null, false, { message: 'Login incorrect' })
114 | }
115 | debug('authenticate user(email: "%s") did ok: user.id=%d', email, user.id)
116 | done(null, user)
117 | })
118 | })
119 | .catch(done)
120 | }
121 | ))
122 |
123 | auth.get('/whoami', (req, res) => res.send(req.user))
124 |
125 | // POST requests for local login:
126 | auth.post('/login/local', passport.authenticate('local', {successRedirect: '/'}))
127 |
128 | // GET requests for OAuth login:
129 | // Register this route as a callback URL with OAuth provider
130 | auth.get('/login/:strategy', (req, res, next) =>
131 | passport.authenticate(req.params.strategy, {
132 | scope: 'email', // You may want to ask for additional OAuth scopes. These are
133 | // provider specific, and let you access additional data (like
134 | // their friends or email), or perform actions on their behalf.
135 | successRedirect: '/',
136 | // Specify other config here
137 | })(req, res, next)
138 | )
139 |
140 | auth.post('/logout', (req, res) => {
141 | req.logout()
142 | res.redirect('/api/auth/whoami')
143 | })
144 |
145 | module.exports = auth
146 |
--------------------------------------------------------------------------------
/app/components/Jokes.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | export default class BonesJokes extends Component {
4 | componentDidMount() {
5 | this.nextJoke()
6 | }
7 |
8 | nextJoke = () =>
9 | this.setState({
10 | joke: randomJoke(),
11 | answered: false,
12 | })
13 |
14 | answer = () =>
15 | this.setState({answered: true})
16 |
17 | render() {
18 | if (!this.state) { return null }
19 |
20 | const {joke, answered} = this.state
21 | return (
22 |
23 |
{joke.q}
24 | {answered &&
{joke.a}
}
25 | ~xoxo, bones
26 |
27 | )
28 | }
29 | }
30 |
31 | function randomJoke() {
32 | return jokes[Math.floor(Math.random() * jokes.length)]
33 | }
34 |
35 | const jokes = `Q: Who won the skeleton beauty contest?
36 | A: No body
37 | Q: What do skeletons say before they begin dining?
38 | A: Bone appetit !
39 | Q: When does a skeleton laugh?
40 | A: When something tickles his funny bone.
41 | Q: Why didn't the skeleton dance at the Halloween party?
42 | A: It had no body to dance with.
43 | Q: What type of art do skeletons like?
44 | A: Skull tures
45 | Q: What did the skeleton say when his brother told a lie?
46 | A: You can't fool me, I can see right through you.
47 | Q: What did the skeleton say while riding his Harley Davidson motorcycle?
48 | A: I'm bone to be wild!
49 | Q: Why didn't the skeleton dance at the party?
50 | A: He had no body to dance with.
51 | Q: What do you give a skeleton for valentine's day?
52 | A: Bone-bones in a heart shaped box.
53 | Q: Who was the most famous skeleton detective?
54 | A: Sherlock Bones.
55 | Q: Who was the most famous French skeleton?
56 | A: Napoleon bone-apart
57 | Q: What instrument do skeletons play?
58 | A: Trom-BONE.
59 | Q: What does a skeleton orders at a restaurant?
60 | A: Spare ribs!!!
61 | Q: When does a skeleton laugh?
62 | A: When something tickles his funny bone.
63 | Q: Why didn't the skeleton eat the cafeteria food?
64 | A: Because he didn't have the stomach for it!
65 | Q: Why couldn't the skeleton cross the road?
66 | A: He didn't have the guts.
67 | Q: Why are skeletons usually so calm ?
68 | A: Nothing gets under their skin !
69 | Q: Why do skeletons hate winter?
70 | A: Beacuse the cold goes right through them !
71 | Q: Why are graveyards so noisy ?
72 | A: Beacause of all the coffin !
73 | Q: Why didn't the skeleton go to the party ?
74 | A: He had no body to go with !
75 | Q: What happened when the skeletons rode pogo sticks ?
76 | A: They had a rattling good time !
77 | Q: Why did the skeleton go to hospital ?
78 | A: To have his ghoul stones removed !
79 | Q: How did the skeleton know it was going to rain ?
80 | A: He could feel it in his bones !
81 | Q: What's a skeleton's favourite musical instrument ?
82 | A: A trom-bone !
83 | Q: How do skeletons call their friends ?
84 | A: On the telebone !
85 | Q: What do you call a skeleton who won't get up in the mornings ?
86 | A: Lazy bones !
87 | Q: What do boney people use to get into their homes ?
88 | A: Skeleton keys !
89 | Q: What do you call a skeleton who acts in Westerns ?
90 | A: Skint Eastwood !
91 | Q: What happened to the boat that sank in the sea full of piranha fish ?
92 | A: It came back with a skeleton crew !
93 | Q: What do you call a skeleton snake ?
94 | A: A rattler !
95 | Q: What is a skeletons like to drink milk ?
96 | A: Milk - it's so good for the bones !
97 | Q: Why did the skeleton stay out in the snow all night ?
98 | A: He was a numbskull !
99 | Q: What do you call a stupid skeleton ?
100 | A: Bonehead !
101 | Q: What happened to the skeleton who stayed by the fire too long ?
102 | A: He became bone dry !
103 | Q: What happened to the lazy skeleton ?
104 | A: He was bone idle !
105 | Q: Why did the skeleton pupil stay late at school ?
106 | A: He was boning up for his exams !
107 | Q: What sort of soup do skeletons like ?
108 | A: One with plenty of body in it !
109 | Q: Why did the skeleton run up a tree ?
110 | A: Because a dog was after his bones !
111 | Q: What did the skeleton say to his girlfriend ?
112 | A: I love every bone in your body !
113 | Q: Why wasn't the naughty skeleton afraid of the police ?
114 | A: Because he knew they couldn't pin anything on him !
115 | Q: How do skeletons get their mail ?
116 | A: By bony express !
117 | Q: Why don't skeletons play music in church ?
118 | A: They have no organs !
119 | Q: What kind of plate does a skeleton eat off ?
120 | A: Bone china !
121 | Q: Why do skeletons hate winter ?
122 | A: Because the wind just goes straight through them !
123 | Q: What's a skeleton's favourite pop group ?
124 | A: Boney M !
125 | Q: What do you do if you see a skeleton running across a road ?
126 | A: Jump out of your skin and join him !
127 | Q: What did the old skeleton complain of ?
128 | A: Aching bones !
129 | Q: What is a skeleton ?
130 | A: Somebody on a diet who forgot to say "when" !
131 | Q: What happened to the skeleton that was attacked by a dog ?
132 | A: He ran off with some bones and didn't leave him with a leg to stand on !
133 | Q: Why are skeletons so calm ?
134 | A: Because nothing gets under their skin !
135 | Q: What do you call a skeleton that is always telling lies ?
136 | A: A boney phoney !
137 | Q: Why didn't the skeleton want to play football ?
138 | A: Because his heart wasn't in it !
139 | Q: What happened to the skeleton who went to a party ?
140 | A: All the others used him as a coat rack !
141 | Q: What do you call a skeleton who presses the door bell ?
142 | A: A dead ringer !
143 | Q: When does a skeleton laugh?
144 | A: When something tickles his funny bone.
145 | Q: How did skeletons send their letters in the old days?
146 | A: By bony express!
147 | Q: How do you make a skeleton laugh?
148 | A: Tickle his funny bone!`
149 | .split('\n')
150 | .reduce((all, row, i) =>
151 | i % 2 === 0
152 | ? [...all, {q: row}]
153 | : [...all.slice(0, all.length - 1), Object.assign({a: row}, all[all.length - 1])],
154 | [])
155 |
--------------------------------------------------------------------------------