9 | );
10 | };
11 |
12 | ReactDOM.render(, document.querySelector('#root'));
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # auth-graphql-starter
2 |
3 | Starter project from a GraphQL course on Udemy.com - Section 3!
4 |
5 | ### Setup
6 |
7 | - Run `npm install --legacy-peer-deps` in the root of the project to install dependencies
8 | - Access the application at `localhost:4000` in your browser
9 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: './client/index.js',
6 | output: {
7 | path: '/',
8 | filename: 'bundle.js'
9 | },
10 | module: {
11 | rules: [
12 | {
13 | use: 'babel-loader',
14 | test: /\.js$/,
15 | exclude: /node_modules/
16 | }
17 | ]
18 | },
19 | plugins: [
20 | new HtmlWebpackPlugin({
21 | template: 'client/index.html'
22 | })
23 | ]
24 | };
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "users",
3 | "version": "1.0.0",
4 | "description": "Starter pack for an auth-included graphql project",
5 | "repository": {
6 | "type": "git",
7 | "url": "github.com/stephengrider"
8 | },
9 | "main": "index.js",
10 | "scripts": {
11 | "dev": "nodemon index.js --ignore client"
12 | },
13 | "author": "",
14 | "license": "ISC",
15 | "dependencies": {
16 | "apollo-client": "^0.8.1",
17 | "axios": "^0.15.3",
18 | "babel-core": "^6.22.1",
19 | "babel-loader": "^6.2.10",
20 | "babel-preset-env": "^1.1.8",
21 | "babel-preset-react": "^6.22.0",
22 | "bcrypt-nodejs": "0.0.3",
23 | "body-parser": "^1.16.0",
24 | "connect-mongo": "^3.2.0",
25 | "css-loader": "^0.26.1",
26 | "express": "^4.14.0",
27 | "express-graphql": "^0.6.1",
28 | "express-session": "^1.15.0",
29 | "graphql": "^0.8.2",
30 | "graphql-tag": "^1.2.4",
31 | "html-webpack-plugin": "^2.26.0",
32 | "lodash": "^4.17.4",
33 | "mongoose": "^6.11.2",
34 | "nodemon": "^2.0.22",
35 | "passport": "^0.3.2",
36 | "passport-local": "^1.0.0",
37 | "react": "^15.4.2",
38 | "react-apollo": "^0.9.0",
39 | "react-dom": "^15.4.2",
40 | "react-router": "^3.0.2",
41 | "style-loader": "^0.13.1",
42 | "webpack": "^2.2.0",
43 | "webpack-dev-middleware": "^1.9.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server/models/user.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcrypt-nodejs');
2 | const crypto = require('crypto');
3 | const mongoose = require('mongoose');
4 | const Schema = mongoose.Schema;
5 |
6 | // Every user has an email and password. The password is not stored as
7 | // plain text - see the authentication helpers below.
8 | const UserSchema = new Schema({
9 | email: String,
10 | password: String
11 | });
12 |
13 | // The user's password is never saved in plain text. Prior to saving the
14 | // user model, we 'salt' and 'hash' the users password. This is a one way
15 | // procedure that modifies the password - the plain text password cannot be
16 | // derived from the salted + hashed version. See 'comparePassword' to understand
17 | // how this is used.
18 | UserSchema.pre('save', function save(next) {
19 | const user = this;
20 | if (!user.isModified('password')) { return next(); }
21 | bcrypt.genSalt(10, (err, salt) => {
22 | if (err) { return next(err); }
23 | bcrypt.hash(user.password, salt, null, (err, hash) => {
24 | if (err) { return next(err); }
25 | user.password = hash;
26 | next();
27 | });
28 | });
29 | });
30 |
31 | // We need to compare the plain text password (submitted whenever logging in)
32 | // with the salted + hashed version that is sitting in the database.
33 | // 'bcrypt.compare' takes the plain text password and hashes it, then compares
34 | // that hashed password to the one stored in the DB. Remember that hashing is
35 | // a one way process - the passwords are never compared in plain text form.
36 | UserSchema.methods.comparePassword = function comparePassword(candidatePassword, cb) {
37 | bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
38 | cb(err, isMatch);
39 | });
40 | };
41 |
42 | mongoose.model('user', UserSchema);
43 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const models = require('./models');
3 | const expressGraphQL = require('express-graphql');
4 | const mongoose = require('mongoose');
5 | const session = require('express-session');
6 | const passport = require('passport');
7 | const passportConfig = require('./services/auth');
8 | const MongoStore = require('connect-mongo')(session);
9 | const schema = require('./schema/schema');
10 |
11 | // Create a new Express application
12 | const app = express();
13 |
14 | // Replace with your Mongo Atlas URI
15 | const MONGO_URI = '';
16 | if (!MONGO_URI) {
17 | throw new Error('You must provide a Mongo Atlas URI');
18 | }
19 |
20 | // Mongoose's built in promise library is deprecated, replace it with ES2015 Promise
21 | mongoose.Promise = global.Promise;
22 |
23 | // Connect to the mongoDB instance and log a message
24 | // on success or failure
25 | mongoose.set('strictQuery', false);
26 |
27 | mongoose.connect(MONGO_URI);
28 | mongoose.connection
29 | .once('open', () => console.log('Connected to Mongo Atlas instance.'))
30 | .on('error', (error) =>
31 | console.log('Error connecting to Mongo Atlas:', error)
32 | );
33 |
34 | // Configures express to use sessions. This places an encrypted identifier
35 | // on the users cookie. When a user makes a request, this middleware examines
36 | // the cookie and modifies the request object to indicate which user made the request
37 | // The cookie itself only contains the id of a session; more data about the session
38 | // is stored inside of MongoDB.
39 | app.use(
40 | session({
41 | resave: true,
42 | saveUninitialized: true,
43 | secret: 'aaabbbccc',
44 | store: new MongoStore({
45 | url: MONGO_URI,
46 | autoReconnect: true
47 | })
48 | })
49 | );
50 |
51 | // Passport is wired into express as a middleware. When a request comes in,
52 | // Passport will examine the request's session (as set by the above config) and
53 | // assign the current user to the 'req.user' object. See also servces/auth.js
54 | app.use(passport.initialize());
55 | app.use(passport.session());
56 |
57 | // Instruct Express to pass on any request made to the '/graphql' route
58 | // to the GraphQL instance.
59 | app.use(
60 | '/graphql',
61 | expressGraphQL({
62 | schema,
63 | graphiql: true
64 | })
65 | );
66 |
67 | // Webpack runs as a middleware. If any request comes in for the root route ('/')
68 | // Webpack will respond with the output of the webpack process: an HTML file and
69 | // a single bundle.js output of all of our client side Javascript
70 | const webpackMiddleware = require('webpack-dev-middleware');
71 | const webpack = require('webpack');
72 | const webpackConfig = require('../webpack.config.js');
73 | app.use(webpackMiddleware(webpack(webpackConfig)));
74 |
75 | module.exports = app;
76 |
--------------------------------------------------------------------------------
/server/services/auth.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const passport = require('passport');
3 | const LocalStrategy = require('passport-local').Strategy;
4 |
5 | const User = mongoose.model('user');
6 |
7 | // SerializeUser is used to provide some identifying token that can be saved
8 | // in the users session. We traditionally use the 'ID' for this.
9 | passport.serializeUser((user, done) => {
10 | done(null, user.id);
11 | });
12 |
13 | // The counterpart of 'serializeUser'. Given only a user's ID, we must return
14 | // the user object. This object is placed on 'req.user'.
15 | passport.deserializeUser((id, done) => {
16 | User.findById(id, (err, user) => {
17 | done(err, user);
18 | });
19 | });
20 |
21 | // Instructs Passport how to authenticate a user using a locally saved email
22 | // and password combination. This strategy is called whenever a user attempts to
23 | // log in. We first find the user model in MongoDB that matches the submitted email,
24 | // then check to see if the provided password matches the saved password. There
25 | // are two obvious failure points here: the email might not exist in our DB or
26 | // the password might not match the saved one. In either case, we call the 'done'
27 | // callback, including a string that messages why the authentication process failed.
28 | // This string is provided back to the GraphQL client.
29 | passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
30 | User.findOne({ email: email.toLowerCase() }, (err, user) => {
31 | if (err) { return done(err); }
32 | if (!user) { return done(null, false, 'Invalid Credentials'); }
33 | user.comparePassword(password, (err, isMatch) => {
34 | if (err) { return done(err); }
35 | if (isMatch) {
36 | return done(null, user);
37 | }
38 | return done(null, false, 'Invalid credentials.');
39 | });
40 | });
41 | }));
42 |
43 | // Creates a new user account. We first check to see if a user already exists
44 | // with this email address to avoid making multiple accounts with identical addresses
45 | // If it does not, we save the existing user. After the user is created, it is
46 | // provided to the 'req.logIn' function. This is apart of Passport JS.
47 | // Notice the Promise created in the second 'then' statement. This is done
48 | // because Passport only supports callbacks, while GraphQL only supports promises
49 | // for async code! Awkward!
50 | function signup({ email, password, req }) {
51 | const user = new User({ email, password });
52 | if (!email || !password) { throw new Error('You must provide an email and password.'); }
53 |
54 | return User.findOne({ email })
55 | .then(existingUser => {
56 | if (existingUser) { throw new Error('Email in use'); }
57 | return user.save();
58 | })
59 | .then(user => {
60 | return new Promise((resolve, reject) => {
61 | req.logIn(user, (err) => {
62 | if (err) { reject(err); }
63 | resolve(user);
64 | });
65 | });
66 | });
67 | }
68 |
69 | // Logs in a user. This will invoke the 'local-strategy' defined above in this
70 | // file. Notice the strange method signature here: the 'passport.authenticate'
71 | // function returns a function, as its indended to be used as a middleware with
72 | // Express. We have another compatibility layer here to make it work nicely with
73 | // GraphQL, as GraphQL always expects to see a promise for handling async code.
74 | function login({ email, password, req }) {
75 | return new Promise((resolve, reject) => {
76 | passport.authenticate('local', (err, user) => {
77 | if (!user) { reject('Invalid credentials.') }
78 |
79 | req.login(user, () => resolve(user));
80 | })({ body: { email, password } });
81 | });
82 | }
83 |
84 | module.exports = { signup, login };
85 |
--------------------------------------------------------------------------------