├── config ├── secrets.js ├── jest │ ├── server.json │ ├── fileTransform.js │ └── cssTransform.js ├── db.js ├── getHttpsConfig.js ├── paths.js ├── modules.js ├── env.js ├── webpackDevServer.config.js └── webpack.config.js ├── src-client ├── index.css ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── index.jsx ├── api │ └── api.jsx ├── App.test.jsx ├── App.css ├── private-route.jsx ├── components │ ├── posts │ │ ├── posts-container.jsx │ │ ├── posts-add.jsx │ │ └── posts-table.jsx │ ├── header.jsx │ └── auth │ │ ├── sign-in.jsx │ │ └── sign-up.jsx ├── store.jsx ├── App.jsx └── logo.svg ├── .env.example ├── .env.test ├── scripts ├── start-server-prod.js ├── test.js ├── db-migrate.js ├── start-server-dev.js ├── db-seed.js ├── start-client-dev.js └── build.js ├── migrations ├── 1564491788184_posts-add-user-relation.js ├── 1530234244089_initial-migration.js └── 1555584527837_add-users.js ├── src-server ├── db.js ├── components │ ├── auth │ │ ├── jwt.js │ │ ├── local.js │ │ └── helpers.js │ ├── users │ │ └── index.js │ └── posts │ │ └── index.js ├── app.test.js ├── routes │ ├── api │ │ └── posts.js │ └── auth │ │ └── index.js └── app.js ├── .gitignore ├── README.md └── package.json /config/secrets.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | jwt: process.env.JWT_SECRET, 3 | }; 4 | -------------------------------------------------------------------------------- /src-client/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src-client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrach/starter-postgres-express-react/HEAD/src-client/public/favicon.ico -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DB_NAME=starter 2 | DB_USER=starter 3 | DB_PASSWORD=password 4 | DB_HOST=localhost 5 | DB_PORT=5432 6 | DB_ENABLE_SSL=1 7 | JWT_SECRET=starter 8 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | DB_NAME=starter_test 2 | DB_USER=starter 3 | DB_PASSWORD=password 4 | DB_HOST=localhost 5 | DB_PORT=5432 6 | DB_ENABLE_SSL=0 7 | JWT_SECRET=starter-test-secret 8 | -------------------------------------------------------------------------------- /scripts/start-server-prod.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.env.PORT = 3000; 4 | process.env.NODE_ENV = 'production'; 5 | const server = require('./start-server-dev'); 6 | server(); 7 | -------------------------------------------------------------------------------- /src-client/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /migrations/1564491788184_posts-add-user-relation.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined; 2 | 3 | exports.up = (pgm) => { 4 | pgm.dropColumn('posts', 'author'); 5 | pgm.addColumns('posts', { 6 | user_id: { 7 | type: 'integer' 8 | } 9 | }); 10 | }; 11 | 12 | exports.down = () => { 13 | 14 | }; 15 | -------------------------------------------------------------------------------- /src-client/api/api.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const instance = axios.create({ 4 | baseURL: 'http://127.0.0.1:3000/' 5 | }); 6 | 7 | instance.interceptors.request.use((config) => { 8 | config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`; 9 | return config; 10 | }); 11 | 12 | export default instance; 13 | -------------------------------------------------------------------------------- /config/jest/server.json: -------------------------------------------------------------------------------- 1 | { 2 | "rootDir": "../../src-server", 3 | "collectCoverageFrom": [ 4 | "/**/*.{js,jsx,mjs}" 5 | ], 6 | "testMatch": [ 7 | "/**/__tests__/**/*.js", 8 | "/**/?(*.)(spec|test).js" 9 | ], 10 | "testEnvironment": "node", 11 | "moduleFileExtensions": [ 12 | "js", 13 | "json" 14 | ] 15 | } -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src-server/db.js: -------------------------------------------------------------------------------- 1 | const massive = require('massive'); 2 | const { connectionStr, schema } = require('../config/db'); 3 | 4 | module.exports = async () => { 5 | // connect to Massive and get the db instance 6 | const db = await massive(connectionStr, { 7 | // explicitly specify the used schemas 8 | allowedSchemas: [schema] 9 | }); 10 | 11 | return { db }; 12 | }; 13 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/db.js: -------------------------------------------------------------------------------- 1 | const hostWithPort = `${process.env.DB_HOST}${process.env.DB_PORT ? `:${process.env.DB_PORT}` : ''}`; 2 | let connectionStr = `postgres://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${hostWithPort}/${process.env.DB_NAME}`; 3 | if (process.env.DB_ENABLE_SSL) connectionStr += '?ssl=true'; 4 | 5 | const schema = 'public'; 6 | 7 | module.exports = { 8 | connectionStr, 9 | schema 10 | }; 11 | -------------------------------------------------------------------------------- /src-client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src-client/App.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', async () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | await new Promise((resolve) => { 9 | process.nextTick(() => { 10 | ReactDOM.unmountComponentAtNode(div); 11 | resolve(); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # public 10 | /public 11 | 12 | # build (output folder) 13 | /build 14 | 15 | .eslintcache 16 | 17 | # misc 18 | .idea 19 | .DS_Store 20 | .env 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | -------------------------------------------------------------------------------- /src-client/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | padding: 20px; 13 | color: white; 14 | } 15 | 16 | .App-title { 17 | font-size: 1.5em; 18 | } 19 | 20 | .App-intro { 21 | font-size: large; 22 | } 23 | 24 | @keyframes App-logo-spin { 25 | from { transform: rotate(0deg); } 26 | to { transform: rotate(360deg); } 27 | } 28 | -------------------------------------------------------------------------------- /migrations/1530234244089_initial-migration.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined; 2 | 3 | exports.up = (pgm) => { 4 | pgm.createTable('posts', { 5 | id: 'id', 6 | title: { 7 | type: 'text', 8 | notNull: true 9 | }, 10 | author: { 11 | type: 'text', 12 | notNull: true 13 | }, 14 | content: { 15 | type: 'text', 16 | notNull: true 17 | } 18 | }); 19 | pgm.createIndex('posts', 'author'); 20 | }; 21 | 22 | exports.down = (pgm) => { 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /src-server/components/auth/jwt.js: -------------------------------------------------------------------------------- 1 | const { Strategy: JWTStrategy, ExtractJwt } = require('passport-jwt'); 2 | const _ = require('lodash'); 3 | const secrets = require('../../../config/secrets'); 4 | 5 | module.exports = (app) => { 6 | const users = require('../users')(app); 7 | return new JWTStrategy({ 8 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 9 | secretOrKey: secrets.jwt 10 | }, 11 | async (jwtPayload, cb) => { 12 | try { 13 | const user = await users.getOne(jwtPayload.id); 14 | cb(null, _.omit(user, 'password')); 15 | } catch (err) { 16 | cb(err); 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /migrations/1555584527837_add-users.js: -------------------------------------------------------------------------------- 1 | exports.shorthands = undefined; 2 | 3 | exports.up = (pgm) => { 4 | pgm.createTable('users', { 5 | id: 'id', 6 | email: { 7 | type: 'varchar(255)', 8 | notNull: true 9 | }, 10 | password: { 11 | type: 'varchar(72)', 12 | notNull: true 13 | }, 14 | firstName: { 15 | type: 'varchar(255)', 16 | notNull: true 17 | }, 18 | lastName: { 19 | type: 'varchar(255)', 20 | notNull: true 21 | }, 22 | createdAt: { 23 | type: 'timestamp', 24 | notNull: true, 25 | default: pgm.func('current_timestamp') 26 | } 27 | }); 28 | }; 29 | 30 | exports.down = (pgm) => { 31 | 32 | }; 33 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const jest = require('jest'); 5 | 6 | // Do this as the first thing so that any code reading it knows the right env. 7 | process.env.BABEL_ENV = 'test'; 8 | process.env.NODE_ENV = 'test'; 9 | process.env.PUBLIC_URL = ''; 10 | 11 | // Makes the script crash on unhandled rejections instead of silently 12 | // ignoring them. In the future, promise rejections that are not handled will 13 | // terminate the Node.js process with a non-zero exit code. 14 | process.on('unhandledRejection', err => { 15 | throw err; 16 | }); 17 | 18 | // Ensure environment variables are read. 19 | require('../config/env'); 20 | 21 | const argv = process.argv.slice(2); 22 | 23 | jest.run(argv); 24 | -------------------------------------------------------------------------------- /src-server/components/auth/local.js: -------------------------------------------------------------------------------- 1 | const LocalStrategy = require('passport-local').Strategy; 2 | const _ = require('lodash'); 3 | const auth = require('./helpers'); 4 | 5 | module.exports = (app) => { 6 | const users = require('../users')(app); 7 | return new LocalStrategy({ 8 | usernameField: 'email', 9 | passwordField: 'password' 10 | }, 11 | async (email, password, cb) => { 12 | try { 13 | const user = await users.getOneByEmail(email); 14 | if (!user || !auth.checkPassword(password, user.password)) { 15 | cb(null, false, { message: 'Incorrect email or password.' }); 16 | return; 17 | } 18 | cb(null, _.omit(user, 'password'), {}); 19 | } catch (err) { 20 | cb(err); 21 | } 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /scripts/db-migrate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const migrate = require('node-pg-migrate'); 3 | 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | require('../config/env'); 6 | 7 | const { connectionStr, schema } = require('../config/db'); 8 | 9 | const databaseUrl = connectionStr; 10 | const direction = 'up'; 11 | const dir = './migrations'; 12 | const migrationsTable = 'migrations'; 13 | const migrationsSchema = schema; 14 | 15 | function main() { 16 | // Execute the migrations 17 | console.log("Running the migrations..."); 18 | return migrate.default({ 19 | databaseUrl, 20 | migrationsTable, 21 | migrationsSchema, 22 | schema, 23 | direction, 24 | dir 25 | }); 26 | } 27 | 28 | 29 | if (require.main === module) { 30 | main(); 31 | } else { 32 | module.exports = main; 33 | } 34 | -------------------------------------------------------------------------------- /src-client/private-route.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { withStore } from './store'; 5 | 6 | const PrivateRoute = ({ store, component: Component, ...rest }) => { 7 | const user = store.get('user'); 8 | return ( 9 | (user && user.id ? ( 12 | 13 | ) : ( 14 | 15 | ))} 16 | /> 17 | ); 18 | }; 19 | 20 | PrivateRoute.propTypes = { 21 | // eslint-disable-next-line react/forbid-prop-types 22 | store: PropTypes.object.isRequired, 23 | // eslint-disable-next-line react/forbid-prop-types 24 | component: PropTypes.any.isRequired 25 | }; 26 | 27 | export default withStore(PrivateRoute); 28 | -------------------------------------------------------------------------------- /src-server/components/users/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (app) => { 2 | const db = app.get('db'); 3 | const { users } = db; 4 | const module = {}; 5 | 6 | // Create 7 | module.create = async (row) => { 8 | if (!row) throw new Error('No row data given'); 9 | delete row.id; 10 | return users.save(row); 11 | }; 12 | 13 | // Get all 14 | module.get = async () => users.find(); 15 | 16 | // Get one 17 | module.getOne = async (id) => users.findOne({ id }); 18 | module.getOneByEmail = async (email) => users.findOne({ email }); 19 | 20 | // Update 21 | module.update = async (id, row) => { 22 | if (!Number(id)) throw new Error('No id given'); 23 | row.id = id; 24 | return users.save(row); 25 | }; 26 | 27 | // Delete 28 | module.delete = async (id) => { 29 | if (!Number(id)) throw new Error('No id given'); 30 | return users.destroy({ id }); 31 | }; 32 | 33 | return module; 34 | }; 35 | -------------------------------------------------------------------------------- /src-server/app.test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | 3 | const request = require('supertest'); 4 | const migrate = require('../scripts/db-migrate'); 5 | const seed = require('../scripts/db-seed'); 6 | const App = require('./app'); 7 | 8 | describe('Run basic server tests', () => { 9 | let app = {}; 10 | 11 | // Run migrations, clear DB, then seeding 12 | beforeAll(async () => { 13 | await migrate(); 14 | const { db } = await seed.openDB(); 15 | await seed.clearDB(db); 16 | await seed.seed(db); 17 | await seed.closeDB(db); 18 | }, 30000); 19 | 20 | // Wait for the app to load 21 | beforeAll(async () => { 22 | app = await App(); 23 | }, 30000); 24 | 25 | it('should have a successful DB connection', () => { 26 | const db = app.get('db'); 27 | return expect(typeof db).toBe('object'); 28 | }); 29 | 30 | it('should respond 200 to the [GET /]', () => request(app).get('/').expect(200)); 31 | }); 32 | -------------------------------------------------------------------------------- /src-client/components/posts/posts-container.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import PostAdd from './posts-add'; 4 | import PostTable from './posts-table'; 5 | 6 | export default class PostContainer extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.sendToEdit = this.sendToEdit.bind(this); 10 | this.refreshTable = this.refreshTable.bind(this); 11 | this.postAddForm = React.createRef(); 12 | this.postTable = React.createRef(); 13 | } 14 | 15 | sendToEdit(post) { 16 | this.postAddForm.current.updateAction(post); 17 | } 18 | 19 | refreshTable(post) { 20 | this.postTable.current.fetchPosts(post); 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | 27 | 28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src-server/components/posts/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (app) => { 2 | const db = app.get('db'); 3 | const { posts } = db; 4 | const module = {}; 5 | 6 | // Create 7 | module.create = async (user, row) => { 8 | if (!row) throw new Error('No row data given'); 9 | delete row.id; 10 | return posts.save({ ...row, user_id: user.id }); 11 | }; 12 | 13 | // Get all 14 | module.get = async () => db.query('select p.*, u.email as author from posts p left join users u ON p.user_id=u.id'); 15 | 16 | // Get one 17 | module.getOne = async (id) => db.query( 18 | 'select p.*, u.email as author from posts p left join users u ON p.user_id=u.id where p.id=$1', 19 | [id], 20 | { single: true } 21 | ); 22 | 23 | // Update 24 | module.update = async (id, row) => { 25 | if (!Number(id)) throw new Error('No id given'); 26 | row.id = id; 27 | return posts.save(row); 28 | }; 29 | 30 | // Delete 31 | module.delete = async (id) => { 32 | if (!Number(id)) throw new Error('No id given'); 33 | return posts.destroy({ id }); 34 | }; 35 | 36 | return module; 37 | }; 38 | -------------------------------------------------------------------------------- /src-client/store.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-multi-comp */ 2 | import React from 'react'; 3 | 4 | const StoreContext = React.createContext(); 5 | const createStore = (WrappedComponent) => ( 6 | class extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | // eslint-disable-next-line react/destructuring-assignment 11 | get: (key) => this.state[key], 12 | set: (key, value) => { 13 | this.setState({ [key]: value }); 14 | }, 15 | remove: (key) => { 16 | this.setState({ [key]: undefined }); 17 | } 18 | }; 19 | } 20 | 21 | render() { 22 | return ( 23 | 24 | 25 | 26 | ); 27 | } 28 | }); 29 | 30 | const withStore = (WrappedComponent) => ( 31 | function Wrapper(props) { 32 | return ( 33 | 34 | {(context) => } 35 | 36 | ); 37 | } 38 | ); 39 | 40 | export { StoreContext, createStore, withStore }; 41 | -------------------------------------------------------------------------------- /src-server/components/auth/helpers.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const jwt = require('jsonwebtoken'); 3 | const passport = require('passport'); 4 | const secrets = require('../../../config/secrets'); 5 | 6 | function createHash(password, salt) { 7 | const s = salt || crypto.randomBytes(4).toString('hex'); 8 | const hmac = crypto.createHmac('sha256', s); 9 | return s + hmac.update(password).digest('hex'); 10 | } 11 | 12 | function checkPassword(plain, hashed) { 13 | const salt = hashed.slice(0, 8); 14 | return createHash(plain, salt) === hashed; 15 | } 16 | 17 | function signUser(user) { 18 | return jwt.sign({ 19 | id: user.id, 20 | email: user.email 21 | }, secrets.jwt); 22 | } 23 | 24 | function authenticate(req, res, next) { 25 | passport.authenticate('jwt', { session: false }, (err1, user) => { 26 | if (err1 || !user) { 27 | res.status(401).json({ message: 'Unauthorized' }); 28 | return; 29 | } 30 | 31 | req.logIn(user, { session: false }, (err2) => { 32 | if (err2) { 33 | res.send(err2); 34 | } else { 35 | next(); 36 | } 37 | }); 38 | })(req, res); 39 | } 40 | 41 | module.exports = { 42 | createHash, checkPassword, signUser, authenticate 43 | }; 44 | -------------------------------------------------------------------------------- /src-server/routes/api/posts.js: -------------------------------------------------------------------------------- 1 | const Router = require('express-promise-router'); 2 | const _ = require('lodash'); 3 | const Posts = require('../../components/posts'); 4 | const auth = require('../../components/auth/helpers'); 5 | 6 | module.exports = (app) => { 7 | const router = Router(); 8 | const posts = Posts(app); 9 | 10 | // Create 11 | router.post('/', auth.authenticate, async (req, res) => { 12 | const data = await posts.create(req.user, _.pick(req.body, 'content', 'title')); 13 | res.json(data); 14 | }); 15 | 16 | // Get all 17 | router.get('/', auth.authenticate, async (req, res) => { 18 | const data = await posts.get(); 19 | res.json(data); 20 | }); 21 | 22 | // Get one 23 | router.get('/:id(\\d+)', auth.authenticate, async (req, res) => { 24 | const data = await posts.getOne(req.params.id); 25 | res.json(data); 26 | }); 27 | 28 | // Update 29 | router.put('/:id(\\d+)', auth.authenticate, async (req, res) => { 30 | const data = await posts.update(req.params.id, _.pick(req.body, 'content', 'title')); 31 | res.json(data); 32 | }); 33 | 34 | // Delete 35 | router.delete('/:id(\\d+)', auth.authenticate, async (req, res) => { 36 | const data = await posts.delete(req.params.id); 37 | res.json(data); 38 | }); 39 | 40 | return Router().use('/posts', router); 41 | }; 42 | -------------------------------------------------------------------------------- /src-server/routes/auth/index.js: -------------------------------------------------------------------------------- 1 | const Router = require('express-promise-router'); 2 | const passport = require('passport'); 3 | const _ = require('lodash'); 4 | const auth = require('../../components/auth/helpers'); 5 | 6 | module.exports = (app) => { 7 | const router = Router(); 8 | const users = require('../../components/users')(app); 9 | 10 | router.post('/login', (req, res) => { 11 | passport.authenticate('local', { session: false }, (err1, user) => { 12 | if (err1 || !user) { 13 | res.status(401).json({ message: 'Unauthorized' }); 14 | return; 15 | } 16 | 17 | req.logIn(user, { session: false }, (err2) => { 18 | if (err2) { 19 | res.send(err2); 20 | } else { 21 | const token = auth.signUser(user); 22 | res.json({ user, token }); 23 | } 24 | }); 25 | })(req, res); 26 | }); 27 | 28 | router.post('/register', async (req, res) => { 29 | const params = _.pick(req.body, 'email', 'firstName', 'lastName', 'password'); 30 | params.password = auth.createHash(params.password); 31 | console.log(params); 32 | 33 | const user = await users.create(params); 34 | const token = auth.signUser(user); 35 | return res.json({ user, token }); 36 | }); 37 | 38 | router.get('/me', auth.authenticate, (req, res) => res.json(req.user)); 39 | 40 | return router; 41 | }; 42 | -------------------------------------------------------------------------------- /src-client/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import PostsContainer from './components/posts/posts-container'; 6 | import SignIn from './components/auth/sign-in'; 7 | import SignUp from './components/auth/sign-up'; 8 | import Header from './components/header'; 9 | 10 | import 'bootstrap/dist/css/bootstrap.min.css'; 11 | import './App.css'; 12 | import { createStore } from './store'; 13 | import PrivateRoute from './private-route'; 14 | import API from './api/api'; 15 | 16 | // Define the main app 17 | const App = ({ store }) => { 18 | const user = store.get('user'); 19 | if (!user) { 20 | API.get('auth/me').then(({ data }) => { 21 | store.set('user', data); 22 | }).catch(() => { 23 | store.set('user', {}); 24 | }); 25 | return null; 26 | } 27 | 28 | return ( 29 | 30 |
31 |
32 |

33 | To get started, edit 34 | {' '} 35 | 36 | src/App.jsx 37 | 38 | {' '} 39 | and save to reload. 40 |

41 |
42 |
43 | 44 | 45 | 46 |
47 |
48 |
49 |
50 | ); 51 | }; 52 | 53 | App.propTypes = { 54 | // eslint-disable-next-line react/forbid-prop-types 55 | store: PropTypes.object.isRequired 56 | }; 57 | 58 | export default createStore(App); 59 | -------------------------------------------------------------------------------- /src-client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src-server/app.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const express = require('express'); 3 | const path = require('path'); 4 | const logger = require('morgan'); 5 | const passport = require('passport'); 6 | const DB = require('./db.js'); 7 | const apiPosts = require('./routes/api/posts'); 8 | const apiAuth = require('./routes/auth'); 9 | 10 | module.exports = async () => { 11 | const app = express(); 12 | 13 | // view engine setup 14 | app.set('views', path.join(__dirname, 'views')); 15 | app.set('view engine', 'ejs'); 16 | 17 | app.use(logger('dev')); 18 | app.use(express.json()); 19 | app.use(express.urlencoded({ extended: false })); 20 | 21 | const { db } = await DB(); 22 | app.set('db', db); 23 | 24 | app.use(express.static(path.join(__dirname, '../public'))); 25 | 26 | // Enable CORS 27 | app.use((req, res, next) => { 28 | res.header('Access-Control-Allow-Origin', '*'); 29 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); 30 | res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); 31 | next(); 32 | }); 33 | 34 | passport.use(require('./components/auth/local')(app)); 35 | passport.use(require('./components/auth/jwt')(app)); 36 | 37 | app.get('/', (req, res) => res.json({ status: 'ok' })); 38 | app.use('/auth', apiAuth(app)); 39 | app.use('/api', apiPosts(app)); 40 | 41 | /* 42 | app.use('/', (req, res) => { 43 | res.sendFile(path.join(__dirname, '../client/app/dist/index.html')); 44 | }); 45 | */ 46 | 47 | // catch 404 and forward to error handler 48 | app.use((req, res, next) => { 49 | next(createError(404)); 50 | }); 51 | 52 | // error handler 53 | app.use((err, req, res) => { 54 | // send the error response 55 | res.status(err.status || 500); 56 | if (err.status === 401) res.send(''); 57 | else res.send(err.message); 58 | }); 59 | 60 | return app; 61 | }; 62 | -------------------------------------------------------------------------------- /config/getHttpsConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const paths = require('./paths'); 8 | 9 | // Ensure the certificate and key provided are valid and if not 10 | // throw an easy to debug error 11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { 12 | let encrypted; 13 | try { 14 | // publicEncrypt will throw an error with an invalid cert 15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); 16 | } catch (err) { 17 | throw new Error( 18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` 19 | ); 20 | } 21 | 22 | try { 23 | // privateDecrypt will throw an error with an invalid key 24 | crypto.privateDecrypt(key, encrypted); 25 | } catch (err) { 26 | throw new Error( 27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ 28 | err.message 29 | }` 30 | ); 31 | } 32 | } 33 | 34 | // Read file and throw an error if it doesn't exist 35 | function readEnvFile(file, type) { 36 | if (!fs.existsSync(file)) { 37 | throw new Error( 38 | `You specified ${chalk.cyan( 39 | type 40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.` 41 | ); 42 | } 43 | return fs.readFileSync(file); 44 | } 45 | 46 | // Get the https config 47 | // Return cert files if provided in env, otherwise just true or false 48 | function getHttpsConfig() { 49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; 50 | const isHttps = HTTPS === 'true'; 51 | 52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { 53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); 54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); 55 | const config = { 56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), 57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'), 58 | }; 59 | 60 | validateKeyAndCerts({ ...config, keyFile, crtFile }); 61 | return config; 62 | } 63 | return isHttps; 64 | } 65 | 66 | module.exports = getHttpsConfig; 67 | -------------------------------------------------------------------------------- /src-client/components/header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | import { withStore } from '../store'; 6 | import logo from '../logo.svg'; 7 | import API from '../api/api'; 8 | 9 | // Define the main app 10 | class Header extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.handleLogout = this.handleLogout.bind(this); 14 | } 15 | 16 | componentDidMount() { 17 | const { store } = this.props; 18 | if (!store.get('user')) { 19 | API.get('auth/me').then(({ data }) => { 20 | store.set('user', data); 21 | }); 22 | } 23 | } 24 | 25 | handleLogout() { 26 | const { store } = this.props; 27 | store.set('user', {}); 28 | localStorage.removeItem('token'); 29 | } 30 | 31 | render() { 32 | const { store } = this.props; 33 | const user = store.get('user'); 34 | return ( 35 |
36 |
37 | logo 38 |

39 | Welcome to React. This demo was modified to include <POSTS> module 40 |

41 | {user && user.id && ( 42 |

43 | {'Hello, '} 44 | {user.firstName} 45 | {' '} 46 | {user.lastName} 47 |

48 | )} 49 |
50 | {!user.id && ( 51 | 52 | Sign In 53 | 54 | )} 55 | {!user.id && ( 56 | 57 | Sign Up 58 | 59 | )} 60 | {user && user.id && ( 61 | 64 | )} 65 |
66 |
67 |
68 | ); 69 | } 70 | } 71 | 72 | Header.propTypes = { 73 | // eslint-disable-next-line react/forbid-prop-types 74 | store: PropTypes.object.isRequired, 75 | }; 76 | export default withStore(Header); 77 | -------------------------------------------------------------------------------- /scripts/start-server-dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const http = require('http'); 3 | 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | require('../config/env'); 6 | const App = require('../src-server/app'); 7 | 8 | function main() { 9 | App().then((app) => { 10 | // Function for normalizing a port into a number, string, or false. 11 | function normalizePort(val) { 12 | const port = parseInt(val, 10); 13 | 14 | if (Number.isNaN(port)) { 15 | // named pipe 16 | return val; 17 | } 18 | 19 | if (port >= 0) { 20 | // port number 21 | return port; 22 | } 23 | 24 | return false; 25 | } 26 | 27 | // Event listener for HTTP server "error" event. 28 | function onError(error) { 29 | if (error.syscall !== 'listen') { 30 | throw error; 31 | } 32 | 33 | const bind = typeof port === 'string' 34 | ? `Pipe ${port}` 35 | : `Port ${port}`; 36 | 37 | // handle specific listen errors with friendly messages 38 | switch (error.code) { 39 | case 'EACCES': 40 | console.error(`${bind} requires elevated privileges`); 41 | process.exit(1); 42 | break; 43 | case 'EADDRINUSE': 44 | console.error(`${bind} is already in use`); 45 | process.exit(1); 46 | break; 47 | default: 48 | throw error; 49 | } 50 | } 51 | 52 | // Event listener for HTTP server "listening" event. 53 | function onListening() { 54 | const addr = server.address(); 55 | const bind = typeof addr === 'string' 56 | ? `pipe ${addr}` 57 | : `port ${addr.port}`; 58 | console.log(`Listening on ${bind}`); 59 | } 60 | 61 | // ---------- 62 | // MAIN CODE 63 | // ---------- 64 | 65 | // Get port from environment and store in Express. 66 | const port = normalizePort(process.env.PORT || '5000'); 67 | app.set('port', port); 68 | 69 | // Create HTTP server. 70 | const server = http.createServer(app); 71 | 72 | // Listen on provided port, on all network interfaces. 73 | server.listen(port); 74 | server.on('error', onError); 75 | server.on('listening', onListening); 76 | }).catch((err) => { 77 | console.error(err); 78 | process.exit(1); 79 | }); 80 | } 81 | 82 | if (require.main === module) { 83 | main(); 84 | } else { 85 | module.exports = main; 86 | } 87 | -------------------------------------------------------------------------------- /scripts/db-seed.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const faker = require('faker'); 3 | const _ = require('lodash'); 4 | 5 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 6 | require('../config/env'); 7 | 8 | const DB = require('../src-server/db'); 9 | const auth = require('../src-server/components/auth/helpers'); 10 | 11 | const tablePosts = 'posts'; 12 | const tableUsers = 'users'; 13 | 14 | const createRecordPost = (db, table, user) => db[table].insert({ 15 | title: faker.commerce.product(), 16 | author: faker.internet.userName(), 17 | content: faker.lorem.text(), 18 | user_id: user.id, 19 | }); 20 | 21 | function openDB() { 22 | console.log('Connecting to the DB...'); 23 | return DB(); 24 | } 25 | 26 | function seedPosts(db, users) { 27 | // Seed with fake data 28 | console.log('Seeding [posts]...'); 29 | const records = []; 30 | try { 31 | for (let i = 1; i <= 10; i += 1) { 32 | const user = users[_.random(users.length - 1)]; 33 | records.push(createRecordPost(db, tablePosts, user)); 34 | } 35 | } catch (e) { 36 | console.error(e); 37 | } 38 | 39 | return Promise.all(records); 40 | } 41 | 42 | function seedUsers(db) { 43 | console.log('Seeding [users]...'); 44 | const users = [{ 45 | email: 'user@test.com', 46 | password: auth.createHash('password'), 47 | firstName: 'User', 48 | lastName: 'Test' 49 | }]; 50 | 51 | return db[tableUsers].insert(users); 52 | } 53 | 54 | function seed(db) { 55 | // Run seeding functions 56 | return seedUsers(db) 57 | .then(users => seedPosts(db, users)) 58 | .then(() => { 59 | console.log('Successfully completed the seeding process'); 60 | }); 61 | } 62 | 63 | function clearDB(db) { 64 | if (process.env.NODE_ENV !== 'test') throw new Error('ClearDB can only be run on TEST DB!!!'); 65 | 66 | // Clear [posts] and restart the sequence 67 | return db[tablePosts].destroy({}) 68 | .then(() => db.query('ALTER SEQUENCE posts_id_seq RESTART WITH 1')) 69 | .then(() => db[tableUsers].destroy({})) 70 | .then(() => db.query('ALTER SEQUENCE users_id_seq RESTART WITH 1')) 71 | .then(() => { 72 | console.log('Successfully cleared the DB'); 73 | }); 74 | } 75 | 76 | function closeDB(db) { 77 | return db.instance.$pool.end(); 78 | } 79 | 80 | if (require.main === module) { 81 | openDB().then(db => seed(db.db)); 82 | } else { 83 | module.exports = { 84 | openDB, seed, clearDB, closeDB 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 4 | 5 | // Make sure any symlinks in the project folder are resolved: 6 | // https://github.com/facebook/create-react-app/issues/637 7 | const appDirectory = fs.realpathSync(process.cwd()); 8 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 9 | 10 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 11 | // "public path" at which the app is served. 12 | // webpack needs to know it to put the right