├── .gitignore ├── server ├── models │ ├── index.js │ └── user.js ├── schema │ ├── schema.js │ └── types │ │ └── root_query_type.js ├── server.js └── services │ └── auth.js ├── .babelrc ├── .DS_Store ├── index.js ├── client ├── index.js └── index.html ├── README.md ├── webpack.config.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /server/models/index.js: -------------------------------------------------------------------------------- 1 | require('./user'); 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephenGrider/auth-graphql-starter/master/.DS_Store -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const app = require('./server/server'); 2 | 3 | app.listen(4000, () => { 4 | console.log('Listening'); 5 | }); 6 | -------------------------------------------------------------------------------- /server/schema/schema.js: -------------------------------------------------------------------------------- 1 | const graphql = require('graphql'); 2 | const { GraphQLSchema } = graphql; 3 | 4 | const RootQueryType = require('./types/root_query_type'); 5 | 6 | module.exports = new GraphQLSchema({ 7 | query: RootQueryType 8 | }); 9 | -------------------------------------------------------------------------------- /server/schema/types/root_query_type.js: -------------------------------------------------------------------------------- 1 | const graphql = require('graphql'); 2 | const { GraphQLObjectType } = graphql; 3 | 4 | const RootQueryType = new GraphQLObjectType({ 5 | name: 'RootQueryType' 6 | }); 7 | 8 | module.exports = RootQueryType; 9 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | const Root = () => { 5 | return ( 6 |
7 | Auth Starter 8 |
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 | --------------------------------------------------------------------------------