├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── example-simple-react ├── Procfile ├── README.md ├── client │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── App.js │ │ ├── App.test.js │ │ ├── components │ │ │ ├── LoginButton.js │ │ │ ├── LoginMenu.js │ │ │ └── Navbar.js │ │ ├── index.css │ │ ├── index.js │ │ ├── pages │ │ │ ├── CreateAccountPage.js │ │ │ ├── HomePage.js │ │ │ ├── LoginPage.js │ │ │ └── NotFoundPage.js │ │ ├── registerServiceWorker.js │ │ └── services │ │ │ └── withUser.js │ └── yarn.lock ├── nodemon.json ├── package.json ├── passport.js ├── routes │ ├── apiRoutes.js │ ├── htmlRoutes.js │ └── index.js ├── server.js └── yarn.lock ├── example-simple ├── README.md ├── package.json ├── passport.js ├── routes │ ├── htmlRoutes.js │ └── index.js ├── server.js ├── views │ ├── create.handlebars │ ├── error.handlebars │ ├── home.handlebars │ ├── layouts │ │ └── main.handlebars │ └── login.handlebars └── yarn.lock ├── example-social-media-react ├── .env ├── Procfile ├── README.md ├── client │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── App.js │ │ ├── App.test.js │ │ ├── components │ │ │ ├── LoginButton.js │ │ │ ├── LoginMenu.js │ │ │ ├── Navbar.js │ │ │ └── ProtectedRoute.js │ │ ├── index.css │ │ ├── index.js │ │ ├── pages │ │ │ ├── AuthFailedPage.js │ │ │ ├── CreateAccountPage.js │ │ │ ├── HomePage.js │ │ │ ├── LoginPage.js │ │ │ ├── MembersOnlyPage.js │ │ │ ├── NotFoundPage.js │ │ │ ├── TestSpotifyPage.js │ │ │ └── TestTwitterPage.js │ │ ├── registerServiceWorker.js │ │ └── services │ │ │ └── withUser.js │ └── yarn.lock ├── nodemon.json ├── package.json ├── passport.js ├── routes │ ├── apiRoutes.js │ ├── htmlRoutes.js │ └── index.js ├── server.js └── yarn.lock ├── package.json ├── shared ├── middleware │ ├── mongoose.js │ └── mustBeLoggedIn.js └── models │ ├── SocialMediaMembership.js │ ├── User.js │ └── index.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | insert_final_newline = false 22 | trim_trailing_whitespace = false 23 | 24 | [*.pug] 25 | trim_trailing_whitespace = false 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | build 4 | .env 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mike Grabski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PassportJS Examples 2 | 3 | This repository contains several example configurations using PassportJS as authentication middleware. PassportJS is very flexible, at a cost of being a bit confusing to set up and tailor to your particular application's needs. 4 | 5 | Please note this repository is NOT a conclusive or exhaustive list of ways you can leverage PassportJS. 6 | 7 | ## Project Structure 8 | 9 | To save myself time and typing, I will reuse as much code as possible. I will also make each example as simple as possible while demonstrating functionality and proper structure. 10 | 11 | Here's what's inside: 12 | 13 | - `shared`: This folder contains any code that is shared between all the examples, like user models and utilities. 14 | - `shared/middleware`: This folder contains any common middleware for setting up internals of the server, like connecting to mongoose. 15 | - `shared/models`: This folder contains any common mongoose models, most importantly it has our User model. 16 | 17 | ### Examples 18 | 19 | - `example-simple`: A very simple Express server that uses Handlebars and basic form posts to authenticate users using the `passport-local` strategy. See the README in that folder for more info. 20 | - `example-simple-react`: A very simple express server that uses React and the `passport-local` strategy. This example also shows a way to ensure someone can't access a route unless they are logged in (see `/shared/middleware/mustBeLoggedIn.js`). This could actually be used in any express server using passport on any route. 21 | - `example-social-media-react`: A refinement of the simple React app, but supports multiple social media logins in addition to username/password. As an added bonus, it shows how to use access tokens provided by the social media site's passport strategy to access the user's data from the social media site's API. In this example, users that log in via Spotify can retrieve their playlists, and likewise for Twitter users' tweets. 22 | 23 | 24 | ## Miscellaneous 25 | 26 | - Any examples that use server-side template rendering are using Handlebars. Pug is better :), but this set of examples was built primarily for Bootcamp students that were taught Handlebars. Using Pug instead is quite easy, but that exercise is left to you at the moment. 27 | 28 | ## What is PassportJS 29 | 30 | PassportJS is a Node package intended to be used with the ExpressJS web applications. It can be dropped into your application to add authentication support. Your application will instruct PassportJS to use one or more **Strategies**. 31 | 32 | ### Strategies 33 | 34 | A Strategy is like middleware for PassportJS that attempts to authenticate a user based on a request. How the Strategy authenticates a user is dependent on the Strategy implementation you decide to use. Strategies can vary from simple, such as [LocalStrategy](https://github.com/jaredhanson/passport-local) who simply authenticates a user using username/password against your application (usually using a database), to a more complex strategy using OAuth 2 that allows users to log in using a socia media account. There are [500+ strategies](http://www.passportjs.org/packages/), so the place to start is determining how you want users to be able to authenticate. Start simple and add from there; remember that your app is allowed to use several strategies. 35 | 36 | #### Do you want users to be able to sign up using username and password? 37 | 38 | Use the [passport-local](https://github.com/jaredhanson/passport-local) package for the LocalStrategy. Users will simply authenticate using a username and password, and you'll configure the strategy on how to find the user in your database and then check the provided password is correct. 39 | 40 | **Caveat**: This is effectively managing the account's username and password inside your application. Security is HARD and absolutely CRITICAL. So if you're not ready for secure password management, go with a social media identity provider (Google, Twitter, Facebook, etc.) instead. Additionally, if your app will only work by accessing a user's data on a social media site, then you should _not_ use LocalStrategy, but the strategy for that social media site. 41 | 42 | #### Do you need to access a social media API on behalf of a user? 43 | 44 | Use the appropriate strategy for the social media site. Here are some common strategies: 45 | 46 | - Google: [passport-google-oauth](https://github.com/jaredhanson/passport-google-oauth) 47 | - Twitter: [passport-twitter](https://github.com/jaredhanson/passport-twitter) 48 | - Spotify: [passport-spotify](https://github.com/JMPerez/passport-spotify) 49 | - Facebook: [passport-facebook](https://github.com/jaredhanson/passport-facebook) 50 | 51 | #### Do you want to have a mix of local and social media accounts, or have advanced API authentication requirements and don't want to do all the auth code yourself? 52 | 53 | Consider signing up for Auth0. Security is hard, and if you're not comfortable doing it or ready to take on the responsibility, let the pros do it for you. It's free to start, can secure your APIs, and allows you to easily implement authentication using any variety of identity providers, including custom username/password database, Facebook, Google, etc. Auth0 does the heavy lifting for securely managing credentials, OAuth 2 exchanges between your apps and the identity providers, and all you need to do is drop a little code and some config values into your app. It also makes many other advanced authn/authz tasks easy for you, like SSO, SAML, and a whole slew of other things. 54 | 55 | The Auth0 team has provided the [passport-auth0](https://github.com/auth0/passport-auth0) to drop into your app. You can get more detailed and ready-to-cut-and-paste code once you create an account and create your first client in your account. -------------------------------------------------------------------------------- /example-simple-react/Procfile: -------------------------------------------------------------------------------- 1 | web: npm run server 2 | -------------------------------------------------------------------------------- /example-simple-react/README.md: -------------------------------------------------------------------------------- 1 | # Simple Example Using Passport-Local with Express and React 2 | 3 | The home page hides sensitive data unless you're logged in. The navbar also changes based on whether or not you are logged in. Finally, it has create user and login views. 4 | 5 | ## Running the Example 6 | 7 | 1. In a separate terminal window, start monogd 8 | 2. Navigate to the root of the repo (the parent of `example-simple-react`) 9 | 3. Run `yarn install` 10 | 4. Navigate back to `example-simple-react` 11 | 5. Run `yarn install`. 12 | 6. `cd client` 13 | 7. `yarn install` 14 | 8. `cd ..` 15 | 9. Run `yarn start` to run the server 16 | 10. Create-react-app will start your express server and launch your browser to http://localhost:3000 for you. 17 | 18 | ## Highlights 19 | 20 | `server.js` is pretty typical express server. We use handlebars as the renderer, body-parser, and connects using mogoose before starting. The most interesting parts are on lines 21 through 28. We have some code in separate modules to _first_ configure Passport and _second_ configure our routes (which have dependencies on passport). 21 | 22 | `passport.js` has our passport configuration. The order the various pieces are configured are important. Other than that, there are descriptive comments about what each section is doing. You do NOT have to use sessions, however for typical web apps they are probably the simplest and best way to keep a user logged in once they have logged in. So, I'd recommend setting that up unless you know you definitely don't want or need it. 23 | 24 | `routes/apiRoutes.js` This contains a few routes: 25 | 26 | - `/api/auth` is where we go to read, create, or delete our current login session. The create portion (`POST /api/auth`) uses the `passport.authenticate` middleware, which will read the user's credentials they posted and either log them in or return a 401 error. `GET /api/auth` simply returns the currently logged in user's info, if they are logged in. `DELETE /api/auth` will effectively log the user out. 27 | - `/api/users` has a POST route for creating a new user. 28 | - `/api/stuff` is just a test route that returns an array of strings if the user is logged in. if the user is not logged in, it will return a 403 Forbidden error. 29 | 30 | `routes/htmlRoutes.js` merely renders the index.html in client/build for any route (other than our API routes). This is to make the client-side react-router experience work correctly. 31 | 32 | `client/src/services/withUser.js` is a little bit of React magic. We need a single state to track our current user object. We also need to be able to inject that user as a prop to any component that needs to be aware of the current user. Said components also need to have their user prop updated if the user state changes. Finally, we need a way of updating the state from a component so everyone is notified. There are a few ways to do this, but in this example we're using what's called a High Order Component. It comes with a function called `withUser` that can be called to wrap any component. It will then do exactly what was described above: it will ensure your wrapped component receives a user prop and keep it up-to-date if the user state changes. It also has an `update` function that can be imported and called by any component that needs to change the state (as you can see in `client/src/pages/LoginPage.js`). 33 | 34 | This is a greatly simplified version of what you might see in Flux or Redux. You can use Flux, Redux, etc. if you want a more mature approach that can be used to track other states your app needs if you want, but be aware the learning curve for something like Redux is deceptively high. 35 | 36 | Finally, the React app is using `material-ui` and `react-flexbox-grid` for UI. You obviously don't need to use these modules, and use your favorite react component library. 37 | 38 | ## Production Notes 39 | 40 | Client-side validation is important. The forms used to create an account and log in do bare minimum validation and are probably not suitable for a real application. You might want to use a validation module or form module with built-in validation for React. 41 | 42 | If you ARE using sessions, you need to use a production-ready session store. A session store is a bit of code that express-session uses to store session data. Without a store, it can't keep track of sessions and the whole thing won't work. By default, express-session has a built-in store that just keeps sessions in memory. That is what we're using here in this example. However, the authors of express-session strongly warn you against using this default store in production. [There are lots of other stores to choose from](https://github.com/expressjs/session#compatible-session-stores), like storing session data in Redis, Memcache, or even a database. I like using Redis to cache information like sessions, but you need a Redis server that your local and production servers can connect to in order to get this working. -------------------------------------------------------------------------------- /example-simple-react/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:3001/", 6 | "dependencies": { 7 | "axios": "^0.18.0", 8 | "material-ui": "^0.20.0", 9 | "react": "^16.0.0", 10 | "react-dom": "^16.0.0", 11 | "react-flexbox-grid": "^2.0.0", 12 | "react-router-dom": "^4.2.2", 13 | "react-scripts": "1.0.14" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example-simple-react/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grbsk/passport-examples/f6e6af6289f3743b2d08e0e7f49194eed0840e55/example-simple-react/client/public/favicon.ico -------------------------------------------------------------------------------- /example-simple-react/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /example-simple-react/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": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example-simple-react/client/src/App.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { Component, Fragment } from 'react'; 3 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 5 | 6 | import Navbar from './components/Navbar'; 7 | 8 | import { withUser, update } from './services/withUser'; 9 | 10 | import CreateAccountPage from './pages/CreateAccountPage'; 11 | import HomePage from './pages/HomePage'; 12 | import LoginPage from './pages/LoginPage'; 13 | import NotFoundPage from './pages/NotFoundPage'; 14 | 15 | class App extends Component { 16 | componentDidMount() { 17 | // this is going to double check that the user is still actually logged in 18 | // if the app is reloaded. it's possible that we still have a user in sessionStorage 19 | // but the user's session cookie expired. 20 | axios.get('/api/auth') 21 | .then(res => { 22 | // if we get here, the user's session is still good. we'll update the user 23 | // to make sure we're using the most recent values just in case 24 | update(res.data); 25 | }) 26 | .catch(err => { 27 | // if we get a 401 response, that means the user is no longer logged in 28 | if (err.response.status === 401) { 29 | update(null); 30 | } 31 | }); 32 | } 33 | render() { 34 | const { user } = this.props; 35 | return ( 36 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | export default withUser(App); 56 | -------------------------------------------------------------------------------- /example-simple-react/client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /example-simple-react/client/src/components/LoginButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FlatButton from 'material-ui/FlatButton'; 3 | 4 | const LoginButton = (props) => ( 5 | 6 | ); 7 | 8 | export default LoginButton; 9 | -------------------------------------------------------------------------------- /example-simple-react/client/src/components/LoginMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IconButton from 'material-ui/IconButton'; 3 | import IconMenu from 'material-ui/IconMenu'; 4 | import MenuItem from 'material-ui/MenuItem'; 5 | import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'; 6 | 7 | const LoginMenu = (props) => { 8 | const { onLogOut, username, ...otherProps } = props; 9 | return ( 10 | 14 | } 15 | targetOrigin={{ horizontal: 'right', vertical: 'top' }} 16 | anchorOrigin={{ horizontal: 'right', vertical: 'top' }} 17 | > 18 | 19 | 20 | 21 | ) 22 | }; 23 | 24 | export default LoginMenu; 25 | -------------------------------------------------------------------------------- /example-simple-react/client/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React from 'react'; 3 | import { withRouter } from 'react-router-dom'; 4 | import AppBar from 'material-ui/AppBar'; 5 | 6 | import LoginButton from './LoginButton'; 7 | import LoginMenu from './LoginMenu'; 8 | 9 | import { update } from '../services/withUser'; 10 | 11 | const Navbar = (props) => { 12 | const { user } = props; 13 | const username = user ? user.username : null; 14 | const handleLogIn = () => { 15 | props.history.push('/login'); 16 | }; 17 | const handleLogOut = () => { 18 | axios.delete('/api/auth') 19 | .then(() => { 20 | // unsets the currently logged in user. all components wrapped in withUser 21 | // will be updated with a null user and rerender accordingly 22 | update(null); 23 | }) 24 | .catch((err) => { 25 | console.log(err); 26 | }); 27 | } 28 | return ( 29 | 34 | : } 35 | /> 36 | ) 37 | }; 38 | 39 | export default withRouter(Navbar); 40 | -------------------------------------------------------------------------------- /example-simple-react/client/src/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | min-height: 100%; 5 | font-family: 'Roboto', sans-serif 6 | } 7 | -------------------------------------------------------------------------------- /example-simple-react/client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import './index.css'; 5 | import registerServiceWorker from "./registerServiceWorker"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /example-simple-react/client/src/pages/CreateAccountPage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { Component } from 'react'; 3 | import { Grid, Row, Col } from 'react-flexbox-grid'; 4 | import TextField from 'material-ui/TextField'; 5 | import RaisedButton from 'material-ui/RaisedButton'; 6 | 7 | class CreateAccountPage extends Component { 8 | state = { 9 | username: null, 10 | password: null, 11 | error: null 12 | } 13 | handleInputChanged = (event) => { 14 | this.setState({ 15 | [event.target.name]: event.target.value 16 | }); 17 | } 18 | handleLogin = (event) => { 19 | event.preventDefault(); 20 | 21 | const { username, password } = this.state; 22 | const { history } = this.props; 23 | 24 | // clear any previous errors so we don't confuse the user 25 | this.setState({ 26 | error: null 27 | }); 28 | 29 | // check to make sure they've entered a username and password. 30 | // this is very poor validation, and there are better ways 31 | // to do this in react, but this will suffice for the example 32 | if (!username || !password) { 33 | this.setState({ 34 | error: 'A username and password is required.' 35 | }); 36 | return; 37 | } 38 | 39 | // post an auth request 40 | axios.post('/api/users', { 41 | username, 42 | password 43 | }) 44 | .then(user => { 45 | // if the response is successful, make them log in 46 | history.push('/login'); 47 | }) 48 | .catch(err => { 49 | 50 | this.setState({ 51 | error: err.response.data.message || err.message 52 | }); 53 | }); 54 | } 55 | render() { 56 | const { error } = this.state; 57 | 58 | return ( 59 | 60 | 61 | 62 |
63 |

Create Account

64 | {error && 65 |
66 | {error} 67 |
68 | } 69 |
70 | 76 |
77 |
78 | 85 |
86 |
87 | 88 | Create Account 89 | 90 |
91 |
92 | 93 |
94 |
95 | ); 96 | } 97 | } 98 | 99 | export default CreateAccountPage; 100 | -------------------------------------------------------------------------------- /example-simple-react/client/src/pages/HomePage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { Component, Fragment } from 'react'; 3 | import { List, ListItem } from 'material-ui/List'; 4 | import { withUser } from '../services/withUser'; 5 | 6 | class HomePage extends Component { 7 | state = { 8 | stuff: null 9 | } 10 | componentDidMount() { 11 | // only try loading stuff if the user is logged in. 12 | if (!this.props.user) { 13 | return; 14 | } 15 | 16 | axios.get('/api/stuff') 17 | .then(res => { 18 | this.setState({ 19 | stuff: res.data 20 | }); 21 | }) 22 | .catch(err => { 23 | // if we got an error, we'll just log it and set stuff to an empty array 24 | console.log(err); 25 | this.setState({ 26 | stuff: [] 27 | }); 28 | }); 29 | } 30 | render() { 31 | const { user } = this.props; // get the user prop from props 32 | const { stuff } = this.state; // get stuff from state 33 | 34 | return ( 35 | 36 | {user && stuff && 37 |
38 | Welcome back, {user.username}! 39 | 40 | {stuff.map((s, i) => )} 41 | 42 |
43 | } 44 | {user && !stuff && 45 |
Hold on, looking for your stuff...
46 | } 47 | {!user && 48 |
Hey! I don't recognize you! Register and log in using the link above
49 | } 50 |
51 | ); 52 | } 53 | } 54 | 55 | // withUser function will wrap the specified component in another component that will 56 | // inject the currently logged in user as a prop called "user" 57 | export default withUser(HomePage); 58 | -------------------------------------------------------------------------------- /example-simple-react/client/src/pages/LoginPage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { Component } from 'react'; 3 | import { Grid, Row, Col } from 'react-flexbox-grid'; 4 | import { Link } from 'react-router-dom'; 5 | import TextField from 'material-ui/TextField'; 6 | import RaisedButton from 'material-ui/RaisedButton'; 7 | import { update } from '../services/withUser'; 8 | 9 | class LoginPage extends Component { 10 | state = { 11 | username: null, 12 | password: null 13 | } 14 | handleInputChanged = (event) => { 15 | this.setState({ 16 | [event.target.name]: event.target.value 17 | }); 18 | } 19 | handleLogin = (event) => { 20 | event.preventDefault(); 21 | 22 | const { username, password } = this.state; 23 | const { history } = this.props; 24 | 25 | // post an auth request 26 | axios.post('/api/auth', { 27 | username, 28 | password 29 | }) 30 | .then(user => { 31 | // if the response is successful, update the current user and redirect to the home page 32 | update(user.data); 33 | history.push('/'); 34 | }) 35 | .catch(err => { 36 | // an error occured, so let's record the error in our state so we can display it in render 37 | // if the error response status code is 401, it's an invalid username or password. 38 | // if it's any other status code, there's some other unhandled error so we'll just show 39 | // the generic message. 40 | this.setState({ 41 | error: err.response.status === 401 ? 'Invalid username or password.' : err.message 42 | }); 43 | }); 44 | } 45 | render() { 46 | const { error } = this.state; 47 | 48 | return ( 49 | 50 | 51 | 52 |
53 |

Log In

54 | {error && 55 |
56 | {error} 57 |
58 | } 59 |
60 | 66 |
67 |
68 | 75 |
76 |
77 | 78 | Log In 79 | 80 |
81 |

82 | or 83 |

84 |

85 | 86 | Register 87 | 88 |

89 |
90 | 91 |
92 |
93 | ); 94 | } 95 | } 96 | 97 | export default LoginPage; 98 | -------------------------------------------------------------------------------- /example-simple-react/client/src/pages/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class NotFoundPage extends Component { 4 | render() { 5 | return ( 6 |
7 | Page not found. 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default NotFoundPage; 14 | -------------------------------------------------------------------------------- /example-simple-react/client/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === "localhost" || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === "[::1]" || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener("load", () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === "installed") { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log("New content is available; please refresh."); 60 | } else { 61 | // At this point, everything has been pre-cached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log("Content is cached for offline use."); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error("Error during service worker registration:", error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get("content-type").indexOf("javascript") === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | "No internet connection found. App is running in offline mode." 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ("serviceWorker" in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /example-simple-react/client/src/services/withUser.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // checks to see if the session storage already has a user key 4 | // if it does, try to parse it as JSON string into an object, and use 5 | // that as the starting value for state. What this does is effectively 6 | // retain the currently logged in user if the user does a manual refresh 7 | // in their browser. if we don't back the user by something that remains 8 | // between refreshes, the user will "lose" their logged in state. 9 | const stateFromStore = sessionStorage.getItem('user'); 10 | let state = stateFromStore ? JSON.parse(stateFromStore) : null; 11 | 12 | const subscribers = []; 13 | 14 | const unsubscribe = subscriber => { 15 | const index = subscribers.findIndex(subscriber); 16 | index >= 0 && subscribers.splice(index, 1); 17 | }; 18 | const subscribe = subscriber => { 19 | subscribers.push(subscriber); 20 | return () => unsubscribe(subscriber); 21 | }; 22 | 23 | export const withUser = Component => { 24 | return class WithUser extends React.Component { 25 | componentDidMount() { 26 | this.unsubscribe = subscribe(this.forceUpdate.bind(this)); 27 | } 28 | render() { 29 | const newProps = { ...this.props, user: state }; 30 | return ; 31 | } 32 | componentWillUnmount() { 33 | this.unsubscribe(); 34 | } 35 | }; 36 | }; 37 | 38 | export const update = newState => { 39 | state = newState; 40 | // update the "user" key in the session storage with whatever the new state value is 41 | // Remember to stringify the state object because sessionStorage can only store strings 42 | sessionStorage.setItem('user', state ? JSON.stringify(state) : null); 43 | subscribers.forEach(subscriber => subscriber()); 44 | }; 45 | -------------------------------------------------------------------------------- /example-simple-react/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "client/*" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /example-simple-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern", 3 | "version": "1.0.0", 4 | "description": "Mern Demo", 5 | "main": "server.js", 6 | "scripts": { 7 | "server": "node server.js", 8 | "client": "cd client && npm run start", 9 | "start": "./node_modules/.bin/concurrently \"./node_modules/.bin/nodemon\" \"npm run client\"", 10 | "build": "cd client && npm run build", 11 | "deploy": "yarn build && git add . && git commit -m \"Building for production\" && git push heroku master", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "concurrently": "^3.5.0", 18 | "nodemon": "^1.11.0" 19 | }, 20 | "dependencies": { 21 | "body-parser": "^1.18.2", 22 | "cookie-parser": "^1.4.3", 23 | "express": "^4.15.4", 24 | "express-session": "^1.15.6", 25 | "morgan": "^1.9.0", 26 | "passport": "^0.4.0", 27 | "passport-local": "^1.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example-simple-react/passport.js: -------------------------------------------------------------------------------- 1 | const session = require('express-session'); 2 | const cookieparser = require('cookie-parser'); 3 | const passport = require('passport'); 4 | const LocalStrategy = require('passport-local').Strategy; 5 | const db = require('../shared/models'); 6 | 7 | // export a function that receives the Express app we will configure for Passport 8 | module.exports = (app) => { 9 | // these two middlewares are required to make passport work with sessions 10 | // sessions are optional, but an easy solution to keeping users 11 | // logged in until they log out. 12 | app.use(cookieparser()); 13 | app.use(session({ 14 | // this should be changed to something cryptographically secure for production 15 | secret: 'keyboard cat', 16 | resave: false, 17 | saveUninitialized: false, 18 | // automatically extends the session age on each request. useful if you want 19 | // the user's activity to extend their session. If you want an absolute session 20 | // expiration, set to false 21 | rolling: true, 22 | name: 'sid', // don't use the default session cookie name 23 | // set your options for the session cookie 24 | cookie: { 25 | httpOnly: true, 26 | // the duration in milliseconds that the cookie is valid 27 | maxAge: 20 * 60 * 1000, // 20 minutes 28 | // recommended you use this setting in production if you have a well-known domain you want to restrict the cookies to. 29 | // domain: 'your.domain.com', 30 | // recommended you use this setting in production if your site is published using HTTPS 31 | // secure: true, 32 | } 33 | })); 34 | 35 | // Only necessary when using sessions. 36 | // This tells Passport how or what data to save about a user in the session cookie. 37 | // It's recommended you only serialize something like a unique username or user ID. 38 | // I prefer user ID. 39 | passport.serializeUser((user, done) => { 40 | done(null, user._id); 41 | }); 42 | 43 | // Only necessary when using sessions. 44 | // This tells Passport how to turn the user ID we serialize in the session cookie 45 | // back into the actual User record from our Mongo database. 46 | // Here, we simply find the user with the matching ID and return that. 47 | // This will cause the User record to be available on each authenticated request via the req.user property. 48 | passport.deserializeUser(function (userId, done) { 49 | db.User.findById(userId) 50 | .then(function (user) { 51 | done(null, user); 52 | }) 53 | .catch(function (err) { 54 | done(err); 55 | }); 56 | }); 57 | 58 | // this tells passport to use the "local" strategy, and configures the strategy 59 | // with a function that will be called when the user tries to authenticate with 60 | // a username and password. We simply look the user up, hash the password they 61 | // provided with the salt from the real password, and compare the results. if 62 | // the original and current hashes are the same, the user entered the correct password. 63 | passport.use(new LocalStrategy((username, password, done) => { 64 | const errorMsg = 'Invalid username or password'; 65 | 66 | db.User.findOne({ username }) 67 | .then(user => { 68 | // if no matching user was found... 69 | if (!user) { 70 | return done(null, false, { message: errorMsg }); 71 | } 72 | 73 | // call our validate method, which will call done with the user if the 74 | // passwords match, or false if they don't 75 | return user.validatePassword(password) 76 | .then(isMatch => done(null, isMatch ? user : false, isMatch ? null : { message: errorMsg })); 77 | }) 78 | .catch(done); 79 | })); 80 | 81 | // initialize passport. this is required, after you set up passport but BEFORE you use passport.session (if using) 82 | app.use(passport.initialize()); 83 | // only required if using sessions. this will add middleware from passport 84 | // that will serialize/deserialize the user from the session cookie and add 85 | // them to req.user 86 | app.use(passport.session()); 87 | } 88 | -------------------------------------------------------------------------------- /example-simple-react/routes/apiRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const passport = require('passport'); 3 | const router = express.Router(); 4 | const db = require('../../shared/models'); 5 | const mustBeLoggedIn = require('../../shared/middleware/mustBeLoggedIn'); 6 | 7 | function getCurrentUser(req, res) { 8 | // I'm picking only the specific fields its OK for the audience to see publicly 9 | // never send the whole user object in the response, and only show things it's OK 10 | // for others to read (like ID, name, email address, etc.) 11 | const { id, username } = req.user; 12 | res.json({ 13 | id, username 14 | }); 15 | } 16 | 17 | router.route('/auth') 18 | // GET to /api/auth will return current logged in user info 19 | .get((req, res) => { 20 | if (!req.user) { 21 | return res.status(401).json({ 22 | message: 'You are not currently logged in.' 23 | }) 24 | } 25 | 26 | getCurrentUser(req, res); 27 | }) 28 | // POST to /api/auth with username and password will authenticate the user 29 | .post(passport.authenticate('local'), (req, res) => { 30 | if (!req.user) { 31 | return res.status(401).json({ 32 | message: 'Invalid username or password.' 33 | }) 34 | } 35 | 36 | getCurrentUser(req, res); 37 | }) 38 | // DELETE to /api/auth will log the user out 39 | .delete((req, res) => { 40 | req.logout(); 41 | req.session.destroy(); 42 | res.json({ 43 | message: 'You have been logged out.' 44 | }); 45 | }); 46 | 47 | router.route('/users') 48 | // POST to /api/users will create a new user 49 | .post((req, res, next) => { 50 | db.User.create(req.body) 51 | .then(user => { 52 | const { id, username } = user; 53 | res.json({ 54 | id, username 55 | }); 56 | }) 57 | .catch(err => { 58 | // if this error code is thrown, that means the username already exists. 59 | // let's handle that nicely by redirecting them back to the create screen 60 | // with that flash message 61 | if (err.code === 11000) { 62 | res.status(400).json({ 63 | message: 'Username already in use.' 64 | }) 65 | } 66 | 67 | // otherwise, it's some nasty unexpected error, so we'll just send it off to 68 | // to the next middleware to handle the error. 69 | next(err); 70 | }); 71 | }); 72 | 73 | // this route is just returns an array of strings if the user is logged in 74 | // to demonstrate that we can ensure a user must be logged in to use a route 75 | router.route('/stuff') 76 | .get(mustBeLoggedIn(), (req, res) => { 77 | // at this point we can assume the user is logged in. if not, the mustBeLoggedIn middleware would have caught it 78 | res.json([ 79 | 'Bears', 80 | 'Beets', 81 | 'Battlestar Galactica' 82 | ]); 83 | }); 84 | 85 | 86 | module.exports = router; 87 | 88 | -------------------------------------------------------------------------------- /example-simple-react/routes/htmlRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require("path"); 3 | const router = express.Router(); 4 | 5 | // Serve up static assets (usually on heroku) 6 | router.use(express.static("client/build")); 7 | 8 | // Send every request to the React app 9 | // Define any API routes before this runs 10 | router.get("*", function (req, res) { 11 | res.sendFile(path.join(__dirname, "../client/build/index.html")); 12 | }); 13 | 14 | 15 | module.exports = router; 16 | 17 | -------------------------------------------------------------------------------- /example-simple-react/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const router = express.Router(); 4 | // add API routes to current router 5 | // NOTE: API routes must be added first, because htmlRoutes has a wildcard route 6 | // which will swallow anything that isn't matched first 7 | // NOTE: All routes exported from apiRoutes will get placed under the /api path 8 | // this is just to save a little typing so in my api routes I don't have to put 9 | // /api in front of each route. 10 | router.use('/api', require('./apiRoutes')); 11 | 12 | // add HTML routes to current router 13 | router.use(require('./htmlRoutes')); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /example-simple-react/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const bodyparser = require('body-parser'); 3 | const morgan = require('morgan'); 4 | 5 | const PORT = process.env.PORT || 3001; 6 | const app = express(); 7 | 8 | // let's set up some basic middleware for our express app 9 | // logs requests to the console. not necessary to make passport work, but useful 10 | app.use(morgan('dev')); 11 | // Use body-parser for reading form submissions into objects 12 | app.use(bodyparser.urlencoded({ extended: true })); 13 | // Use body-parser for reading application/json into objects 14 | app.use(bodyparser.json()); 15 | 16 | // Serve up static assets (usually on heroku) 17 | if (process.env.NODE_ENV === "production") { 18 | app.use(express.static("client/build")); 19 | } 20 | 21 | // configure using our exported passport function. 22 | // we need to pass the express app we want configured! 23 | // order is important! you need to set up passport 24 | // before you start using it in your routes. 25 | require('./passport')(app); 26 | 27 | // use the routes we configured. 28 | app.use(require('./routes')); 29 | 30 | // Here's a little custom error handling middleware 31 | // that logs the error to console, then renders an 32 | // error page describing the error. 33 | app.use((error, req, res, next) => { 34 | console.error(error); 35 | res.json({ 36 | error 37 | }) 38 | }); 39 | 40 | // configure mongoose 41 | require('../shared/middleware/mongoose')() 42 | .then(() => { 43 | // mongo is connected, so now we can start the express server. 44 | app.listen(PORT, () => console.log(`Server up and running on ${PORT}.`)); 45 | }) 46 | .catch(err => { 47 | // an error occurred connecting to mongo! 48 | // log the error and exit 49 | console.error('Unable to connect to mongo.') 50 | console.error(err); 51 | }); 52 | -------------------------------------------------------------------------------- /example-simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple Example Using Passport-Local with a Traditional Server 2 | 3 | By "traditional server", I mean a web server that uses simple UI with simple form posts. 4 | 5 | The home page hides sensitive data unless you're logged in. The navbar also changes based on whether or not you are logged in. Finally, it has create user and login views. 6 | 7 | ## Running the Example 8 | 9 | 1. In a separate terminal window, start monogd 10 | 2. Navigate to the root of the repo (the parent of `example-simple`) 11 | 3. Run `yarn install` 12 | 4. Navigate back to `example-simple` 13 | 5. Run `yarn install`. 14 | 6. Run `yarn start` to run the server 15 | 7. Open your browser and visit [http://localhost:3001](http://localhost:3001 16 | 17 | ## Highlights 18 | 19 | `server.js` is pretty typical express server. We use handlebars as the renderer, body-parser, and connects using mogoose before starting. The most interesting parts are on lines 21 through 28. We have some code in separate modules to _first_ configure Passport and _second_ configure our routes (which have dependencies on passport). 20 | 21 | `passport.js` has our passport configuration. The order the various pieces are configured are important. Other than that, there are descriptive comments about what each section is doing. You do NOT have to use sessions, however for typical web apps they are probably the simplest and best way to keep a user logged in once they have logged in. So, I'd recommend setting that up unless you know you definitely don't want or need it. 22 | 23 | `routes/htmRoutes.js` contain our routes for the homepage, creating an account, and logging in and logging out. The most important part of the PassportJS integration starts on line 20. This is the route to which our login form POSTs the username and password they entered. This route uses the `passport.authenticate` middleware, which we are instructing to use the `local` strategy. It will call our strategy's verify callback that we configured, which essentially passes in the username/password the user entered and validates the credentials. If the user's credentials are correct, we redirect back to the home page (you can do whatever you want, though). If the credentials are _incorrect_, we send them back to the login page. We are telling `passport.authenticate` to use the `failureFlash` option so that we can send the authentication failure message back to the login page so it can be displayed. You do not need to use this, but it is handy. In order to make this work, you need to install the `connect-flash` middleware and configure it (we configure it on line 37 of `passport.js`). Next, you need to call `req.flash('error')` in the route where you want to read the error message (if there is one). You can see examples on lines 16 and 47 of `htmlRoutes.js`. Basically, what we're doing is creating a data object that includes a flash property set to the result of `req.flash('error')`, which gets sent to the handlebars template that we're rendering. Speaking of which... 24 | 25 | The last notable parts are using handlebars to conditionally render based on the data object we pass in. You can see in `views/layouts/main.handlebars`, we look to see if the data object has a `user` property. If they do, we consider the user logged in and show the logged in version of the navbar. Otherwise, the navbar shows a link to log in. Note that our route calling `res.render` needs to pass in a data object with the property called `user` that is equal to the result of `req.user`. This effectively passes the current logged in user for the request to our handlebars templates. You can see it in use on line 9 of `htmlRoutes.js`. 26 | 27 | ## Production Notes 28 | 29 | If you ARE using sessions, you need to use a production-ready session store. A session store is a bit of code that express-session uses to store session data. Without a store, it can't keep track of sessions and the whole thing won't work. By default, express-session has a built-in store that just keeps sessions in memory. That is what we're using here in this example. However, the authors of express-session strongly warn you against using this default store in production. [There are lots of other stores to choose from](https://github.com/expressjs/session#compatible-session-stores), like storing session data in Redis, Memcache, or even a database. I like using Redis to cache information like sessions, but you need a Redis server that your local and production servers can connect to in order to get this working. -------------------------------------------------------------------------------- /example-simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-simple", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon server.js" 9 | }, 10 | "author": "Mike Grabski (http://mikegrabski.com/)", 11 | "license": "MIT", 12 | "dependencies": { 13 | "body-parser": "^1.18.2", 14 | "connect-flash": "^0.1.1", 15 | "cookie-parser": "^1.4.3", 16 | "express": "^4.16.3", 17 | "express-handlebars": "^3.0.0", 18 | "express-session": "^1.15.6", 19 | "morgan": "^1.9.0", 20 | "passport": "^0.4.0", 21 | "passport-local": "^1.0.0" 22 | }, 23 | "devDependencies": { 24 | "nodemon": "^1.17.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example-simple/passport.js: -------------------------------------------------------------------------------- 1 | const session = require('express-session'); 2 | const cookieparser = require('cookie-parser'); 3 | const passport = require('passport'); 4 | const LocalStrategy = require('passport-local').Strategy; 5 | const flash = require('connect-flash'); 6 | const db = require('../shared/models'); 7 | 8 | // export a function that receives the Express app we will configure for Passport 9 | module.exports = (app) => { 10 | // these two middlewares are required to make passport work with sessions 11 | // sessions are optional, but an easy solution to keeping users 12 | // logged in until they log out. 13 | app.use(cookieparser()); 14 | app.use(session({ 15 | // this should be changed to something cryptographically secure for production 16 | secret: 'keyboard cat', 17 | resave: false, 18 | saveUninitialized: false, 19 | // automatically extends the session age on each request. useful if you want 20 | // the user's activity to extend their session. If you want an absolute session 21 | // expiration, set to false 22 | rolling: true, 23 | name: 'sid', // don't use the default session cookie name 24 | // set your options for the session cookie 25 | cookie: { 26 | httpOnly: true, 27 | // the duration in milliseconds that the cookie is valid 28 | maxAge: 20 * 60 * 1000, // 20 minutes 29 | // recommended you use this setting in production if you have a well-known domain you want to restrict the cookies to. 30 | // domain: 'your.domain.com', 31 | // recommended you use this setting in production if your site is published using HTTPS 32 | // secure: true, 33 | } 34 | })); 35 | // you don't need to use the flash middleware. however, in this example, 36 | // we're using it so we can show any authentication error messages in our login form. 37 | app.use(flash()); 38 | 39 | // Only necessary when using sessions. 40 | // This tells Passport how or what data to save about a user in the session cookie. 41 | // It's recommended you only serialize something like a unique username or user ID. 42 | // I prefer user ID. 43 | passport.serializeUser((user, done) => { 44 | done(null, user._id); 45 | }); 46 | 47 | // Only necessary when using sessions. 48 | // This tells Passport how to turn the user ID we serialize in the session cookie 49 | // back into the actual User record from our Mongo database. 50 | // Here, we simply find the user with the matching ID and return that. 51 | // This will cause the User record to be available on each authenticated request via the req.user property. 52 | passport.deserializeUser(function (userId, done) { 53 | db.User.findById(userId) 54 | .then(function (user) { 55 | done(null, user); 56 | }) 57 | .catch(function (err) { 58 | done(err); 59 | }); 60 | }); 61 | 62 | // this tells passport to use the "local" strategy, and configures the strategy 63 | // with a function that will be called when the user tries to authenticate with 64 | // a username and password. We simply look the user up, hash the password they 65 | // provided with the salt from the real password, and compare the results. if 66 | // the original and current hashes are the same, the user entered the correct password. 67 | passport.use(new LocalStrategy((username, password, done) => { 68 | const errorMsg = 'Invalid username or password'; 69 | 70 | db.User.findOne({username}) 71 | .then(user => { 72 | // if no matching user was found... 73 | if (!user) { 74 | return done(null, false, {message: errorMsg}); 75 | } 76 | 77 | // call our validate method, which will call done with the user if the 78 | // passwords match, or false if they don't 79 | return user.validatePassword(password) 80 | .then(isMatch => done(null, isMatch ? user : false, isMatch ? null : { message: errorMsg })); 81 | }) 82 | .catch(done); 83 | })); 84 | 85 | // initialize passport. this is required, after you set up passport but BEFORE you use passport.session (if using) 86 | app.use(passport.initialize()); 87 | // only required if using sessions. this will add middleware from passport 88 | // that will serialize/deserialize the user from the session cookie and add 89 | // them to req.user 90 | app.use(passport.session()); 91 | } 92 | -------------------------------------------------------------------------------- /example-simple/routes/htmlRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const passport = require('passport'); 3 | const db = require('../../shared/models'); 4 | const router = express.Router(); 5 | 6 | // shows the home view. if the user is logged in, their User record is available at req.user 7 | router.get('/', (req, res) => { 8 | res.render('home', { 9 | user: req.user 10 | }); 11 | }); 12 | 13 | // simply shows the login view. 14 | router.get('/login', (req, res) => { 15 | res.render('login', { 16 | flash: req.flash('error') 17 | }); 18 | }); 19 | 20 | // configures this route to authenticate the request against the "local" strategy 21 | // if they authenticate correcty, it'll redirect back to the home page. if they do not 22 | // it'll send them back to the login page. 23 | router.post( 24 | '/login', 25 | passport.authenticate( 26 | 'local', 27 | { 28 | successRedirect: '/', 29 | failureRedirect: '/login', 30 | failureFlash: true 31 | } 32 | ) 33 | ); 34 | 35 | // logs the user out and destroys their current session, then sends them back to the homepage 36 | router.get('/logout', (req, res) => { 37 | // logs the user out 38 | req.logout(); 39 | // destroys their session completely 40 | req.session.destroy(); 41 | res.redirect('/'); 42 | }); 43 | 44 | // shows the create view. 45 | router.get('/create', (req, res) => { 46 | res.render('create', { 47 | flash: req.flash('error') 48 | }); 49 | }); 50 | 51 | // when a user submits a new user, this will create the account. 52 | router.post('/create', (req, res, next) => { 53 | const user = new db.User(req.body); 54 | user.save(req.body) 55 | .then(req => { 56 | res.redirect('/login'); 57 | }) 58 | .catch(err => { 59 | // if this error code is thrown, that means the username already exists. 60 | // let's handle that nicely by redirecting them back to the create screen 61 | // with that flash message 62 | if (err.code === 11000) { 63 | req.flash("error", "That username is already in use."); 64 | return res.redirect('/create'); 65 | } 66 | 67 | // otherwise, it's some nasty unexpected error, so we'll just send it off to 68 | // to the next middleware to handle the error. 69 | next(err); 70 | }); 71 | }); 72 | 73 | module.exports = router; 74 | 75 | -------------------------------------------------------------------------------- /example-simple/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const router = express.Router(); 4 | 5 | // add HTML routes to current router 6 | router.use(require('./htmlRoutes')); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /example-simple/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyparser = require('body-parser'); 3 | const morgan = require('morgan'); 4 | const handlebars = require('express-handlebars'); 5 | 6 | const PORT = process.env.PORT || 3001; 7 | const app = express(); 8 | 9 | // let's set up some basic middleware for our express app 10 | // logs requests to the console. not necessary to make passport work, but useful 11 | app.use(morgan('dev')); 12 | // Use body-parser for reading form submissions into objects 13 | app.use(bodyparser.urlencoded({ extended: true })); 14 | // Use body-parser for reading application/json into objects 15 | app.use(bodyparser.json()); 16 | 17 | // configure handlebars (or pug, if you prefer) 18 | app.engine("handlebars", handlebars({ defaultLayout: "main" })); 19 | app.set("view engine", "handlebars"); 20 | 21 | // configure using our exported passport function. 22 | // we need to pass the express app we want configured! 23 | // order is important! you need to set up passport 24 | // before you start using it in your routes. 25 | require('./passport')(app); 26 | 27 | // use the routes we configured. 28 | app.use(require('./routes')); 29 | 30 | // Here's a little custom error handling middleware 31 | // that logs the error to console, then renders an 32 | // error page describing the error. 33 | app.use((error, req, res, next) => { 34 | console.error(error); 35 | res.render('error', { 36 | user: req.user, 37 | error 38 | }); 39 | }); 40 | 41 | // configure mongoose 42 | require('../shared/middleware/mongoose')() 43 | .then(() => { 44 | // mongo is connected, so now we can start the express server. 45 | app.listen(PORT, () => console.log(`Server up and running on ${PORT}.`)); 46 | }) 47 | .catch(err => { 48 | // an error occurred connecting to mongo! 49 | // log the error and exit 50 | console.error('Unable to connect to mongo.') 51 | console.error(err); 52 | }); 53 | 54 | -------------------------------------------------------------------------------- /example-simple/views/create.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

Create a New Account

3 |
4 |
5 | {{#if flash}} 6 |
7 |
8 |
{{flash}}
9 |
10 |
11 | {{/if}} 12 |
13 |
14 | 15 | 16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /example-simple/views/error.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

Sorry, an unexpected error occurred!

3 |

{{error.message}}

4 |
5 |     {{error.stack}}
6 |   
7 |
8 | -------------------------------------------------------------------------------- /example-simple/views/home.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{#if user}} 3 | Welcome back, {{user.username}}! Since you're logged in, you can see the secret stuff. 4 | 5 |
    6 |
  • Bears
  • 7 |
  • Beets
  • 8 |
  • Battlestar Galactica
  • 9 |
10 | {{/if}} 11 | {{#unless user}} 12 | Hey! You have to log in to see secret stuff! Click "Log In" at the top. 13 | {{/unless}} 14 |
15 | -------------------------------------------------------------------------------- /example-simple/views/layouts/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PassportJS Simple Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{#if user}} 17 | 22 | {{/if}} 23 | 45 | 46 |
47 | {{{body}}} 48 |
49 | 50 | 51 | 53 | 54 | 55 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /example-simple/views/login.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

Log In

3 |
4 |
5 | {{#if flash}} 6 |
7 |
8 |
{{flash}}
9 |
10 |
11 | {{/if}} 12 |
13 |
14 | 15 | 16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 | 31 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /example-social-media-react/.env: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT COMMIT YOUR .env FILE!!! This file is only used for development! 2 | # I'm only including it here so you can see what you need to configure. 3 | # Committing a .env file is bad because you might accidentally expose your 4 | # API keys and secrets to the public if they browse your repo 5 | # YOU HAVE BEEN WARNED! 6 | 7 | # Necessary config values for Spotify Integration 8 | SPOTIFY_CLIENT_ID=your-client-id-here 9 | SPOTIFY_CLIENT_SECRET=your-client-secret-here 10 | 11 | # necessary config values for Twitter integration 12 | TWITTER_CONSUMER_KEY=your-consumer-key-here 13 | TWITTER_CONSUMER_SECRET=your-consumer-secret-here 14 | 15 | # You'll need to change this to match the URL of your published site on Heroku 16 | # e.g. https://my-app.herokuapp.com 17 | APP_BASE_URL=http://localhost:3001 18 | -------------------------------------------------------------------------------- /example-social-media-react/Procfile: -------------------------------------------------------------------------------- 1 | web: npm run server 2 | -------------------------------------------------------------------------------- /example-social-media-react/README.md: -------------------------------------------------------------------------------- 1 | # Simple Example Using Passport-Local AND Passport-Spotify AAAAAAND Passport-Twitter, with Express and React 2 | 3 | This example is built off the previous example-simple-react. The major additions are that, in addition to signing up using username/password, users have the option to sign up via Spotify _or_ Twitter. And yes, it is possible to set your app up so a user can link multiple social media memberships to their current account. Perhaps I'll set that up in a later example. Also, if your app doesn't need a username/password option or the ability to utilize multiple social media accounts (say, for example, your app exclusively uses Spotify to function, you don't need username/password or any other social media provider). Easy! Just strip out or ignore anything you don't need! This example will still show you how to authenticate using various providers and then connect to their APIs. 4 | 5 | With that in mind, users that log in with Spotify or Twitter will be directed to bonus examples for the respective social media site they logged in with. Be aware, however, that until you refine this app to allow users to add additional social media memberships to an existing account, you'll only be able to see the Twitter example if logged in via Twitter, Spotify via Spotify, and username/password people can only see stuff on the homepage! Why? Well, naturally you need an access token for the respective API you're trying to query, and that comes from the strategy when we log in. 6 | 7 | ## Running the Example 8 | 9 | 1. In a separate terminal window, start monogd 10 | 2. Navigate to the root of the repo (the parent of `example-simple-react`) 11 | 3. Run `yarn install` 12 | 4. Navigate back to `example-simple-react` 13 | 5. Run `yarn install`. 14 | 6. `cd client` 15 | 7. `yarn install` 16 | 8. `cd ..` 17 | 9. Run `yarn start` to run the server 18 | 10. Create-react-app will start your express server and launch your browser to http://localhost:3000 for you. 19 | 20 | ## Highlights 21 | 22 | (in the root) `shared/models/SocialMediaMembership.js` is a new Mongoose model that we use to keep track of all of the social media accounts a user has linked to their user account. The model describes the type of membership via the `provider` prop (e.g. `"spotify"` or `"twitter"`). It also captures the social media site's ID for the user via the `providerUserId` prop. This is important because this ID is how we can find the user in our local database using the social media account every time they log in. So in essence, this model is the bridge from the social media site to our local user. It's also used to capture any access tokens or other information that social media site may require to access the user's data from that site's API. **WARNING**: You should never store tokens in plaintext anywhere in your application. You'll want to encrypt them before storing, and decrypt them before using them. However, that's beyond the scope of this example so I'm keeping it simple (for now). Google encrypting and decrypting strings in node and you should be able to figure it out from there. 23 | 24 | `.env` has configuration keys for local development **ONLY**. 25 | 26 | `package.json` has been modified on line 9 to use `dotenv` to load the .env file variables when running in local development mode. 27 | 28 | `server.js` is pretty typical express server. We use handlebars as the renderer, body-parser, and connects using mogoose before starting. The most interesting parts are on lines 21 through 28. We have some code in separate modules to _first_ configure Passport and _second_ configure our routes (which have dependencies on passport). 29 | 30 | `passport.js` has our passport configuration. The order the various pieces are configured are important. Other than that, there are descriptive comments about what each section is doing. You do NOT have to use sessions, however for typical web apps they are probably the simplest and best way to keep a user logged in once they have logged in. So, I'd recommend setting that up unless you know you definitely don't want or need it. The major difference in this example of passport.js is that we define multiple strategies. Remove any strategies you don't need, and add any you want. See the documentation for your strategy for more info on how to implement it. These examples should give you a good idea on how to start. Here are the basics, though: 31 | 32 | 1. Search the `SocialMediaMembership` for a record that matches the social media site's user ID and the provider your strategy uses. 33 | 2. If we find a matching record, that means the user has logged in before and we can simply let the strategy know. Easy enough. 34 | 3. If we _don't_ find a maching record, this is a brand new user. Now, it's up to you how you want to handle this. The simplest thing to do, but you're not required to do, is just create a new user and membership on the spot and consider them logged in. If your app has complex needs, you can set up this workflow however you want. But we're keeping it simple here. Anyway, once you create the new user, let the Strategy know. 35 | 36 | Remember to register in the social media provider's developer area to receive a client ID and secret for your app! 37 | 38 | `routes/apiRoutes.js` This contains a few routes: 39 | 40 | - `/api/auth` is where we go to read, create, or delete our current login session. The create portion (`POST /api/auth`) uses the `passport.authenticate` middleware, which will read the user's credentials they posted and either log them in or return a 401 error. `GET /api/auth` simply returns the currently logged in user's info, if they are logged in. `DELETE /api/auth` will effectively log the user out. 41 | - `/api/users` has a POST route for creating a new user. 42 | - `/api/stuff` is just a test route that returns an array of strings if the user is logged in. if the user is not logged in, it will return a 403 Forbidden error. 43 | - `/api/playlists` is a test route that will load the current user's Spotify playlists. The user must be logged in via Spotify or this won't work, and they'll get a 403. 44 | - `/api/tweets` is another test route that will load the current user's recent tweets. The user must be logged in via Twitter for this to work or they'll get a 403. 45 | 46 | `routes/htmlRoutes.js` has a little fancy to it. We define 4 new routes to support Passport authentication to Spotify and Twitter. Since these strategies use OAuth, they require the user to be redirected to a login page on the provider (the provider being Spotify or Twitter, in this example). The provider then asks the user to confirm they want to give access to your app, and if authorized, the provider will redirect _back_ to your app to complete the login process by passing access tokens and profile information to your Strategy. So, each provider needs two routes: 1) `/auth/{provider name}` will initiate the authentication process and 2) `/auth/{provider name}/callback` will complete it. You'll need to register the callback URL in the social media site's developer settings for your app or you'll recieve an error from the site. 47 | 48 | Note that we need to add in a little workaround to get the authentication redirects to work properly when running locally in development mode. This is because the front end is running on a separate development server (port 3000) from the backend (port 3001). This isn't necessary for production, but not putting in a workaround like this will make testing it locally hell. 49 | 50 | `client/src/components/ProtectedRoute.js` is a custom component that can be used like a normal route (see `App.js` line 54), except if the user is NOT logged in, it will redirect the user to /login. If the user IS logged in, it will display the route normally. There is one additional option. If you'd like to change the route that it redirects to, you can specify the correct route with the `redirectTo` route, e.g. `` An added bonus, `ProtectedRoute` injects the current `user` as a prop into the route component so you don't have to do the extra step of wrapping your component in `withUser` (see `MembersOnlyPage.js`). Using this component instead of regular `Route` is useful in the situation where you *don't* want your route to display at all unless they are logged in. 51 | 52 | `client/src/services/withUser.js` is a little bit of React magic. We need a single state to track our current user object. We also need to be able to inject that user as a prop to any component that needs to be aware of the current user. Said components also need to have their user prop updated if the user state changes. Finally, we need a way of updating the state from a component so everyone is notified. There are a few ways to do this, but in this example we're using what's called a High Order Component. It comes with a function called `withUser` that can be called to wrap any component. It will then do exactly what was described above: it will ensure your wrapped component receives a user prop and keep it up-to-date if the user state changes. It also has an `update` function that can be imported and called by any component that needs to change the state (as you can see in `client/src/pages/LoginPage.js`). 53 | 54 | This is a greatly simplified version of what you might see in Flux or Redux. You can use Flux, Redux, etc. if you want a more mature approach that can be used to track other states your app needs if you want, but be aware the learning curve for something like Redux is deceptively high. 55 | 56 | `client/src/pages/TestTwitterPage` and `TestSpotifyPage` simply detect if the user is logged in, and if so, retrieves the respective test data and displays it. 57 | 58 | `client/src/pages/LoginPage` and `CreateAccountPage` have been updated to add social media login links to our Express backend routes to initiate the authentication redirects. Again, we had to add in a workaround to deal with the fact that the backend redirect routes are on a different port than the front end. 59 | 60 | `client/src/components/LoginMenu.js` has been updated to show additional menu items in the dropdown based on whether or not the user has the respective social media membership. 61 | 62 | Finally, the React app is using `material-ui` and `react-flexbox-grid` for UI. You obviously don't need to use these modules, and use your favorite react component library. 63 | 64 | ## Production Notes 65 | 66 | Your strategies (and possibly other parts of your application) receive sensitive but critical confguration values via environmental variables. Locally, we are using `dotenv` to load these keys and values from the `.env` file. This is NOT how you should configure environmental variables in production. Instead, you'll need to refer to your host's instructions on how to best do that. With Heroku, you simply log into the dashboard for your app, click on Settings, find Config Variables, and click Reveal Config Vars. Simply add an entry for each variable named the same as it is in the .env file (or used in your code via `process.env.[key name here]`), and type in the associated value. Heroku keeps these saved securely for your app, and ensures those keys are fed into your app when it runs. 67 | 68 | Client-side validation is important. The forms used to create an account and log in do bare minimum validation and are probably not suitable for a real application. You might want to use a validation module or form module with built-in validation for React. 69 | 70 | If you ARE using sessions, you need to use a production-ready session store. A session store is a bit of code that express-session uses to store session data. Without a store, it can't keep track of sessions and the whole thing won't work. By default, express-session has a built-in store that just keeps sessions in memory. That is what we're using here in this example. However, the authors of express-session strongly warn you against using this default store in production. [There are lots of other stores to choose from](https://github.com/expressjs/session#compatible-session-stores), like storing session data in Redis, Memcache, or even a database. I like using Redis to cache information like sessions, but you need a Redis server that your local and production servers can connect to in order to get this working. -------------------------------------------------------------------------------- /example-social-media-react/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:3001/", 6 | "dependencies": { 7 | "axios": "^0.18.0", 8 | "material-ui": "^0.20.0", 9 | "react": "^16.0.0", 10 | "react-dom": "^16.0.0", 11 | "react-flexbox-grid": "^2.0.0", 12 | "react-router-dom": "^4.2.2", 13 | "react-scripts": "1.0.14" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example-social-media-react/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grbsk/passport-examples/f6e6af6289f3743b2d08e0e7f49194eed0840e55/example-social-media-react/client/public/favicon.ico -------------------------------------------------------------------------------- /example-social-media-react/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /example-social-media-react/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": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/App.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { Component, Fragment } from 'react'; 3 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 5 | 6 | import Navbar from './components/Navbar'; 7 | import ProtectedRoute from './components/ProtectedRoute'; 8 | 9 | import { withUser, update } from './services/withUser'; 10 | 11 | import CreateAccountPage from './pages/CreateAccountPage'; 12 | import HomePage from './pages/HomePage'; 13 | import LoginPage from './pages/LoginPage'; 14 | import NotFoundPage from './pages/NotFoundPage'; 15 | import AuthFailedPage from './pages/AuthFailedPage'; 16 | import TestSpotifyPage from './pages/TestSpotifyPage'; 17 | import TestTwitterPage from './pages/TestTwitterPage'; 18 | import MembersOnlyPage from './pages/MembersOnlyPage'; 19 | 20 | class App extends Component { 21 | componentDidMount() { 22 | // this is going to double check that the user is still actually logged in 23 | // if the app is reloaded. it's possible that we still have a user in sessionStorage 24 | // but the user's session cookie expired. 25 | axios.get('/api/auth') 26 | .then(res => { 27 | // if we get here, the user's session is still good. we'll update the user 28 | // to make sure we're using the most recent values just in case 29 | update(res.data); 30 | }) 31 | .catch(err => { 32 | // if we get a 401 response, that means the user is no longer logged in 33 | if (err.response.status === 401) { 34 | update(null); 35 | } 36 | }); 37 | } 38 | render() { 39 | const { user } = this.props; 40 | return ( 41 | 42 | 43 | 44 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ); 61 | } 62 | } 63 | 64 | export default withUser(App); 65 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/components/LoginButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FlatButton from 'material-ui/FlatButton'; 3 | 4 | const LoginButton = (props) => ( 5 | 6 | ); 7 | 8 | export default LoginButton; 9 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/components/LoginMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import IconButton from 'material-ui/IconButton'; 4 | import IconMenu from 'material-ui/IconMenu'; 5 | import MenuItem from 'material-ui/MenuItem'; 6 | import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'; 7 | 8 | const LoginMenu = (props) => { 9 | const { onLogOut, user: {username, memberships}, ...otherProps } = props; 10 | const handleTestSpotifyClick = () => { 11 | props.history.push('/testspotify'); 12 | } 13 | const handleTestTwitterClick = () => { 14 | props.history.push('/testtwitter'); 15 | } 16 | return ( 17 | 21 | } 22 | targetOrigin={{ horizontal: 'right', vertical: 'top' }} 23 | anchorOrigin={{ horizontal: 'right', vertical: 'top' }} 24 | > 25 | 26 | 27 | { 28 | // show the spotify menu item if their user account is connected to spotify 29 | memberships.includes('spotify') && 30 | 31 | } 32 | { 33 | // show the twitter menu item if their user account is connected to twitter 34 | memberships.includes('twitter') && 35 | 36 | } 37 | 38 | ) 39 | }; 40 | 41 | // because this component is not used in a route, we don't have access to the history prop 42 | // which we need to change the current route. we can get the history prop by wrapping our 43 | // LoginMenu with withRouter, kinda like what withUser does but for routing! 44 | export default withRouter(LoginMenu); 45 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React from 'react'; 3 | import { withRouter } from 'react-router-dom'; 4 | import AppBar from 'material-ui/AppBar'; 5 | 6 | import LoginButton from './LoginButton'; 7 | import LoginMenu from './LoginMenu'; 8 | 9 | import { update } from '../services/withUser'; 10 | 11 | const Navbar = (props) => { 12 | const { user } = props; 13 | const handleLogIn = () => { 14 | props.history.push('/login'); 15 | }; 16 | const handleLogOut = () => { 17 | axios.delete('/api/auth') 18 | .then(() => { 19 | // unsets the currently logged in user. all components wrapped in withUser 20 | // will be updated with a null user and rerender accordingly 21 | update(null); 22 | }) 23 | .catch((err) => { 24 | console.log(err); 25 | }); 26 | } 27 | const handleTitleClick = () => { 28 | props.history.push('/'); 29 | } 30 | return ( 31 | 36 | : } 37 | onTitleClick={handleTitleClick} 38 | /> 39 | ) 40 | }; 41 | 42 | export default withRouter(Navbar); 43 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/components/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import { withUser } from '../services/withUser'; 4 | 5 | const ProtectedRoute = ({ component: Component, user, redirectTo, ...rest }) => ( 6 | ( 7 | user === null 8 | ? 12 | : 13 | )} /> 14 | ); 15 | 16 | export default withUser(ProtectedRoute); 17 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | min-height: 100%; 5 | font-family: 'Roboto', sans-serif 6 | } 7 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import './index.css'; 5 | import registerServiceWorker from "./registerServiceWorker"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/pages/AuthFailedPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class AuthFailedPage extends Component { 4 | render() { 5 | return ( 6 |
7 | Sorry! There was a problem trying to log in. 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default AuthFailedPage; 14 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/pages/CreateAccountPage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { Component } from 'react'; 3 | import { Grid, Row, Col } from 'react-flexbox-grid'; 4 | import TextField from 'material-ui/TextField'; 5 | import RaisedButton from 'material-ui/RaisedButton'; 6 | 7 | let baseUrl = ''; 8 | 9 | // This is a hack to get around the fact that our backend server 10 | // that social media sites need to call back to is on a different 11 | // port than our front end when we're running in development mode 12 | if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { 13 | baseUrl = 'http://localhost:3001'; 14 | } 15 | 16 | class CreateAccountPage extends Component { 17 | state = { 18 | username: null, 19 | password: null, 20 | error: null 21 | } 22 | handleInputChanged = (event) => { 23 | this.setState({ 24 | [event.target.name]: event.target.value 25 | }); 26 | } 27 | handleLogin = (event) => { 28 | event.preventDefault(); 29 | 30 | const { username, password } = this.state; 31 | const { history } = this.props; 32 | 33 | // clear any previous errors so we don't confuse the user 34 | this.setState({ 35 | error: null 36 | }); 37 | 38 | // check to make sure they've entered a username and password. 39 | // this is very poor validation, and there are better ways 40 | // to do this in react, but this will suffice for the example 41 | if (!username || !password) { 42 | this.setState({ 43 | error: 'A username and password is required.' 44 | }); 45 | return; 46 | } 47 | 48 | // post an auth request 49 | axios.post('/api/users', { 50 | username, 51 | password 52 | }) 53 | .then(user => { 54 | // if the response is successful, make them log in 55 | history.push('/login'); 56 | }) 57 | .catch(err => { 58 | 59 | this.setState({ 60 | error: err.response.data.message || err.message 61 | }); 62 | }); 63 | } 64 | render() { 65 | const { error } = this.state; 66 | 67 | return ( 68 | 69 | 70 | 71 |
72 |

Create Account

73 | {error && 74 |
75 | {error} 76 |
77 | } 78 |
79 | 85 |
86 |
87 | 94 |
95 |
96 | 101 |
102 |

Or

103 |
104 | 110 |
111 |
112 | 118 |
119 |
120 | 121 |
122 |
123 | ); 124 | } 125 | } 126 | 127 | export default CreateAccountPage; 128 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/pages/HomePage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { Component, Fragment } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import { List, ListItem } from 'material-ui/List'; 5 | import { withUser } from '../services/withUser'; 6 | 7 | class HomePage extends Component { 8 | state = { 9 | stuff: null 10 | } 11 | componentDidMount() { 12 | // only try loading stuff if the user is logged in. 13 | if (!this.props.user) { 14 | return; 15 | } 16 | 17 | axios.get('/api/stuff') 18 | .then(res => { 19 | this.setState({ 20 | stuff: res.data 21 | }); 22 | }) 23 | .catch(err => { 24 | // if we got an error, we'll just log it and set stuff to an empty array 25 | console.log(err); 26 | this.setState({ 27 | stuff: [] 28 | }); 29 | }); 30 | } 31 | render() { 32 | const { user } = this.props; // get the user prop from props 33 | const { stuff } = this.state; // get stuff from state 34 | 35 | return ( 36 | 37 | {user && stuff && 38 |
39 | Welcome back, {user.username}! 40 | 41 | {stuff.map((s, i) => )} 42 | 43 |
44 | } 45 | {user && !stuff && 46 |
Hold on, looking for your stuff...
47 | } 48 | {!user && 49 |
Hey! I don't recognize you! Register and log in using the link above
50 | } 51 | 52 |

53 | 54 | Click here 55 | to go the members only area. If you are not logged in, 56 | it'll redirect you to the login page. 57 |

58 |
59 | ); 60 | } 61 | } 62 | 63 | // withUser function will wrap the specified component in another component that will 64 | // inject the currently logged in user as a prop called "user" 65 | export default withUser(HomePage); 66 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/pages/LoginPage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { Component } from 'react'; 3 | import { Grid, Row, Col } from 'react-flexbox-grid'; 4 | import { Link } from 'react-router-dom'; 5 | import TextField from 'material-ui/TextField'; 6 | import RaisedButton from 'material-ui/RaisedButton'; 7 | import { update } from '../services/withUser'; 8 | 9 | let baseUrl = ''; 10 | 11 | // This is a hack to get around the fact that our backend server 12 | // that social media sites need to call back to is on a different 13 | // port than our front end when we're running in development mode 14 | if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { 15 | baseUrl = 'http://localhost:3001'; 16 | } 17 | 18 | class LoginPage extends Component { 19 | state = { 20 | username: null, 21 | password: null 22 | } 23 | handleInputChanged = (event) => { 24 | this.setState({ 25 | [event.target.name]: event.target.value 26 | }); 27 | } 28 | handleLogin = (event) => { 29 | event.preventDefault(); 30 | 31 | const { username, password } = this.state; 32 | const { history } = this.props; 33 | 34 | // post an auth request 35 | axios.post('/api/auth', { 36 | username, 37 | password 38 | }) 39 | .then(user => { 40 | // if the response is successful, update the current user and redirect to the home page 41 | update(user.data); 42 | history.push('/'); 43 | }) 44 | .catch(err => { 45 | // an error occured, so let's record the error in our state so we can display it in render 46 | // if the error response status code is 401, it's an invalid username or password. 47 | // if it's any other status code, there's some other unhandled error so we'll just show 48 | // the generic message. 49 | this.setState({ 50 | error: err.response.status === 401 ? 'Invalid username or password.' : err.message 51 | }); 52 | }); 53 | } 54 | render() { 55 | const { error } = this.state; 56 | 57 | return ( 58 | 59 | 60 | 61 |
62 |

Log In

63 | {error && 64 |
65 | {error} 66 |
67 | } 68 |
69 | 75 |
76 |
77 | 84 |
85 |
86 | 91 |
92 |
93 | 99 |
100 |
101 | 107 |
108 |

109 | New here? 110 | Sign up! 111 | 112 |

113 |
114 | 115 |
116 |
117 | ); 118 | } 119 | } 120 | 121 | export default LoginPage; 122 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/pages/MembersOnlyPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | const MembersOnlyPage = (props) => { 5 | // in this case, the user prop is getting injected by 6 | // the ProtectedRoute component, so we don't need to 7 | // directly use withUser 8 | const { user } = props; 9 | 10 | return ( 11 |
12 |

Members Only Area

13 |

14 | Welcome, {user.username}. You can only see this page if you are logged in. 15 | If any unauthenticated plebs try to access this route, they'll get redirected 16 | to the login page. 17 |

18 |
19 | ) 20 | }; 21 | 22 | export default MembersOnlyPage; 23 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/pages/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class NotFoundPage extends Component { 4 | render() { 5 | return ( 6 |
7 | Page not found. 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default NotFoundPage; 14 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/pages/TestSpotifyPage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { Component, Fragment } from 'react'; 3 | import { List, ListItem } from 'material-ui/List'; 4 | import { withUser } from '../services/withUser'; 5 | 6 | class TestSpotifyPage extends Component { 7 | state = { 8 | playlists: null, 9 | playlistError: false 10 | } 11 | 12 | loadPlaylists() { 13 | if (this.state.playlists) { 14 | // if we've already loaded playlists, don't need to reload again 15 | return; 16 | } 17 | 18 | axios.get('/api/playlists') 19 | .then(res => { 20 | this.setState({ 21 | playlists: res.data.items 22 | }); 23 | }) 24 | .catch(err => { 25 | // if we got an error, that means they probably didn't log in with spotify 26 | console.log(err); 27 | this.setState({ 28 | playlists: null, 29 | playlistError: true 30 | }); 31 | }); 32 | } 33 | 34 | componentDidMount() { 35 | this.loadPlaylists(); 36 | } 37 | 38 | renderPlaylists() { 39 | const { user } = this.props; // get the user prop from props 40 | const { playlists, playlistError } = this.state; // get playlists from state 41 | 42 | // they aren't logged in yet! 43 | if (!user) { 44 | return ( 45 |
Hey! You need to log in with Spotify to do this!
46 | ); 47 | } 48 | 49 | // we found their playlists, so display them! 50 | if (!playlistError && playlists) { 51 | return ( 52 |
53 | Welcome back, {user.username}! Here are your private playlists. 54 | {playlists.length > 0 && 55 | 56 | {playlists.map((item) => )} 57 | 58 | } 59 | {playlists.length === 0 && 60 |

You don't have any playlists yet!

61 | } 62 |
63 | ); 64 | } 65 | 66 | // we're still loading the playlists 67 | if (!playlistError) { 68 | return (
Hold on, I'm loading your playlists...
); 69 | } 70 | 71 | // oops! we had a problem trying to load the playlists. they probably 72 | // didn't log in using spotify 73 | return (
Oops! We couldn't load your playlists. Maybe you didn't log in using Spotify?
); 74 | } 75 | 76 | render() { 77 | 78 | 79 | return ( 80 | 81 | {this.renderPlaylists()} 82 | 83 | ); 84 | } 85 | } 86 | 87 | // withUser function will wrap the specified component in another component that will 88 | // inject the currently logged in user as a prop called "user" 89 | export default withUser(TestSpotifyPage); 90 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/pages/TestTwitterPage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import React, { Component, Fragment } from 'react'; 3 | import { List, ListItem } from 'material-ui/List'; 4 | import { withUser } from '../services/withUser'; 5 | 6 | class TestTwitterPage extends Component { 7 | state = { 8 | tweets: null, 9 | tweetsError: false 10 | } 11 | 12 | loadTweets() { 13 | if (this.state.tweets) { 14 | // if we've already loaded tweets, don't need to reload again 15 | return; 16 | } 17 | 18 | axios.get('/api/tweets') 19 | .then(res => { 20 | this.setState({ 21 | tweets: res.data 22 | }); 23 | }) 24 | .catch(err => { 25 | // if we got an error, that means they probably didn't log in with twitter 26 | console.log(err); 27 | this.setState({ 28 | tweets: null, 29 | playlistError: true 30 | }); 31 | }); 32 | } 33 | 34 | componentDidMount() { 35 | this.loadTweets(); 36 | } 37 | 38 | renderTweets() { 39 | const { user } = this.props; // get the user prop from props 40 | const { tweets, tweetsError } = this.state; // get tweets from state 41 | 42 | // they aren't logged in yet! 43 | if (!user) { 44 | return ( 45 |
Hey! You need to log in with Twitter to do this!
46 | ); 47 | } 48 | 49 | // we found their tweets, so display them! 50 | if (!tweetsError && tweets) { 51 | return ( 52 |
53 | Welcome back, {user.username}! Here are your recent tweets. 54 | {tweets.length > 0 && 55 | 56 | {tweets.map((item) => )} 57 | 58 | } 59 | {tweets.length === 0 && 60 |

You don't have any tweets yet!

61 | } 62 |
63 | ); 64 | } 65 | 66 | // we're still loading the tweets 67 | if (!tweetsError) { 68 | return (
Hold on, I'm loading your tweets...
); 69 | } 70 | 71 | // oops! we had a problem trying to load the tweets. they probably 72 | // didn't log in using twitter 73 | return (
Oops! We couldn't load your tweets. Maybe you didn't log in using Twitter?
); 74 | } 75 | 76 | render() { 77 | 78 | 79 | return ( 80 | 81 | {this.renderTweets()} 82 | 83 | ); 84 | } 85 | } 86 | 87 | // withUser function will wrap the specified component in another component that will 88 | // inject the currently logged in user as a prop called "user" 89 | export default withUser(TestTwitterPage); 90 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === "localhost" || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === "[::1]" || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener("load", () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === "installed") { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log("New content is available; please refresh."); 60 | } else { 61 | // At this point, everything has been pre-cached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log("Content is cached for offline use."); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error("Error during service worker registration:", error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get("content-type").indexOf("javascript") === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | "No internet connection found. App is running in offline mode." 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ("serviceWorker" in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /example-social-media-react/client/src/services/withUser.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // checks to see if the session storage already has a user key 4 | // if it does, try to parse it as JSON string into an object, and use 5 | // that as the starting value for state. What this does is effectively 6 | // retain the currently logged in user if the user does a manual refresh 7 | // in their browser. if we don't back the user by something that remains 8 | // between refreshes, the user will "lose" their logged in state. 9 | const stateFromStore = sessionStorage.getItem('user'); 10 | let state = stateFromStore ? JSON.parse(stateFromStore) : null; 11 | 12 | const subscribers = []; 13 | 14 | const unsubscribe = subscriber => { 15 | const index = subscribers.findIndex(subscriber); 16 | index >= 0 && subscribers.splice(index, 1); 17 | }; 18 | const subscribe = subscriber => { 19 | subscribers.push(subscriber); 20 | return () => unsubscribe(subscriber); 21 | }; 22 | 23 | export const withUser = Component => { 24 | return class WithUser extends React.Component { 25 | componentDidMount() { 26 | this.unsubscribe = subscribe(this.forceUpdate.bind(this)); 27 | } 28 | render() { 29 | const newProps = { ...this.props, user: state }; 30 | return ; 31 | } 32 | componentWillUnmount() { 33 | this.unsubscribe(); 34 | } 35 | }; 36 | }; 37 | 38 | export const update = newState => { 39 | state = newState; 40 | // update the "user" key in the session storage with whatever the new state value is 41 | // Remember to stringify the state object because sessionStorage can only store strings 42 | sessionStorage.setItem('user', state ? JSON.stringify(state) : null); 43 | subscribers.forEach(subscriber => subscriber()); 44 | }; 45 | -------------------------------------------------------------------------------- /example-social-media-react/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "client/*" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /example-social-media-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern", 3 | "version": "1.0.0", 4 | "description": "Mern Demo", 5 | "main": "server.js", 6 | "scripts": { 7 | "server": "node -r dotenv/config' server.js", 8 | "client": "cd client && npm run start", 9 | "start": "./node_modules/.bin/concurrently \"./node_modules/.bin/nodemon --exec 'node -r dotenv/config' server.js\" \"npm run client\"", 10 | "build": "cd client && npm run build", 11 | "deploy": "yarn build && git add . && git commit -m \"Building for production\" && git push heroku master", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "concurrently": "^3.5.0", 18 | "nodemon": "^1.11.0" 19 | }, 20 | "dependencies": { 21 | "axios": "^0.18.0", 22 | "body-parser": "^1.18.2", 23 | "cookie-parser": "^1.4.3", 24 | "dotenv": "^5.0.1", 25 | "express": "^4.15.4", 26 | "express-session": "^1.15.6", 27 | "morgan": "^1.9.0", 28 | "passport": "^0.4.0", 29 | "passport-local": "^1.0.0", 30 | "passport-spotify": "^1.0.0", 31 | "passport-twitter": "^1.0.4", 32 | "twitter": "^1.7.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example-social-media-react/passport.js: -------------------------------------------------------------------------------- 1 | const session = require('express-session'); 2 | const cookieparser = require('cookie-parser'); 3 | const passport = require('passport'); 4 | const LocalStrategy = require('passport-local').Strategy; 5 | const SpotifyStrategy = require('passport-spotify').Strategy; 6 | const TwitterStrategy = require('passport-twitter').Strategy; 7 | const db = require('../shared/models'); 8 | 9 | // export a function that receives the Express app we will configure for Passport 10 | module.exports = (app) => { 11 | // these two middlewares are required to make passport work with sessions 12 | // sessions are optional, but an easy solution to keeping users 13 | // logged in until they log out. 14 | app.use(cookieparser()); 15 | app.use(session({ 16 | // this should be changed to something cryptographically secure for production 17 | secret: 'keyboard cat', 18 | resave: false, 19 | saveUninitialized: false, 20 | // automatically extends the session age on each request. useful if you want 21 | // the user's activity to extend their session. If you want an absolute session 22 | // expiration, set to false 23 | rolling: true, 24 | name: 'sid', // don't use the default session cookie name 25 | // set your options for the session cookie 26 | cookie: { 27 | httpOnly: true, 28 | // the duration in milliseconds that the cookie is valid 29 | maxAge: 20 * 60 * 1000, // 20 minutes 30 | // recommended you use this setting in production if you have a well-known domain you want to restrict the cookies to. 31 | // domain: 'your.domain.com', 32 | // recommended you use this setting in production if your site is published using HTTPS 33 | // secure: true, 34 | } 35 | })); 36 | 37 | // Only necessary when using sessions. 38 | // This tells Passport how or what data to save about a user in the session cookie. 39 | // It's recommended you only serialize something like a unique username or user ID. 40 | // I prefer user ID. 41 | passport.serializeUser((user, done) => { 42 | done(null, user._id); 43 | }); 44 | 45 | // Only necessary when using sessions. 46 | // This tells Passport how to turn the user ID we serialize in the session cookie 47 | // back into the actual User record from our Mongo database. 48 | // Here, we simply find the user with the matching ID and return that. 49 | // This will cause the User record to be available on each authenticated request via the req.user property. 50 | passport.deserializeUser(function (userId, done) { 51 | db.User.findById(userId) 52 | .then(function (user) { 53 | done(null, user); 54 | }) 55 | .catch(function (err) { 56 | done(err); 57 | }); 58 | }); 59 | 60 | // this tells passport to use the "local" strategy, and configures the strategy 61 | // with a function that will be called when the user tries to authenticate with 62 | // a username and password. We simply look the user up, hash the password they 63 | // provided with the salt from the real password, and compare the results. if 64 | // the original and current hashes are the same, the user entered the correct password. 65 | passport.use(new LocalStrategy((username, password, done) => { 66 | const errorMsg = 'Invalid username or password'; 67 | 68 | db.User.findOne({ username }) 69 | .then(user => { 70 | // if no matching user was found... 71 | if (!user) { 72 | return done(null, false, { message: errorMsg }); 73 | } 74 | 75 | // call our validate method, which will call done with the user if the 76 | // passwords match, or false if they don't 77 | return user.validatePassword(password) 78 | .then(isMatch => done(null, isMatch ? user : false, isMatch ? null : { message: errorMsg })); 79 | }) 80 | .catch(done); 81 | })); 82 | 83 | // this tells passport that we also support a "spotify" strategy. 84 | // in this case, we tell it how to find a User that has a socialMedia entry 85 | // of type "spotify" that has the same profile ID of the spotify account 86 | // they used to log in. 87 | passport.use(new SpotifyStrategy( 88 | { 89 | // the client ID spotify assigned to your app 90 | clientID: process.env.SPOTIFY_CLIENT_ID, 91 | // the secret spotify assigned to your app 92 | clientSecret: process.env.SPOTIFY_CLIENT_SECRET, 93 | // the path spotify will call back to once the user has logged in. 94 | // We'll define this route in routes/htmlRoutes.js 95 | callbackURL: `${process.env.APP_BASE_URL}/auth/spotify/callback`, 96 | // The minimum scopes you'll need to read the user's 97 | // full name and email address are 'user-read-private', 'user-read-email'. 98 | // If you want to do more than that, 99 | // like actually read the user's Spotify data via their API, 100 | // you need to tell Spotify what data you wish to access via scopes 101 | // See: https://developer.spotify.com/web-api/using-scopes/ 102 | scope: [ 103 | 'user-read-private', 'user-read-email', 104 | 'playlist-read-private', 'playlist-read-collaborative' 105 | ] 106 | }, 107 | // i'm using async/await with promises to keep my code readable 108 | // you can stick with plain old promises if you'd like (whyyyy??) 109 | async (accessToken, refreshToken, expires_in, profile, done) => { 110 | // we'll need these later 111 | let user; 112 | let membership; 113 | 114 | try { 115 | // first, try to find the user that is connected to this spotify user 116 | // i'm using findOneAndUpdate so that if the membership already exists, 117 | // we just update their access token which will periodically expire 118 | membership = await db.SocialMediaMembership.findOneAndUpdate({ 119 | provider: 'spotify', 120 | providerUserId: profile.id 121 | }, { 122 | accessToken, // you'll typically want to encrypt these before storing db 123 | refreshToken // encrypt me, too! 124 | }) 125 | // need the fully populated user for this membership, this is important! 126 | .populate('userId'); 127 | } catch (err) { 128 | // you need to let the strategy know if there was an error! 129 | return done(err, null); 130 | } 131 | 132 | if (!membership) { 133 | try { 134 | // no user with this spotify account is on file, 135 | // so create a new user and membership for this spotify user 136 | user = await db.User.create({ 137 | username: profile.username 138 | }); 139 | membership = await db.SocialMediaMembership.create({ 140 | provider: 'spotify', 141 | providerUserId: profile.id, 142 | accessToken, 143 | refreshToken, 144 | userId: user.id 145 | }); 146 | } catch (err) { 147 | return done(err, null); 148 | } 149 | } else { 150 | // get the user from the membership 151 | user = membership.userId; 152 | } 153 | 154 | // tell the strategy we found the user 155 | done(null, user); 156 | } 157 | )); 158 | 159 | // this tells passport that we also support a "twitter" strategy. 160 | // in this case, we tell it how to find a User that has a socialMedia entry 161 | // of type "twitter" that has the same profile ID of the twitter account 162 | // they used to log in. 163 | passport.use(new TwitterStrategy( 164 | { 165 | // the consumerKey twitter assigned to your app 166 | consumerKey: process.env.TWITTER_CONSUMER_KEY, 167 | // the secret twitter assigned to your app 168 | consumerSecret: process.env.TWITTER_CONSUMER_SECRET, 169 | // the path twitter will call back to once the user has logged in. 170 | // We'll define this route in routes/htmlRoutes.js 171 | callbackURL: `${process.env.APP_BASE_URL}/auth/twitter/callback` 172 | }, 173 | // i'm using async/await with promises to keep my code readable 174 | // you can stick with plain old promises if you'd like (whyyyy??) 175 | async (token, tokenSecret, profile, done) => { 176 | // we'll need these later 177 | let user; 178 | let membership; 179 | 180 | try { 181 | // first, try to find the user that is connected to this twitter user 182 | // i'm using findOneAndUpdate so that if the membership already exists, 183 | // we just update their access token which will periodically expire 184 | membership = await db.SocialMediaMembership.findOneAndUpdate({ 185 | provider: 'twitter', 186 | providerUserId: profile.id 187 | }, { 188 | token, // you'll typically want to encrypt these before storing db 189 | tokenSecret 190 | }) 191 | // need the fully populated user for this membership, this is important! 192 | .populate('userId'); 193 | } catch (err) { 194 | // you need to let the strategy know if there was an error! 195 | return done(err, null); 196 | } 197 | 198 | if (!membership) { 199 | try { 200 | // no user with this twitter account is on file, 201 | // so create a new user and membership for this twitter user 202 | user = await db.User.create({ 203 | username: profile.username 204 | }); 205 | membership = await db.SocialMediaMembership.create({ 206 | provider: 'twitter', 207 | providerUserId: profile.id, 208 | token, 209 | tokenSecret, 210 | userId: user.id 211 | }); 212 | } catch (err) { 213 | return done(err, null); 214 | } 215 | } else { 216 | // get the user from the membership 217 | user = membership.userId; 218 | } 219 | 220 | // tell the strategy we found the user 221 | done(null, user); 222 | } 223 | )); 224 | 225 | // initialize passport. this is required, after you set up passport but BEFORE you use passport.session (if using) 226 | app.use(passport.initialize()); 227 | // only required if using sessions. this will add middleware from passport 228 | // that will serialize/deserialize the user from the session cookie and add 229 | // them to req.user 230 | app.use(passport.session()); 231 | } 232 | -------------------------------------------------------------------------------- /example-social-media-react/routes/apiRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const axios = require('axios'); 3 | const passport = require('passport'); 4 | const router = express.Router(); 5 | const db = require('../../shared/models'); 6 | const mustBeLoggedIn = require('../../shared/middleware/mustBeLoggedIn'); 7 | const mongoose = require('mongoose'); 8 | const Twitter = require('twitter'); 9 | 10 | async function getCurrentUser(req, res) { 11 | // I'm picking only the specific fields its OK for the audience to see publicly 12 | // never send the whole user object in the response, and only show things it's OK 13 | // for others to read (like ID, name, email address, etc.) 14 | const { id, username } = req.user; 15 | 16 | // i'm also gonna return the names of any social media memberships they have 17 | // in case the UI wants to do anything useful with that information 18 | const memberships = await db.SocialMediaMembership 19 | .find({userId: new mongoose.Types.ObjectId(id) }); 20 | 21 | res.json({ 22 | id, username, 23 | memberships: memberships.map(m => m.provider) 24 | }); 25 | } 26 | 27 | router.route('/auth') 28 | // GET to /api/auth will return current logged in user info 29 | .get((req, res) => { 30 | if (!req.user) { 31 | return res.status(401).json({ 32 | message: 'You are not currently logged in.' 33 | }) 34 | } 35 | 36 | getCurrentUser(req, res); 37 | }) 38 | // POST to /api/auth with username and password will authenticate the user 39 | .post(passport.authenticate('local'), (req, res) => { 40 | if (!req.user) { 41 | return res.status(401).json({ 42 | message: 'Invalid username or password.' 43 | }) 44 | } 45 | 46 | getCurrentUser(req, res); 47 | }) 48 | // DELETE to /api/auth will log the user out 49 | .delete((req, res) => { 50 | req.logout(); 51 | req.session.destroy(); 52 | res.json({ 53 | message: 'You have been logged out.' 54 | }); 55 | }); 56 | 57 | router.route('/users') 58 | // POST to /api/users will create a new user 59 | .post((req, res, next) => { 60 | db.User.create(req.body) 61 | .then(user => { 62 | const { id, username } = user; 63 | res.json({ 64 | id, username, memberships: [] 65 | }); 66 | }) 67 | .catch(err => { 68 | // if this error code is thrown, that means the username already exists. 69 | // let's handle that nicely by redirecting them back to the create screen 70 | // with that flash message 71 | if (err.code === 11000) { 72 | res.status(400).json({ 73 | message: 'Username already in use.' 74 | }) 75 | } 76 | 77 | // otherwise, it's some nasty unexpected error, so we'll just send it off to 78 | // to the next middleware to handle the error. 79 | next(err); 80 | }); 81 | }); 82 | 83 | // this route is just returns an array of strings if the user is logged in 84 | // to demonstrate that we can ensure a user must be logged in to use a route 85 | router.route('/stuff') 86 | .get(mustBeLoggedIn(), (req, res) => { 87 | // at this point we can assume the user is logged in. if not, the mustBeLoggedIn middleware would have caught it 88 | res.json([ 89 | 'Bears', 90 | 'Beets', 91 | 'Battlestar Galactica' 92 | ]); 93 | }); 94 | 95 | // example route for accessing a spotify user's playlists. the user must 96 | // have logged in with spotify in order for this to work! 97 | router.route('/playlists') 98 | .get(mustBeLoggedIn(), async (req, res, next) => { 99 | try { 100 | const membership = await db.SocialMediaMembership 101 | .findAccessToken(req.user.id, 'spotify'); 102 | 103 | // send an error if we can't find an access token for spotify 104 | // they probably didn't log in using spotify 105 | if (!membership) { 106 | return res.status(403).json({ 107 | message: 'You must log in using Spotify to access this resource.' 108 | }); 109 | } 110 | 111 | // fetching the user's playlists 112 | const results = await axios.get('https://api.spotify.com/v1/me/playlists', { 113 | headers: { 114 | Authorization: `Bearer ${membership.accessToken}` 115 | } 116 | }); 117 | 118 | res.json(results.data); 119 | } catch (err) { 120 | next(err); 121 | } 122 | }); 123 | 124 | 125 | // example route for accessing a twitter user's recent tweets. the user must 126 | // have logged in with twitter in order for this to work! 127 | router.route('/tweets') 128 | .get(mustBeLoggedIn(), async (req, res, next) => { 129 | try { 130 | const membership = await db.SocialMediaMembership 131 | .findAccessToken(req.user.id, 'twitter'); 132 | 133 | // send an error if we can't find an access token for Twitter 134 | // they probably didn't log in using Twitter 135 | if (!membership) { 136 | return res.status(403).json({ 137 | message: 'You must log in using Twitter to access this resource.' 138 | }); 139 | } 140 | 141 | const twitter = new Twitter({ 142 | consumer_key: process.env.TWITTER_CONSUMER_KEY, 143 | consumer_secret: process.env.TWITTER_CONSUMER_SECRET, 144 | access_token_key: membership.token, 145 | access_token_secret: membership.tokenSecret 146 | }); 147 | 148 | const results = await twitter.get('statuses/user_timeline', { 149 | user_id: membership.providerUserId 150 | }); 151 | 152 | res.json(results); 153 | } catch (err) { 154 | next(err); 155 | } 156 | }); 157 | 158 | 159 | module.exports = router; 160 | 161 | -------------------------------------------------------------------------------- /example-social-media-react/routes/htmlRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require("path"); 3 | const passport = require('passport'); 4 | const router = express.Router(); 5 | 6 | let baseUrl = ''; 7 | 8 | // This is a hack to get around the fact that our backend server 9 | // that social media sites need to call back to is on a different 10 | // port than our front end when we're running in development mode 11 | if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { 12 | baseUrl = 'http://localhost:3000'; 13 | } 14 | 15 | // we need to define the routes necessary to make passport-spotify work 16 | // here's the URL users need to visit to initiate the spotify login 17 | router.get('/auth/spotify', passport.authenticate('spotify'), (req, res) => { 18 | res.end(); 19 | }); 20 | // here's the URL spotify will call back to finish logging them into your site 21 | router.get('/auth/spotify/callback', passport.authenticate('spotify', { 22 | failureRedirect: `${baseUrl}/auth/failed`, // tell it where to go if they couldn't log in 23 | successRedirect: `${baseUrl}/testspotify`, // tell it where to go if the log in successfully 24 | })); 25 | 26 | // we need to define the routes necessary to make passport-twitter work 27 | // here's the URL users need to visit to initiate the twitter login 28 | // the res.end() function is a hack to fix a problem where the express static 29 | // middleware tries to serve the files out of client/build 30 | router.get('/auth/twitter', passport.authenticate('twitter'), (req, res) => { 31 | res.end(); 32 | }); 33 | // here's the URL twitter will call back to finish logging them into your site 34 | router.get('/auth/twitter/callback', passport.authenticate('twitter', { 35 | failureRedirect: `${baseUrl}/auth/failed`, // tell it where to go if they couldn't log in 36 | successRedirect: `${baseUrl}/testtwitter`, // tell it where to go if the log in successfully 37 | })); 38 | 39 | // Serve up static assets (usually on heroku) 40 | router.use(express.static("client/build", { 41 | index: false 42 | })); 43 | 44 | // Send every request to the React app 45 | // Define any API or static HTML routes before this runs! 46 | router.get("*", function (req, res) { 47 | res.sendFile(path.join(__dirname, "../client/build/index.html")); 48 | }); 49 | 50 | 51 | module.exports = router; 52 | 53 | -------------------------------------------------------------------------------- /example-social-media-react/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const router = express.Router(); 4 | // add API routes to current router 5 | // NOTE: API routes must be added first, because htmlRoutes has a wildcard route 6 | // which will swallow anything that isn't matched first 7 | // NOTE: All routes exported from apiRoutes will get placed under the /api path 8 | // this is just to save a little typing so in my api routes I don't have to put 9 | // /api in front of each route. 10 | router.use('/api', require('./apiRoutes')); 11 | 12 | // add HTML routes to current router 13 | router.use(require('./htmlRoutes')); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /example-social-media-react/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const bodyparser = require('body-parser'); 3 | const morgan = require('morgan'); 4 | 5 | const PORT = process.env.PORT || 3001; 6 | const app = express(); 7 | 8 | // let's set up some basic middleware for our express app 9 | // logs requests to the console. not necessary to make passport work, but useful 10 | app.use(morgan('dev')); 11 | // Use body-parser for reading form submissions into objects 12 | app.use(bodyparser.urlencoded({ extended: true })); 13 | // Use body-parser for reading application/json into objects 14 | app.use(bodyparser.json()); 15 | 16 | // Serve up static assets (usually on heroku) 17 | if (process.env.NODE_ENV === "production") { 18 | app.use(express.static("client/build")); 19 | } 20 | 21 | // configure using our exported passport function. 22 | // we need to pass the express app we want configured! 23 | // order is important! you need to set up passport 24 | // before you start using it in your routes. 25 | require('./passport')(app); 26 | 27 | // use the routes we configured. 28 | app.use(require('./routes')); 29 | 30 | // Here's a little custom error handling middleware 31 | // that logs the error to console, then renders an 32 | // error page describing the error. 33 | app.use((error, req, res, next) => { 34 | console.error(error); 35 | res.status(500).json({ 36 | error 37 | }) 38 | }); 39 | 40 | // configure mongoose 41 | require('../shared/middleware/mongoose')() 42 | .then(() => { 43 | // mongo is connected, so now we can start the express server. 44 | app.listen(PORT, () => console.log(`Server up and running on ${PORT}.`)); 45 | }) 46 | .catch(err => { 47 | // an error occurred connecting to mongo! 48 | // log the error and exit 49 | console.error('Unable to connect to mongo.') 50 | console.error(err); 51 | }); 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passport-examples", 3 | "version": "1.0.0", 4 | "description": "A collection of PassportJS configuration examples", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Mike Grabski (http://mikegrabski.com/)", 10 | "license": "MIT", 11 | "dependencies": { 12 | "bcrypt": "^1.0.3", 13 | "mongoose": "^5.0.11" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /shared/middleware/mongoose.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | module.exports = function() { 4 | // If deployed, use the deployed database. Otherwise use the local mongoHeadlines database 5 | var MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost/passport-examples"; 6 | 7 | // Configure mongoose to use Promises, because callbacks are passe. 8 | mongoose.Promise = global.Promise; 9 | // Connect to the Mongo DB 10 | return mongoose.connect(MONGODB_URI); 11 | } 12 | -------------------------------------------------------------------------------- /shared/middleware/mustBeLoggedIn.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return (req, res, next) => { 3 | // if the user is authenticated, we're all good here. let the next middleware handle it. 4 | if (req.isAuthenticated()) { 5 | return next(); 6 | } 7 | 8 | // if we've gotten to this point, that means the user is NOT authenticated but should be 9 | // so let's respond with an appropriate 403 Forbidden reponse 10 | 11 | // if the client says they want JSON, this is probably an AJAX call so let's respond with a JSON error 12 | if (req.accepts('json')) { 13 | res.status(403).json({ 14 | message: 'You must be logged in to perform this action.' 15 | }); 16 | } else { 17 | // otherwise, try and render a view named "forbidden" 18 | res.status(403).render('forbidden'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /shared/models/SocialMediaMembership.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | const ObjectId = Schema.Types.ObjectId; 4 | 5 | const SocialMediaMembershipSchema = new Schema({ 6 | provider: { 7 | type: String, 8 | required: true 9 | }, 10 | providerUserId: { 11 | type: String, 12 | required: true 13 | }, 14 | // used in this example by Spotify to store the access and refresh tokens they give you 15 | accessToken: String, 16 | refreshToken: String, 17 | // used in this example by Twitter, who doesn't give you a direct access token :( 18 | token: String, 19 | tokenSecret: String, 20 | userId: { 21 | type: ObjectId, 22 | ref: 'User', 23 | required: true 24 | }, 25 | dateAdded: { 26 | type: Date, 27 | default: Date.now, 28 | required: true 29 | } 30 | }); 31 | 32 | // here's a static method to find the membership given user and social media provider 33 | // this is only necessary if you want to actually use the membership info to access the social 34 | // media site's APIs on behalf of the user. 35 | SocialMediaMembershipSchema.statics.findAccessToken = async function(userId, provider) { 36 | const membership = await this.findOne({ 37 | userId: new mongoose.Types.ObjectId(userId), 38 | provider 39 | }); 40 | 41 | return membership; 42 | }; 43 | 44 | const SocialMediaMembership = mongoose.model('SocialMediaMembership', SocialMediaMembershipSchema); 45 | 46 | module.exports = SocialMediaMembership; 47 | -------------------------------------------------------------------------------- /shared/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | // bcrypt is the hashing algorithm we'll use to protect stored credentials. 5 | // NEVER STORE PASSWORDS OR OTHER SENSITIVE DATA AS PLAIN TEXT!!! 6 | const bcrypt = require('bcrypt'); 7 | 8 | // TL;DNR: this determines how expensive it is to generate the hash 9 | // as average computing power grows, you'll increase this number 10 | // to make it prohibitively expensive for someone who has stolen 11 | // your hashed passwords to crack those hashed passwords. Don't make 12 | // this value so high it takes forever to log in or sign up, but don't 13 | // a potential hacker's job easy. 14 | // See: https://codahale.com/how-to-safely-store-a-password/ 15 | const WORK_FACTOR = 10; 16 | 17 | const UserSchema = new Schema({ 18 | username: { 19 | type: String, 20 | required: true, 21 | index: { unique: true } 22 | }, 23 | password: { 24 | type: String 25 | } 26 | }); 27 | 28 | // This pre "save" handler will be called before each time the user is saved. 29 | // it will convert the plaintext password into a securely hashed version so that 30 | // the original plaintext password is never stored in the database 31 | 32 | // NOTE: do NOT use an arrow function for the second argument 33 | // Mongoose passes in the instance being saved via "this", 34 | // but arrow functions preserve "this" as the bound context 35 | // if you use an arrow function, you'll get an error 36 | // "user.isModified is not a function" 37 | UserSchema.pre('save', function(next) { 38 | const user = this; 39 | 40 | // only hash the password if it has been modified (or is new) 41 | if (!user.isModified('password')) { 42 | return next(); 43 | } 44 | 45 | // generate a salt 46 | bcrypt.genSalt(WORK_FACTOR, function (err, salt) { 47 | if (err) return next(err); 48 | 49 | // hash the password along with our new salt 50 | bcrypt.hash(user.password, salt, function (err, hash) { 51 | if (err) return next(err); 52 | 53 | // override the cleartext password with the hashed one 54 | user.password = hash; 55 | // let mongoose know we're done now that we've hashed the plaintext password 56 | next(); 57 | }); 58 | }); 59 | }); 60 | 61 | // Here, we define a method that will be available on each instance of the User. 62 | // This method will validate a given password with the actual password, and resolve 63 | // true if the password is a match, or false if it is not. 64 | // This code returns a Promise rather than using the callback style 65 | UserSchema.methods.validatePassword = function (candidatePassword) { 66 | return new Promise((resolve, reject) => { 67 | bcrypt.compare(candidatePassword, this.password, function (err, isMatch) { 68 | if (err) return reject(err); 69 | resolve(isMatch); 70 | }); 71 | }); 72 | }; 73 | 74 | const User = mongoose.model('User', UserSchema); 75 | 76 | module.exports = User; 77 | -------------------------------------------------------------------------------- /shared/models/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | User: require('./User'), 3 | SocialMediaMembership: require('./SocialMediaMembership') 4 | }; 5 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | abbrev@1: 6 | version "1.1.1" 7 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" 8 | 9 | ajv@^5.1.0: 10 | version "5.5.2" 11 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" 12 | dependencies: 13 | co "^4.6.0" 14 | fast-deep-equal "^1.0.0" 15 | fast-json-stable-stringify "^2.0.0" 16 | json-schema-traverse "^0.3.0" 17 | 18 | ansi-regex@^2.0.0: 19 | version "2.1.1" 20 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 21 | 22 | aproba@^1.0.3: 23 | version "1.2.0" 24 | resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" 25 | 26 | are-we-there-yet@~1.1.2: 27 | version "1.1.4" 28 | resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" 29 | dependencies: 30 | delegates "^1.0.0" 31 | readable-stream "^2.0.6" 32 | 33 | asn1@~0.2.3: 34 | version "0.2.3" 35 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" 36 | 37 | assert-plus@1.0.0, assert-plus@^1.0.0: 38 | version "1.0.0" 39 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 40 | 41 | async@2.1.4: 42 | version "2.1.4" 43 | resolved "https://registry.yarnpkg.com/async/-/async-2.1.4.tgz#2d2160c7788032e4dd6cbe2502f1f9a2c8f6cde4" 44 | dependencies: 45 | lodash "^4.14.0" 46 | 47 | asynckit@^0.4.0: 48 | version "0.4.0" 49 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 50 | 51 | aws-sign2@~0.7.0: 52 | version "0.7.0" 53 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" 54 | 55 | aws4@^1.6.0: 56 | version "1.6.0" 57 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" 58 | 59 | balanced-match@^1.0.0: 60 | version "1.0.0" 61 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 62 | 63 | bcrypt-pbkdf@^1.0.0: 64 | version "1.0.1" 65 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" 66 | dependencies: 67 | tweetnacl "^0.14.3" 68 | 69 | bcrypt@^1.0.3: 70 | version "1.0.3" 71 | resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-1.0.3.tgz#b02ddc6c0b52ea16b8d3cf375d5a32e780dab548" 72 | dependencies: 73 | nan "2.6.2" 74 | node-pre-gyp "0.6.36" 75 | 76 | block-stream@*: 77 | version "0.0.9" 78 | resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" 79 | dependencies: 80 | inherits "~2.0.0" 81 | 82 | bluebird@3.5.0: 83 | version "3.5.0" 84 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" 85 | 86 | boom@4.x.x: 87 | version "4.3.1" 88 | resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" 89 | dependencies: 90 | hoek "4.x.x" 91 | 92 | boom@5.x.x: 93 | version "5.2.0" 94 | resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" 95 | dependencies: 96 | hoek "4.x.x" 97 | 98 | brace-expansion@^1.1.7: 99 | version "1.1.11" 100 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 101 | dependencies: 102 | balanced-match "^1.0.0" 103 | concat-map "0.0.1" 104 | 105 | bson@~1.0.4: 106 | version "1.0.6" 107 | resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.6.tgz#444db59ddd4c24f0cb063aabdc5c8c7b0ceca912" 108 | 109 | caseless@~0.12.0: 110 | version "0.12.0" 111 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 112 | 113 | co@^4.6.0: 114 | version "4.6.0" 115 | resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 116 | 117 | code-point-at@^1.0.0: 118 | version "1.1.0" 119 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 120 | 121 | combined-stream@1.0.6, combined-stream@~1.0.5: 122 | version "1.0.6" 123 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" 124 | dependencies: 125 | delayed-stream "~1.0.0" 126 | 127 | concat-map@0.0.1: 128 | version "0.0.1" 129 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 130 | 131 | console-control-strings@^1.0.0, console-control-strings@~1.1.0: 132 | version "1.1.0" 133 | resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" 134 | 135 | core-util-is@1.0.2, core-util-is@~1.0.0: 136 | version "1.0.2" 137 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 138 | 139 | cryptiles@3.x.x: 140 | version "3.1.2" 141 | resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" 142 | dependencies: 143 | boom "5.x.x" 144 | 145 | dashdash@^1.12.0: 146 | version "1.14.1" 147 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 148 | dependencies: 149 | assert-plus "^1.0.0" 150 | 151 | debug@2.6.9, debug@^2.2.0: 152 | version "2.6.9" 153 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 154 | dependencies: 155 | ms "2.0.0" 156 | 157 | deep-extend@~0.4.0: 158 | version "0.4.2" 159 | resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" 160 | 161 | delayed-stream@~1.0.0: 162 | version "1.0.0" 163 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 164 | 165 | delegates@^1.0.0: 166 | version "1.0.0" 167 | resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" 168 | 169 | ecc-jsbn@~0.1.1: 170 | version "0.1.1" 171 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" 172 | dependencies: 173 | jsbn "~0.1.0" 174 | 175 | extend@~3.0.1: 176 | version "3.0.1" 177 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" 178 | 179 | extsprintf@1.3.0: 180 | version "1.3.0" 181 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" 182 | 183 | extsprintf@^1.2.0: 184 | version "1.4.0" 185 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" 186 | 187 | fast-deep-equal@^1.0.0: 188 | version "1.1.0" 189 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" 190 | 191 | fast-json-stable-stringify@^2.0.0: 192 | version "2.0.0" 193 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" 194 | 195 | forever-agent@~0.6.1: 196 | version "0.6.1" 197 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 198 | 199 | form-data@~2.3.1: 200 | version "2.3.2" 201 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" 202 | dependencies: 203 | asynckit "^0.4.0" 204 | combined-stream "1.0.6" 205 | mime-types "^2.1.12" 206 | 207 | fs.realpath@^1.0.0: 208 | version "1.0.0" 209 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 210 | 211 | fstream-ignore@^1.0.5: 212 | version "1.0.5" 213 | resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" 214 | dependencies: 215 | fstream "^1.0.0" 216 | inherits "2" 217 | minimatch "^3.0.0" 218 | 219 | fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: 220 | version "1.0.11" 221 | resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" 222 | dependencies: 223 | graceful-fs "^4.1.2" 224 | inherits "~2.0.0" 225 | mkdirp ">=0.5 0" 226 | rimraf "2" 227 | 228 | gauge@~2.7.3: 229 | version "2.7.4" 230 | resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" 231 | dependencies: 232 | aproba "^1.0.3" 233 | console-control-strings "^1.0.0" 234 | has-unicode "^2.0.0" 235 | object-assign "^4.1.0" 236 | signal-exit "^3.0.0" 237 | string-width "^1.0.1" 238 | strip-ansi "^3.0.1" 239 | wide-align "^1.1.0" 240 | 241 | getpass@^0.1.1: 242 | version "0.1.7" 243 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" 244 | dependencies: 245 | assert-plus "^1.0.0" 246 | 247 | glob@^7.0.5: 248 | version "7.1.2" 249 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 250 | dependencies: 251 | fs.realpath "^1.0.0" 252 | inflight "^1.0.4" 253 | inherits "2" 254 | minimatch "^3.0.4" 255 | once "^1.3.0" 256 | path-is-absolute "^1.0.0" 257 | 258 | graceful-fs@^4.1.2: 259 | version "4.1.11" 260 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 261 | 262 | har-schema@^2.0.0: 263 | version "2.0.0" 264 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" 265 | 266 | har-validator@~5.0.3: 267 | version "5.0.3" 268 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" 269 | dependencies: 270 | ajv "^5.1.0" 271 | har-schema "^2.0.0" 272 | 273 | has-unicode@^2.0.0: 274 | version "2.0.1" 275 | resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" 276 | 277 | hawk@~6.0.2: 278 | version "6.0.2" 279 | resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" 280 | dependencies: 281 | boom "4.x.x" 282 | cryptiles "3.x.x" 283 | hoek "4.x.x" 284 | sntp "2.x.x" 285 | 286 | hoek@4.x.x: 287 | version "4.2.1" 288 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" 289 | 290 | http-signature@~1.2.0: 291 | version "1.2.0" 292 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 293 | dependencies: 294 | assert-plus "^1.0.0" 295 | jsprim "^1.2.2" 296 | sshpk "^1.7.0" 297 | 298 | inflight@^1.0.4: 299 | version "1.0.6" 300 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 301 | dependencies: 302 | once "^1.3.0" 303 | wrappy "1" 304 | 305 | inherits@2, inherits@~2.0.0, inherits@~2.0.3: 306 | version "2.0.3" 307 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 308 | 309 | ini@~1.3.0: 310 | version "1.3.5" 311 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" 312 | 313 | is-fullwidth-code-point@^1.0.0: 314 | version "1.0.0" 315 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" 316 | dependencies: 317 | number-is-nan "^1.0.0" 318 | 319 | is-typedarray@~1.0.0: 320 | version "1.0.0" 321 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 322 | 323 | isarray@~1.0.0: 324 | version "1.0.0" 325 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 326 | 327 | isstream@~0.1.2: 328 | version "0.1.2" 329 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 330 | 331 | jsbn@~0.1.0: 332 | version "0.1.1" 333 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 334 | 335 | json-schema-traverse@^0.3.0: 336 | version "0.3.1" 337 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" 338 | 339 | json-schema@0.2.3: 340 | version "0.2.3" 341 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 342 | 343 | json-stringify-safe@~5.0.1: 344 | version "5.0.1" 345 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 346 | 347 | jsprim@^1.2.2: 348 | version "1.4.1" 349 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" 350 | dependencies: 351 | assert-plus "1.0.0" 352 | extsprintf "1.3.0" 353 | json-schema "0.2.3" 354 | verror "1.10.0" 355 | 356 | kareem@2.0.5: 357 | version "2.0.5" 358 | resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.0.5.tgz#437e1e40f1be304ee21b3e4790eb3a05418b35ca" 359 | 360 | lodash.get@4.4.2: 361 | version "4.4.2" 362 | resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" 363 | 364 | lodash@^4.14.0: 365 | version "4.17.5" 366 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" 367 | 368 | mime-db@~1.33.0: 369 | version "1.33.0" 370 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" 371 | 372 | mime-types@^2.1.12, mime-types@~2.1.17: 373 | version "2.1.18" 374 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" 375 | dependencies: 376 | mime-db "~1.33.0" 377 | 378 | minimatch@^3.0.0, minimatch@^3.0.4: 379 | version "3.0.4" 380 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 381 | dependencies: 382 | brace-expansion "^1.1.7" 383 | 384 | minimist@0.0.8: 385 | version "0.0.8" 386 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 387 | 388 | minimist@^1.2.0: 389 | version "1.2.0" 390 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 391 | 392 | "mkdirp@>=0.5 0", mkdirp@^0.5.1: 393 | version "0.5.1" 394 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 395 | dependencies: 396 | minimist "0.0.8" 397 | 398 | mongodb-core@3.0.4: 399 | version "3.0.4" 400 | resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-3.0.4.tgz#a3fdf466e697a2f1df87e458e5e2df1c26cc654b" 401 | dependencies: 402 | bson "~1.0.4" 403 | require_optional "^1.0.1" 404 | 405 | mongodb@3.0.4: 406 | version "3.0.4" 407 | resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.0.4.tgz#ee0c0f7bc565edc5f40ee2d23170e522a8ad2286" 408 | dependencies: 409 | mongodb-core "3.0.4" 410 | 411 | mongoose-legacy-pluralize@1.0.2: 412 | version "1.0.2" 413 | resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" 414 | 415 | mongoose@^5.0.11: 416 | version "5.0.11" 417 | resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.0.11.tgz#bb31a98591cd2377312ded28155281110a9e1da0" 418 | dependencies: 419 | async "2.1.4" 420 | bson "~1.0.4" 421 | kareem "2.0.5" 422 | lodash.get "4.4.2" 423 | mongodb "3.0.4" 424 | mongoose-legacy-pluralize "1.0.2" 425 | mpath "0.3.0" 426 | mquery "3.0.0" 427 | ms "2.0.0" 428 | regexp-clone "0.0.1" 429 | sliced "1.0.1" 430 | 431 | mpath@0.3.0: 432 | version "0.3.0" 433 | resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.3.0.tgz#7a58f789e9b5fd3c94520634157960f26bd5ef44" 434 | 435 | mquery@3.0.0: 436 | version "3.0.0" 437 | resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.0.0.tgz#e5f387dbabc0b9b69859e550e810faabe0ceabb0" 438 | dependencies: 439 | bluebird "3.5.0" 440 | debug "2.6.9" 441 | regexp-clone "0.0.1" 442 | sliced "0.0.5" 443 | 444 | ms@2.0.0: 445 | version "2.0.0" 446 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 447 | 448 | nan@2.6.2: 449 | version "2.6.2" 450 | resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" 451 | 452 | node-pre-gyp@0.6.36: 453 | version "0.6.36" 454 | resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" 455 | dependencies: 456 | mkdirp "^0.5.1" 457 | nopt "^4.0.1" 458 | npmlog "^4.0.2" 459 | rc "^1.1.7" 460 | request "^2.81.0" 461 | rimraf "^2.6.1" 462 | semver "^5.3.0" 463 | tar "^2.2.1" 464 | tar-pack "^3.4.0" 465 | 466 | nopt@^4.0.1: 467 | version "4.0.1" 468 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" 469 | dependencies: 470 | abbrev "1" 471 | osenv "^0.1.4" 472 | 473 | npmlog@^4.0.2: 474 | version "4.1.2" 475 | resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" 476 | dependencies: 477 | are-we-there-yet "~1.1.2" 478 | console-control-strings "~1.1.0" 479 | gauge "~2.7.3" 480 | set-blocking "~2.0.0" 481 | 482 | number-is-nan@^1.0.0: 483 | version "1.0.1" 484 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 485 | 486 | oauth-sign@~0.8.2: 487 | version "0.8.2" 488 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" 489 | 490 | object-assign@^4.1.0: 491 | version "4.1.1" 492 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 493 | 494 | once@^1.3.0, once@^1.3.3: 495 | version "1.4.0" 496 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 497 | dependencies: 498 | wrappy "1" 499 | 500 | os-homedir@^1.0.0: 501 | version "1.0.2" 502 | resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" 503 | 504 | os-tmpdir@^1.0.0: 505 | version "1.0.2" 506 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 507 | 508 | osenv@^0.1.4: 509 | version "0.1.5" 510 | resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" 511 | dependencies: 512 | os-homedir "^1.0.0" 513 | os-tmpdir "^1.0.0" 514 | 515 | path-is-absolute@^1.0.0: 516 | version "1.0.1" 517 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 518 | 519 | performance-now@^2.1.0: 520 | version "2.1.0" 521 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" 522 | 523 | process-nextick-args@~2.0.0: 524 | version "2.0.0" 525 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" 526 | 527 | punycode@^1.4.1: 528 | version "1.4.1" 529 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 530 | 531 | qs@~6.5.1: 532 | version "6.5.1" 533 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 534 | 535 | rc@^1.1.7: 536 | version "1.2.6" 537 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.6.tgz#eb18989c6d4f4f162c399f79ddd29f3835568092" 538 | dependencies: 539 | deep-extend "~0.4.0" 540 | ini "~1.3.0" 541 | minimist "^1.2.0" 542 | strip-json-comments "~2.0.1" 543 | 544 | readable-stream@^2.0.6, readable-stream@^2.1.4: 545 | version "2.3.5" 546 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" 547 | dependencies: 548 | core-util-is "~1.0.0" 549 | inherits "~2.0.3" 550 | isarray "~1.0.0" 551 | process-nextick-args "~2.0.0" 552 | safe-buffer "~5.1.1" 553 | string_decoder "~1.0.3" 554 | util-deprecate "~1.0.1" 555 | 556 | regexp-clone@0.0.1: 557 | version "0.0.1" 558 | resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-0.0.1.tgz#a7c2e09891fdbf38fbb10d376fb73003e68ac589" 559 | 560 | request@^2.81.0: 561 | version "2.85.0" 562 | resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" 563 | dependencies: 564 | aws-sign2 "~0.7.0" 565 | aws4 "^1.6.0" 566 | caseless "~0.12.0" 567 | combined-stream "~1.0.5" 568 | extend "~3.0.1" 569 | forever-agent "~0.6.1" 570 | form-data "~2.3.1" 571 | har-validator "~5.0.3" 572 | hawk "~6.0.2" 573 | http-signature "~1.2.0" 574 | is-typedarray "~1.0.0" 575 | isstream "~0.1.2" 576 | json-stringify-safe "~5.0.1" 577 | mime-types "~2.1.17" 578 | oauth-sign "~0.8.2" 579 | performance-now "^2.1.0" 580 | qs "~6.5.1" 581 | safe-buffer "^5.1.1" 582 | stringstream "~0.0.5" 583 | tough-cookie "~2.3.3" 584 | tunnel-agent "^0.6.0" 585 | uuid "^3.1.0" 586 | 587 | require_optional@^1.0.1: 588 | version "1.0.1" 589 | resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" 590 | dependencies: 591 | resolve-from "^2.0.0" 592 | semver "^5.1.0" 593 | 594 | resolve-from@^2.0.0: 595 | version "2.0.0" 596 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" 597 | 598 | rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1: 599 | version "2.6.2" 600 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" 601 | dependencies: 602 | glob "^7.0.5" 603 | 604 | safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 605 | version "5.1.1" 606 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 607 | 608 | semver@^5.1.0, semver@^5.3.0: 609 | version "5.5.0" 610 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" 611 | 612 | set-blocking@~2.0.0: 613 | version "2.0.0" 614 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 615 | 616 | signal-exit@^3.0.0: 617 | version "3.0.2" 618 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 619 | 620 | sliced@0.0.5: 621 | version "0.0.5" 622 | resolved "https://registry.yarnpkg.com/sliced/-/sliced-0.0.5.tgz#5edc044ca4eb6f7816d50ba2fc63e25d8fe4707f" 623 | 624 | sliced@1.0.1: 625 | version "1.0.1" 626 | resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" 627 | 628 | sntp@2.x.x: 629 | version "2.1.0" 630 | resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" 631 | dependencies: 632 | hoek "4.x.x" 633 | 634 | sshpk@^1.7.0: 635 | version "1.14.1" 636 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb" 637 | dependencies: 638 | asn1 "~0.2.3" 639 | assert-plus "^1.0.0" 640 | dashdash "^1.12.0" 641 | getpass "^0.1.1" 642 | optionalDependencies: 643 | bcrypt-pbkdf "^1.0.0" 644 | ecc-jsbn "~0.1.1" 645 | jsbn "~0.1.0" 646 | tweetnacl "~0.14.0" 647 | 648 | string-width@^1.0.1, string-width@^1.0.2: 649 | version "1.0.2" 650 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" 651 | dependencies: 652 | code-point-at "^1.0.0" 653 | is-fullwidth-code-point "^1.0.0" 654 | strip-ansi "^3.0.0" 655 | 656 | string_decoder@~1.0.3: 657 | version "1.0.3" 658 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" 659 | dependencies: 660 | safe-buffer "~5.1.0" 661 | 662 | stringstream@~0.0.5: 663 | version "0.0.5" 664 | resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" 665 | 666 | strip-ansi@^3.0.0, strip-ansi@^3.0.1: 667 | version "3.0.1" 668 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 669 | dependencies: 670 | ansi-regex "^2.0.0" 671 | 672 | strip-json-comments@~2.0.1: 673 | version "2.0.1" 674 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 675 | 676 | tar-pack@^3.4.0: 677 | version "3.4.1" 678 | resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" 679 | dependencies: 680 | debug "^2.2.0" 681 | fstream "^1.0.10" 682 | fstream-ignore "^1.0.5" 683 | once "^1.3.3" 684 | readable-stream "^2.1.4" 685 | rimraf "^2.5.1" 686 | tar "^2.2.1" 687 | uid-number "^0.0.6" 688 | 689 | tar@^2.2.1: 690 | version "2.2.1" 691 | resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" 692 | dependencies: 693 | block-stream "*" 694 | fstream "^1.0.2" 695 | inherits "2" 696 | 697 | tough-cookie@~2.3.3: 698 | version "2.3.4" 699 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" 700 | dependencies: 701 | punycode "^1.4.1" 702 | 703 | tunnel-agent@^0.6.0: 704 | version "0.6.0" 705 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 706 | dependencies: 707 | safe-buffer "^5.0.1" 708 | 709 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 710 | version "0.14.5" 711 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 712 | 713 | uid-number@^0.0.6: 714 | version "0.0.6" 715 | resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" 716 | 717 | util-deprecate@~1.0.1: 718 | version "1.0.2" 719 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 720 | 721 | uuid@^3.1.0: 722 | version "3.2.1" 723 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" 724 | 725 | verror@1.10.0: 726 | version "1.10.0" 727 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 728 | dependencies: 729 | assert-plus "^1.0.0" 730 | core-util-is "1.0.2" 731 | extsprintf "^1.2.0" 732 | 733 | wide-align@^1.1.0: 734 | version "1.1.2" 735 | resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" 736 | dependencies: 737 | string-width "^1.0.2" 738 | 739 | wrappy@1: 740 | version "1.0.2" 741 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 742 | --------------------------------------------------------------------------------