├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |