├── .env.template
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── components
├── footer
│ └── index.js
└── header
│ └── index.js
├── lib
├── config
│ ├── index.js
│ ├── paths.js
│ └── resolvers.js
├── custom-hooks
│ └── index.js
├── fetch
│ └── index.js
├── middleware
│ ├── authenticate.js
│ ├── index.js
│ ├── mongodb.js
│ ├── passport
│ │ ├── github.js
│ │ └── local.js
│ └── session.js
├── mongodb-models
│ └── users.js
├── state
│ └── index.js
└── styles
│ ├── common
│ └── reset.css
│ ├── global.scss
│ ├── layout.scss
│ └── variables.scss
├── next.config.js
├── package.json
├── pages
├── _app.js
├── api
│ ├── auth
│ │ ├── [provider].js
│ │ └── [provider]
│ │ │ └── callback.js
│ ├── logout.js
│ ├── protected-route.js
│ ├── register.js
│ └── user.js
├── index.js
├── login.js
├── private.js
└── protected-example
│ └── index.js
└── yarn.lock
/.env.template:
--------------------------------------------------------------------------------
1 | GITHUB_CLIENT_ID=
2 | GITHUB_CLIENT_SECRET=
3 | GITHUB_CALLBACK_URL=
4 |
5 | MONGODB_CONNECTION=
6 |
7 | COOKIE_NAME=cookie_name_here
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .next
2 | .vscode
3 | .env
4 | node_modules
5 | .idea
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 140,
3 | "singleQuote": true,
4 | "tabWidth": 4,
5 | "trailingComma": "none",
6 | "semi": true,
7 | "arrowParens": "always",
8 | "useTabs": true
9 | }
10 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at contribute@davidwieler.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | 1. Make a PR
2 | 2. Submit said PR
3 | 3. Win
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 David Wieler
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 | # NextJS Session Based Authentication with MongoDB and PassportJS
2 |
3 | This repo is an example on how you can get PassportJS working with NextJS without the need for a custom server, and using only API routes.
4 |
5 | It uses:
6 |
7 | - MongoDB
8 | - MongooseJS for Mongo connections
9 | - PassportJS
10 | - Github Auth setup (passport-github2)
11 | - Email/Username Auth setup. Passwords use the `bcryptjs` library (passport-local)
12 | - Uses Cookies. (I might add some extra functionality later to support JWT)
13 | - Logout and Register routes already set up
14 | - Next Connect for easier middleware
15 | - `next-css`and `next-sass` has been set up in the config file.
16 | - `dotenv` package for using .env files
17 | - I've added some helper aliases to `next.config.js` so we don't have to use relative file paths on imports. (see examples for an explanation)
18 |
19 | ## How to use
20 |
21 | - I'm using yarn, but npm will work too
22 |
23 | 1. Clone this repo
24 | 2. Run `yarn` or `npm install`
25 | 3. Follow the setup steps below
26 | 4. Run `yarn dev` or `npm run dev` to fire up the dev server.
27 | - If all is working, you should see a `Connected to Mongo DB Server` in the console after running `yarn dev`
28 | 5. Hack away.
29 |
30 | ## Setup
31 |
32 | There is a little bit of setup to get this example repo going.
33 |
34 | ### Step 1 - Github OAuth App creation
35 |
36 | You'll need to set up a Github OAuth application for these two pieces of information:
37 |
38 | - client_id
39 | - client_secret
40 |
41 | _! HOLD ON TO THESE UNTIL STEP 3 !_
42 |
43 | The callback URL to give Github: `http://localhost:3000/api/auth/github/callback`
44 |
45 | The callback URL is just another API route. The callback URL's have been set up in:
46 | `pages/api/auth/[provider]/[provider].js`. This should make adding additional PassportJS strategies easier.
47 |
48 | For example, for twitter you could use:
49 |
50 | `http://localhost:3000/api/auth/twitter/callback`
51 |
52 | or
53 |
54 | `http://localhost:3000/api/auth/twitter/authenticate`
55 |
56 | Both would work without needing to change the API route structure.
57 |
58 | ### Step 2 - Get a MongoDB database
59 |
60 | You can either use a localhost, or self-hosted option, or you can get a free hosted option at Mongo Atlas. Which ever option you choose, you'll just need the connection string. No need to make any collections manually. User and Session collections will be created as needed.
61 |
62 | It should look something like this:
63 |
64 | `mongodb+srv://USERNAME:PASSWORD@cloutfeedtest-fr1rq.mongodb.net/DATABASE_NAME?retryWrites=true&w=majority`
65 |
66 | _! HOLD ON TO THIS UNTIL STEP 3 !_
67 |
68 | ### Step 3
69 |
70 | Copy `.env.template` to `.env`, which contains:
71 |
72 | ```
73 | GITHUB_CLIENT_ID=
74 | GITHUB_CLIENT_SECRET=
75 | GITHUB_CALLBACK_URL=http://localhost:3000/api/auth/github/callback
76 |
77 | MONGODB_CONNECTION=
78 |
79 | COOKIE_NAME=cookie_name_here
80 | ```
81 |
82 | Enter the Github OAuth details, your MongoDB connection string, and change the coookie name to whatever you want it to be.
83 |
84 | ## Some examples available at:
85 |
86 | `pages/_app_.js`
87 |
88 | The \_app.js has been configured to check for logged in status on any entry load. When the app loads, it'll check for the users logged in status, and update the home page depending on that. Additionally, it'll run the components `getInitialProps` and `AppContainer.getInitialProps` concurrently, passing both responses into the page's props.
89 |
90 | ---
91 |
92 | `pages/private.js`
93 |
94 | This file is an example of using a custom HOC `withAuth` that will automatically check for logged in status, and redirect to the login page if the user is not logged in.
95 |
96 | ---
97 |
98 | `pages/protected-example/index.js`
99 |
100 | This file is an example of using `getInitialProps` with an authenticated API route.
101 |
102 | ---
103 |
104 | ### Custom config aliasing
105 |
106 | I've added some helper aliases to `next.config.js` so we don't have to use relative file paths on imports.
107 |
108 | These paths and resolvers can be found in `lib/config/paths` and `lib/config/resolvers`.
109 |
110 | These have been set up already:
111 |
112 | ```
113 | lib: resolveApp('lib'),
114 | pages: resolveApp('pages'),
115 | styles: resolveApp('lib/styles'),
116 | customHooks: resolveApp('lib/custom-hooks'),
117 | middleware: resolveApp('lib/middleware'),
118 | mongodbModels: resolveApp('lib/mongodb-models'),
119 | components: resolveApp('components')
120 | ```
121 |
122 | For example, we can import the mongoose user model to find a user by email anywhere in the app like this:
123 |
124 | ```
125 | import { findUserByEmail } from 'mongodbModels/users';
126 | !- instead of -!
127 | import { findUserByEmail } from '../../../lib/mongodb-models/users';
128 | ```
129 |
130 | Components can be imported from the ./components folder like this dynamic import example:
131 |
132 | ```
133 | const Header = dynamic(() => import('components/header'));
134 | const Footer = dynamic(() => import('components/footer'));
135 | ```
136 |
137 | No more relative paths yay!
138 |
139 | # License
140 |
141 | Copyright (c) 2020 David Wieler
142 |
143 | Permission is hereby granted, free of charge, to any person obtaining a copy
144 | of this software and associated documentation files (the "Software"), to deal
145 | in the Software without restriction, including without limitation the rights
146 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
147 | copies of the Software, and to permit persons to whom the Software is
148 | furnished to do so, subject to the following conditions:
149 |
150 | The above copyright notice and this permission notice shall be included in all
151 | copies or substantial portions of the Software.
152 |
153 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
154 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
155 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
156 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
157 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
158 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
159 | OR OTHER DEALINGS IN THE SOFTWARE.
160 |
--------------------------------------------------------------------------------
/components/footer/index.js:
--------------------------------------------------------------------------------
1 | const Footer = () => {
2 | return
Footer Content
;
3 | };
4 |
5 | export default Footer;
6 |
--------------------------------------------------------------------------------
/components/header/index.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | const Header = ({ userData }) => {
4 | return (
5 | <>
6 |
28 | >
29 | );
30 | };
31 |
32 | export default Header;
33 |
--------------------------------------------------------------------------------
/lib/config/index.js:
--------------------------------------------------------------------------------
1 | const paths = require('./paths')
2 | const resolvers = require('./resolvers')
3 |
4 | module.exports = {
5 | paths,
6 | resolvers
7 | }
8 |
--------------------------------------------------------------------------------
/lib/config/paths.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Custom Paths
3 | * This file was created to resolve paths without our app,
4 | * which saves us from backstepping imports like '../../../middleware/mongodb'.
5 | *
6 | * With this file we can require it in our resolvers.js file,
7 | * and use the needed files directly like: import ConnectDB from 'middleware/mongodb'.
8 | *
9 | * While it's used for webpack aliasing, we can also reference this file directly if needed,
10 | * and use the paths as required.
11 | */
12 |
13 | const path = require('path');
14 | const fs = require('fs');
15 |
16 | const appDirectory = fs.realpathSync(path.join(__dirname, '../../'));
17 |
18 | const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
19 |
20 | const paths = {
21 | lib: resolveApp('lib'),
22 | pages: resolveApp('pages'),
23 | styles: resolveApp('lib/styles'),
24 | customHooks: resolveApp('lib/custom-hooks'),
25 | middleware: resolveApp('lib/middleware'),
26 | mongodbModels: resolveApp('lib/mongodb-models'),
27 | components: resolveApp('components')
28 | };
29 |
30 | module.exports = paths;
31 |
--------------------------------------------------------------------------------
/lib/config/resolvers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Custom aliases
3 | * With our custom paths being required in and using them in out next.config.js file,
4 | * we're saved from backstepping imports like '../../../middleware/mongodb',
5 | * and can use the needed functionality directly like: import ConnectDB from 'middleware/mongodb'.
6 | */
7 |
8 | const path = require('path');
9 | const paths = require('./paths');
10 |
11 | module.exports = {
12 | alias: {
13 | lib: paths.lib,
14 | pages: paths.pages,
15 | customHooks: paths.customHooks,
16 | middleware: paths.middleware,
17 | mongodbModels: paths.mongodbModels,
18 | components: paths.components,
19 | styles: paths.styles
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/lib/custom-hooks/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useContext } from 'react';
2 | import Router from 'next/router';
3 | import { GlobalContext } from 'lib/state';
4 | import dynamic from 'next/dynamic';
5 |
6 | const LoginPage = dynamic(() => import('pages/login'));
7 |
8 | export const withAuth = (AuthComponent) => (props) => {
9 | const context = useContext(GlobalContext);
10 | const { state } = context;
11 | const userData = state.userData;
12 |
13 | useEffect(() => {
14 | if (userData) {
15 | if (userData && !userData.failedAuth) return; // do nothing if the user is logged in
16 | Router.replace(props.router.asPath, userData.redirectTo, { shallow: true });
17 | }
18 | }, [userData]);
19 |
20 | if (!userData) {
21 | return null;
22 | }
23 |
24 | if (userData.failedAuth || userData.status === 401) return ;
25 |
26 | return ;
27 | };
28 |
--------------------------------------------------------------------------------
/lib/fetch/index.js:
--------------------------------------------------------------------------------
1 | import fetch from 'isomorphic-unfetch';
2 |
3 | export const fetchWithCreds = async (req, route, post = false) => {
4 | let Cookie;
5 |
6 | if (req) {
7 | Cookie = req.headers.cookie;
8 | } else {
9 | Cookie = '';
10 | }
11 |
12 | const response = await fetch(route, {
13 | credentials: 'include',
14 | headers: { Cookie }
15 | });
16 |
17 | const data = await response.json();
18 |
19 | return data;
20 | };
21 |
--------------------------------------------------------------------------------
/lib/middleware/authenticate.js:
--------------------------------------------------------------------------------
1 | // This is our authentication middleware.
2 | // It simply checks if PassportJS's isAuthenticated method returns false,
3 | // and immediately returns a failed authentation response to the request.
4 |
5 | export const isAuthorized = (req, res, next) => {
6 | if (!req.isAuthenticated()) {
7 | return res.status(401).json({ status: 401, failedAuth: true, redirectTo: process.env.loginFailureURL });
8 | }
9 |
10 | next();
11 | };
12 |
--------------------------------------------------------------------------------
/lib/middleware/index.js:
--------------------------------------------------------------------------------
1 | import nextConnect from 'next-connect';
2 | import connectDB from './mongodb';
3 | import session from './session';
4 |
5 | const middleware = nextConnect();
6 |
7 | middleware.use(connectDB);
8 | middleware.use(session);
9 |
10 | export default middleware;
11 |
--------------------------------------------------------------------------------
/lib/middleware/mongodb.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | export const db = mongoose.connection;
4 |
5 | export const disconnect = () => mongoose.connection.close();
6 |
7 | export default async (req, res, next) => {
8 | if (mongoose.connections[0].readyState) {
9 | return next();
10 | }
11 |
12 | try {
13 | await mongoose.connect(process.env.MONGODB_CONNECTION, { useNewUrlParser: true, useUnifiedTopology: true });
14 | console.log('Connected to Mongo DB Server');
15 | } catch (error) {
16 | console.error('MONGODB ERROR: ', error);
17 | process.exit();
18 | }
19 |
20 | mongoose.connection.on('error', (error) => {
21 | console.error('MONGODB ERROR: ', error);
22 | });
23 |
24 | next();
25 | };
26 |
--------------------------------------------------------------------------------
/lib/middleware/passport/github.js:
--------------------------------------------------------------------------------
1 | import { Strategy as GitHubStrategy } from 'passport-github2';
2 | import { findUserByEmail } from 'mongodbModels/users';
3 |
4 | // STATICALLY configure the Github strategy for use by Passport.
5 | //
6 | // OAuth 2.0-based strategies require a `verify` function which receives the
7 | // credential (`accessToken`) for accessing the Github API on the user's
8 | // behalf, along with the user's profile. The function must invoke `cb`
9 | // with a user object, which will be exposed in the request as `req.user`
10 | // in api handlers after authentication.
11 | const strategy = new GitHubStrategy(
12 | {
13 | clientID: process.env.GITHUB_CLIENT_ID,
14 | clientSecret: process.env.GITHUB_CLIENT_SECRET,
15 | callbackURL: process.env.GITHUB_CALLBACK_URL,
16 | scope: 'user:email, public_repo',
17 | passReqToCallback: true
18 | },
19 | async (req, accessToken, refreshToken, profile, done) => {
20 | const userData = {
21 | name: profile._json.name,
22 | email: profile._json.email,
23 | github: {
24 | id: profile.id,
25 | username: profile.username,
26 | avatar: profile._json.avatar_url
27 | }
28 | };
29 |
30 | // Find the user by email, and do an upsert.
31 | // Which will either return the user in the DB while updating things like name and avatar,
32 | // or create a new user and return the new user data.
33 | let user = await findUserByEmail({
34 | email: userData.email,
35 | upsert: true,
36 | upsertData: userData
37 | });
38 |
39 | // Add our github token to the session incase we need it for something,
40 | // like using the OctoCat API, or making API requests to Github.
41 | req.session.token = accessToken;
42 |
43 | done(null, user);
44 | }
45 | );
46 |
47 | export default strategy;
48 |
--------------------------------------------------------------------------------
/lib/middleware/passport/local.js:
--------------------------------------------------------------------------------
1 | import { Strategy as LocalStrategy } from 'passport-local';
2 | import { findUserByEmail, comparePassword } from 'mongodbModels/users';
3 |
4 | const strategy = new LocalStrategy(
5 | {
6 | usernameField: 'email',
7 | passwordField: 'password'
8 | },
9 | async (email, password, done) => {
10 | // Find the user by email, and include the password in the
11 | // returned data
12 | const user = await findUserByEmail({
13 | email,
14 | includePassword: true
15 | });
16 |
17 | // If there is no user matching the provided email,
18 | // just return false
19 | if (!user) {
20 | return done(null, false);
21 | }
22 |
23 | // If a user is found, we need to compare the passwords using bcrypt.
24 | // If there is no password stored, due to user signing up with GitHub, etc.,
25 | // return false as local strategy won't work without a password.
26 |
27 | // If there is a password, compare the provided one with the saved one.
28 | // If they don't match, just return false.
29 | if (user.password) {
30 | const validPassword = await comparePassword(password, user.password);
31 |
32 | if (validPassword) {
33 | const returnedUser = {
34 | _id: user._id,
35 | email
36 | };
37 |
38 | return done(null, returnedUser);
39 | }
40 | }
41 |
42 | return done(null, false);
43 | }
44 | );
45 |
46 | export default strategy;
47 |
--------------------------------------------------------------------------------
/lib/middleware/session.js:
--------------------------------------------------------------------------------
1 | import passport from 'passport';
2 | import session from 'next-session';
3 | import connectMongo from 'connect-mongo';
4 | import connectDB, { db } from 'middleware/mongodb';
5 | import nextConnect from 'next-connect';
6 | import redirect from 'micro-redirect';
7 |
8 | import localStrategy from 'middleware/passport/local';
9 | import gitHubStrategy from 'middleware/passport/github';
10 |
11 | const handler = nextConnect();
12 |
13 | // Initialize MongoDB Connection
14 | handler.use(connectDB);
15 |
16 | // Our Passport Strategies
17 | // Add additional strats here
18 | passport.use(localStrategy);
19 | passport.use(gitHubStrategy);
20 |
21 | // The PassportJS Serializers
22 | // Customize as needed
23 | passport.serializeUser((user, done) => done(null, user));
24 | passport.deserializeUser((user, done) => done(null, user));
25 |
26 | handler.use((req, res, next) => {
27 | // Polyfill the res.redirect method that PassportJS needs
28 | res.redirect = (location, status = 302) => {
29 | redirect(res, status, location);
30 | };
31 | next();
32 | });
33 |
34 | // The Cookie Settings and Session Store
35 | handler.use((req, res, next) => {
36 | const MongoStore = connectMongo(session);
37 |
38 | return session({
39 | name: process.env.COOKIE_NAME,
40 | storePromisify: true,
41 | store: new MongoStore({ mongooseConnection: db })
42 | })(req, res, next);
43 | });
44 |
45 | // Initialize PassportJS
46 | handler.use((req, res, next) => {
47 | return passport.initialize()(req, res, next);
48 | });
49 |
50 | // Initialize the Session
51 | handler.use((req, res, next) => {
52 | return passport.session()(req, res, next);
53 | });
54 |
55 | export default handler;
56 |
--------------------------------------------------------------------------------
/lib/mongodb-models/users.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | import bcrypt from 'bcryptjs';
3 |
4 | const Schema = mongoose.Schema;
5 |
6 | // Our user schema.
7 | // Password is set to select:false,
8 | // so it won't return in any find statements by default.
9 | const UserSchema = new Schema({
10 | name: String,
11 | email: String,
12 | password: { type: String, select: false },
13 | github: {
14 | id: String,
15 | username: String,
16 | avatar: String
17 | }
18 | });
19 |
20 | // Export the user model.
21 | // On startup, the Mongoose model will be created and returned.
22 | // Any additional times the model is required, it'll return the already created
23 | // schema. If we don't do this, Next will error with a Mongoose error due to it
24 | // trying to recreate the schema.
25 | export const User = mongoose.models.User || mongoose.model('User', UserSchema);
26 |
27 | export const findUserByEmail = async ({ email, includePassword = false, upsert = false, upsertData = {} }) => {
28 | let user = null;
29 |
30 | if (upsert) {
31 | user = await User.findOneAndUpdate({ email: email }, { $set: upsertData }, { upsert: true, useFindAndModify: false, new: true });
32 | } else {
33 | user = await User.findOne({ email }, { password: includePassword });
34 | }
35 |
36 | return user;
37 | };
38 |
39 | export const comparePassword = async (password, userPassword) => {
40 | const compare = await bcrypt.compare(password, userPassword);
41 |
42 | return compare;
43 | };
44 |
45 | export const createUser = async ({ email, password }) => {
46 | if (!email || !password) {
47 | return;
48 | }
49 |
50 | const salt = await bcrypt.genSalt(10);
51 | const hash = await bcrypt.hash(password, salt);
52 |
53 | const userDetails = {
54 | email,
55 | password: hash
56 | };
57 |
58 | const newUser = User(userDetails);
59 | const saveUser = await newUser.save();
60 |
61 | return saveUser;
62 | };
63 |
--------------------------------------------------------------------------------
/lib/state/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | // Create the initial context
5 | export const GlobalContext = React.createContext();
6 |
7 | // Create the context consumer
8 | export const GlobalContextConsumer = GlobalContext.Consumer;
9 |
10 | export const GlobalProvider = (props) => {
11 | const [userData, setUserData] = useState(null);
12 |
13 | // This is the stuff we want to fire on initial app load.
14 | // Be careful here, as this only happens once
15 | // DO NOT ADD ANYTHING TO THE RETURNED ARRAY...
16 | // otherwise the entire state will reload.
17 | useEffect(() => {
18 | // Check for debug flag in URL
19 | console.log('app state test');
20 | }, []);
21 |
22 | return (
23 |
31 | {props.children}
32 |
33 | );
34 | };
35 |
36 | GlobalProvider.propTypes = {
37 | children: PropTypes.node
38 | };
39 |
--------------------------------------------------------------------------------
/lib/styles/common/reset.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0-modified | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html,
7 | body,
8 | div,
9 | span,
10 | applet,
11 | object,
12 | iframe,
13 | h1,
14 | h2,
15 | h3,
16 | h4,
17 | h5,
18 | h6,
19 | p,
20 | blockquote,
21 | pre,
22 | a,
23 | abbr,
24 | acronym,
25 | address,
26 | big,
27 | cite,
28 | code,
29 | del,
30 | dfn,
31 | em,
32 | img,
33 | ins,
34 | kbd,
35 | q,
36 | s,
37 | samp,
38 | small,
39 | strike,
40 | strong,
41 | sub,
42 | sup,
43 | tt,
44 | var,
45 | b,
46 | u,
47 | i,
48 | center,
49 | dl,
50 | dt,
51 | dd,
52 | ol,
53 | ul,
54 | li,
55 | fieldset,
56 | form,
57 | label,
58 | legend,
59 | table,
60 | caption,
61 | tbody,
62 | tfoot,
63 | thead,
64 | tr,
65 | th,
66 | td,
67 | article,
68 | aside,
69 | canvas,
70 | details,
71 | embed,
72 | figure,
73 | figcaption,
74 | footer,
75 | header,
76 | hgroup,
77 | menu,
78 | nav,
79 | output,
80 | ruby,
81 | section,
82 | summary,
83 | time,
84 | mark,
85 | audio,
86 | video {
87 | margin: 0;
88 | padding: 0;
89 | border: 0;
90 | font-size: 100%;
91 | font: inherit;
92 | vertical-align: baseline;
93 | }
94 |
95 | /* make sure to set some focus styles for accessibility */
96 | :focus {
97 | outline: 0;
98 | }
99 |
100 | /* HTML5 display-role reset for older browsers */
101 | article,
102 | aside,
103 | details,
104 | figcaption,
105 | figure,
106 | footer,
107 | header,
108 | hgroup,
109 | menu,
110 | nav,
111 | section {
112 | display: block;
113 | }
114 |
115 | body {
116 | line-height: 1;
117 | }
118 |
119 | ol,
120 | ul {
121 | list-style: none;
122 | }
123 |
124 | blockquote,
125 | q {
126 | quotes: none;
127 | }
128 |
129 | blockquote:before,
130 | blockquote:after,
131 | q:before,
132 | q:after {
133 | content: '';
134 | content: none;
135 | }
136 |
137 | table {
138 | border-collapse: collapse;
139 | border-spacing: 0;
140 | }
141 |
142 | input[type='search']::-webkit-search-cancel-button,
143 | input[type='search']::-webkit-search-decoration,
144 | input[type='search']::-webkit-search-results-button,
145 | input[type='search']::-webkit-search-results-decoration {
146 | -webkit-appearance: none;
147 | -moz-appearance: none;
148 | }
149 |
150 | input[type='search'] {
151 | -webkit-appearance: none;
152 | -moz-appearance: none;
153 | -webkit-box-sizing: content-box;
154 | -moz-box-sizing: content-box;
155 | box-sizing: content-box;
156 | }
157 |
158 | textarea {
159 | overflow: auto;
160 | vertical-align: top;
161 | resize: vertical;
162 | }
163 |
164 | /**
165 | * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
166 | */
167 |
168 | audio,
169 | canvas,
170 | video {
171 | display: inline-block;
172 | *display: inline;
173 | *zoom: 1;
174 | max-width: 100%;
175 | }
176 |
177 | /**
178 | * Prevent modern browsers from displaying `audio` without controls.
179 | * Remove excess height in iOS 5 devices.
180 | */
181 |
182 | audio:not([controls]) {
183 | display: none;
184 | height: 0;
185 | }
186 |
187 | /**
188 | * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
189 | * Known issue: no IE 6 support.
190 | */
191 |
192 | [hidden] {
193 | display: none;
194 | }
195 |
196 | /**
197 | * 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
198 | * `em` units.
199 | * 2. Prevent iOS text size adjust after orientation change, without disabling
200 | * user zoom.
201 | */
202 |
203 | html {
204 | font-size: 100%; /* 1 */
205 | -webkit-text-size-adjust: 100%; /* 2 */
206 | -ms-text-size-adjust: 100%; /* 2 */
207 | }
208 |
209 | /**
210 | * Address `outline` inconsistency between Chrome and other browsers.
211 | */
212 |
213 | a:focus {
214 | outline: thin dotted;
215 | }
216 |
217 | /**
218 | * Improve readability when focused and also mouse hovered in all browsers.
219 | */
220 |
221 | a:active,
222 | a:hover {
223 | outline: 0;
224 | }
225 |
226 | /**
227 | * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
228 | * 2. Improve image quality when scaled in IE 7.
229 | */
230 |
231 | img {
232 | border: 0; /* 1 */
233 | -ms-interpolation-mode: bicubic; /* 2 */
234 | }
235 |
236 | /**
237 | * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
238 | */
239 |
240 | figure {
241 | margin: 0;
242 | }
243 |
244 | /**
245 | * Correct margin displayed oddly in IE 6/7.
246 | */
247 |
248 | form {
249 | margin: 0;
250 | }
251 |
252 | /**
253 | * Define consistent border, margin, and padding.
254 | */
255 |
256 | fieldset {
257 | border: 1px solid #c0c0c0;
258 | margin: 0 2px;
259 | padding: 0.35em 0.625em 0.75em;
260 | }
261 |
262 | /**
263 | * 1. Correct color not being inherited in IE 6/7/8/9.
264 | * 2. Correct text not wrapping in Firefox 3.
265 | * 3. Correct alignment displayed oddly in IE 6/7.
266 | */
267 |
268 | legend {
269 | border: 0; /* 1 */
270 | padding: 0;
271 | white-space: normal; /* 2 */
272 | *margin-left: -7px; /* 3 */
273 | }
274 |
275 | /**
276 | * 1. Correct font size not being inherited in all browsers.
277 | * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
278 | * and Chrome.
279 | * 3. Improve appearance and consistency in all browsers.
280 | */
281 |
282 | button,
283 | input,
284 | select,
285 | textarea {
286 | font-size: 100%; /* 1 */
287 | margin: 0; /* 2 */
288 | vertical-align: baseline; /* 3 */
289 | *vertical-align: middle; /* 3 */
290 | }
291 |
292 | /**
293 | * Address Firefox 3+ setting `line-height` on `input` using `!important` in
294 | * the UA stylesheet.
295 | */
296 |
297 | button,
298 | input {
299 | line-height: normal;
300 | }
301 |
302 | /**
303 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
304 | * All other form control elements do not inherit `text-transform` values.
305 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
306 | * Correct `select` style inheritance in Firefox 4+ and Opera.
307 | */
308 |
309 | button,
310 | select {
311 | text-transform: none;
312 | }
313 |
314 | /**
315 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
316 | * and `video` controls.
317 | * 2. Correct inability to style clickable `input` types in iOS.
318 | * 3. Improve usability and consistency of cursor style between image-type
319 | * `input` and others.
320 | * 4. Remove inner spacing in IE 7 without affecting normal text inputs.
321 | * Known issue: inner spacing remains in IE 6.
322 | */
323 |
324 | button,
325 | html input[type="button"], /* 1 */
326 | input[type="reset"],
327 | input[type="submit"] {
328 | -webkit-appearance: button; /* 2 */
329 | cursor: pointer; /* 3 */
330 | *overflow: visible; /* 4 */
331 | }
332 |
333 | /**
334 | * Re-set default cursor for disabled elements.
335 | */
336 |
337 | button[disabled],
338 | html input[disabled] {
339 | cursor: default;
340 | }
341 |
342 | /**
343 | * 1. Address box sizing set to content-box in IE 8/9.
344 | * 2. Remove excess padding in IE 8/9.
345 | * 3. Remove excess padding in IE 7.
346 | * Known issue: excess padding remains in IE 6.
347 | */
348 |
349 | input[type='checkbox'],
350 | input[type='radio'] {
351 | box-sizing: border-box; /* 1 */
352 | padding: 0; /* 2 */
353 | *height: 13px; /* 3 */
354 | *width: 13px; /* 3 */
355 | }
356 |
357 | /**
358 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
359 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
360 | * (include `-moz` to future-proof).
361 | */
362 |
363 | input[type='search'] {
364 | -webkit-appearance: textfield; /* 1 */
365 | -moz-box-sizing: content-box;
366 | -webkit-box-sizing: content-box; /* 2 */
367 | box-sizing: content-box;
368 | }
369 |
370 | /**
371 | * Remove inner padding and search cancel button in Safari 5 and Chrome
372 | * on OS X.
373 | */
374 |
375 | input[type='search']::-webkit-search-cancel-button,
376 | input[type='search']::-webkit-search-decoration {
377 | -webkit-appearance: none;
378 | }
379 |
380 | /**
381 | * Remove inner padding and border in Firefox 3+.
382 | */
383 |
384 | button::-moz-focus-inner,
385 | input::-moz-focus-inner {
386 | border: 0;
387 | padding: 0;
388 | }
389 |
390 | /**
391 | * 1. Remove default vertical scrollbar in IE 6/7/8/9.
392 | * 2. Improve readability and alignment in all browsers.
393 | */
394 |
395 | textarea {
396 | overflow: auto; /* 1 */
397 | vertical-align: top; /* 2 */
398 | }
399 |
400 | /**
401 | * Remove most spacing between table cells.
402 | */
403 |
404 | table {
405 | border-collapse: collapse;
406 | border-spacing: 0;
407 | }
408 |
409 | html,
410 | button,
411 | input,
412 | select,
413 | textarea {
414 | color: #222;
415 | }
416 |
417 | ::-moz-selection {
418 | background: #b3d4fc;
419 | text-shadow: none;
420 | }
421 |
422 | ::selection {
423 | background: #b3d4fc;
424 | text-shadow: none;
425 | }
426 |
427 | img {
428 | vertical-align: middle;
429 | }
430 |
431 | fieldset {
432 | border: 0;
433 | margin: 0;
434 | padding: 0;
435 | }
436 |
437 | textarea {
438 | resize: vertical;
439 | }
440 |
441 | .chromeframe {
442 | margin: 0.2em 0;
443 | background: #ccc;
444 | color: #000;
445 | padding: 0.2em 0;
446 | }
447 |
--------------------------------------------------------------------------------
/lib/styles/global.scss:
--------------------------------------------------------------------------------
1 | @import './common/reset';
2 | @import './variables';
3 |
4 | @import './layout';
5 |
--------------------------------------------------------------------------------
/lib/styles/layout.scss:
--------------------------------------------------------------------------------
1 | nav {
2 | display: flex;
3 | height: $navHeight;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 0 $unit * 6;
7 | background: #d8d8d8;
8 |
9 | .brand {
10 | font-size: $unit * 4;
11 | text-transform: uppercase;
12 | }
13 | .access {
14 | font-size: $unit * 4;
15 | text-transform: uppercase;
16 | }
17 | }
18 |
19 | .layout {
20 | main {
21 | display: flex;
22 | min-height: calc(100vh - #{$navHeight});
23 |
24 | &.logged-in {
25 | .page {
26 | background: linear-gradient(to bottom, #4989bf 0%, rgba(246, 246, 246, 1) 47%, rgba(255, 255, 255, 1) 100%);
27 | }
28 | }
29 |
30 | h2 {
31 | font-size: $unit * 6;
32 | margin-bottom: $unit * 3;
33 | }
34 |
35 | .page {
36 | display: flex;
37 | padding-top: $unit * 12;
38 | padding-left: $unit * 6;
39 | padding-right: $unit * 6;
40 | width: 100vw;
41 | background: linear-gradient(to bottom, rgba(216, 216, 216, 1) 0%, rgba(246, 246, 246, 1) 47%, rgba(255, 255, 255, 1) 100%);
42 |
43 | &-private {
44 | flex-direction: column;
45 | text-align: center;
46 |
47 | img {
48 | width: 125px;
49 | border-radius: 100%;
50 | margin-bottom: $unit * 3;
51 | }
52 | }
53 |
54 | &-login {
55 | flex-direction: column;
56 | align-items: flex-start;
57 |
58 | .form-wrapper {
59 | padding-bottom: $unit * 6;
60 | margin-bottom: $unit * 6;
61 | border-bottom: 2px solid grey;
62 |
63 | &:last-of-type {
64 | border-bottom: 0;
65 | }
66 |
67 | input {
68 | margin-bottom: $unit * 6;
69 | }
70 |
71 | .error {
72 | color: red;
73 | margin-top: $unit * 3;
74 | }
75 | }
76 | }
77 | }
78 | }
79 | }
80 |
81 | form {
82 | label,
83 | input,
84 | button {
85 | display: block;
86 | }
87 | }
88 |
89 | .footer {
90 | padding: $unit * 6;
91 | }
92 |
--------------------------------------------------------------------------------
/lib/styles/variables.scss:
--------------------------------------------------------------------------------
1 | $navHeight: 100px;
2 | $unit: 4px;
3 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withSass = require('@zeit/next-sass');
2 | const withCSS = require('@zeit/next-css');
3 | const webpack = require('webpack');
4 |
5 | // Load in our environment variables
6 | require('dotenv').config();
7 |
8 | // Load in our config that has custom resolvers for lib and middleware files
9 | const customConfig = require('./lib/config');
10 |
11 | // Wrap webpack using withCSS and withSCSS so we can use regular css and scss files
12 | module.exports = withCSS(
13 | withSass({
14 | env: {
15 | loginFailureURL: '/login',
16 | loginSuccessURL: '/private'
17 | },
18 | webpack(config, options) {
19 | // Add env to next.
20 | // Using `process.browser` to determine if we're client side or server side,
21 | // we can use our secret keys and such, and they are still secure.
22 | // Environment data added here is not available client side
23 | config.plugins.push(new webpack.EnvironmentPlugin(process.env));
24 |
25 | // Add in our resolvers to Next's aliases
26 | // Custom aliases first, so we don't override something in Next by mistake
27 | config.resolve.alias = {
28 | ...customConfig.resolvers.alias,
29 | ...config.resolve.alias
30 | };
31 |
32 | return config;
33 | }
34 | })
35 | );
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api-routes-rest",
3 | "author": "David Wieler",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "dev": "next",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@octokit/rest": "^16.35.0",
12 | "@zeit/next-css": "^1.0.1",
13 | "@zeit/next-sass": "^1.0.1",
14 | "bcryptjs": "^2.4.3",
15 | "connect-mongo": "^3.1.2",
16 | "dotenv": "^8.2.0",
17 | "isomorphic-unfetch": "3.0.0",
18 | "micro-redirect": "^1.0.0",
19 | "mongoose": "^5.7.11",
20 | "next": "latest",
21 | "next-connect": "^0.5.1",
22 | "next-session": "^2.1.1",
23 | "node-sass": "^4.13.1",
24 | "passport": "^0.4.0",
25 | "passport-github2": "^0.1.11",
26 | "passport-local": "^1.0.0",
27 | "react": "^16.8.6",
28 | "react-dom": "^16.8.6",
29 | "serialize-javascript": "^2.1.1",
30 | "webpack": "^4.41.2"
31 | },
32 | "license": "MIT"
33 | }
34 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useContext } from 'react';
2 | import App from 'next/app';
3 | import dynamic from 'next/dynamic';
4 | import { fetchWithCreds } from 'lib/fetch';
5 | import 'styles/global.scss';
6 |
7 | import { GlobalProvider, GlobalContext } from 'lib/state';
8 |
9 | const Header = dynamic(() => import('components/header'));
10 | const Footer = dynamic(() => import('components/footer'));
11 |
12 | const AppContainer = ({ children, userData, router }) => {
13 | const context = useContext(GlobalContext);
14 | const { state, actions } = context;
15 |
16 | useEffect(() => {
17 | if (userData && !state.userData) {
18 | console.log('updating user data', userData);
19 | actions.setUserData(userData);
20 | }
21 | }, [userData]);
22 |
23 | return (
24 | <>
25 |
26 |
{children}
27 |
28 | >
29 | );
30 | };
31 |
32 | AppContainer.getInitialProps = async ({ req }) => {
33 | const userData = await fetchWithCreds(req, 'http://localhost:3000/api/user');
34 | return { userData };
35 | };
36 |
37 | const Main = ({ appProps: { userData }, Component, pageProps, router }) => {
38 | if (!userData) {
39 | return null;
40 | }
41 |
42 | return (
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default class extends App {
50 | static async getInitialProps({ Component, ctx }) {
51 | // Call the page's `getInitialProps` if it exists. Don't `await` it yet,
52 | // because we'd rather `await` them together concurrently.
53 | const pagePropsTask = Component.getInitialProps ? Component.getInitialProps(ctx) : {};
54 |
55 | // Call the container's `getInitialProps` if it exists. Don't `await` it yet,
56 | // because we'd rather `await` them together concurrently.
57 | const appContainerTask = AppContainer.getInitialProps ? AppContainer.getInitialProps(ctx) : { userData: { failedAuth: true } };
58 |
59 | // Both tasks are running concurrently, now await them both.
60 | const [pageProps, appProps] = await Promise.all([pagePropsTask, appContainerTask]);
61 |
62 | return { pageProps, appProps };
63 | }
64 |
65 | render = () => {
66 | const { Component, pageProps, appProps, router } = this.props;
67 |
68 | return (
69 |
70 |
71 |
72 |
73 |
74 | );
75 | };
76 | }
77 |
--------------------------------------------------------------------------------
/pages/api/auth/[provider].js:
--------------------------------------------------------------------------------
1 | import nextConnect from 'next-connect';
2 | import passport from 'passport';
3 | import { isAuthorized } from 'middleware/authenticate';
4 |
5 | import middleware from 'middleware';
6 |
7 | const handler = nextConnect();
8 |
9 | handler.use(middleware);
10 |
11 | handler.get(async (req, res, next) => {
12 | const {
13 | query: { provider },
14 | method
15 | } = req;
16 |
17 | switch (provider) {
18 | case 'github':
19 | return passport.authenticate('github')(req, res, next);
20 | case 'local':
21 | res.status(200).json({ provider, status: 'local login provided at /api/auth/local with method POST' });
22 | break;
23 | default:
24 | res.status(200).json({ provider, status: 'not enabled' });
25 | break;
26 | }
27 | });
28 |
29 | handler.post((req, res, next) =>
30 | passport.authenticate('local', (err, user) => {
31 | if (err) {
32 | return next(err);
33 | }
34 |
35 | if (!user) {
36 | return res.status(401).json({
37 | status: 401,
38 | failedAuth: true,
39 | redirectTo: process.env.loginFailureURL,
40 | errorMessage: 'Username or Password is incorrect'
41 | });
42 | }
43 |
44 | req.logIn(user, (err) => {
45 | if (err) {
46 | return next(err);
47 | }
48 |
49 | res.status(200).json({ status: 200, user });
50 | });
51 | })(req, res, next)
52 | );
53 |
54 | export default handler;
55 |
--------------------------------------------------------------------------------
/pages/api/auth/[provider]/callback.js:
--------------------------------------------------------------------------------
1 | import nextConnect from 'next-connect';
2 | import passport from 'passport';
3 | import middleware from 'middleware';
4 |
5 | const handler = nextConnect();
6 |
7 | handler.use(middleware);
8 |
9 | handler.get(async (req, res, next) => {
10 | const { provider } = req.query;
11 |
12 | passport.authenticate(provider, {
13 | failureRedirect: process.env.loginFailureURL,
14 | successRedirect: process.env.loginSuccessURL
15 | })(req, res, next);
16 | });
17 |
18 | export default handler;
19 |
--------------------------------------------------------------------------------
/pages/api/logout.js:
--------------------------------------------------------------------------------
1 | import nextConnect from 'next-connect';
2 | import middleware from 'middleware';
3 |
4 | const handler = nextConnect();
5 |
6 | handler.use(middleware);
7 |
8 | handler.get(async (req, res, next) => {
9 | if (req.user) {
10 | req.logout();
11 | res.redirect('/');
12 | } else {
13 | res.redirect(process.env.loginFailureURL);
14 | }
15 | });
16 |
17 | export default handler;
18 |
--------------------------------------------------------------------------------
/pages/api/protected-route.js:
--------------------------------------------------------------------------------
1 | import nextConnect from 'next-connect';
2 | import middleware from 'middleware';
3 | import { isAuthorized } from 'middleware/authenticate';
4 | import { findUserByEmail } from 'mongodbModels/users';
5 |
6 | const handler = nextConnect();
7 |
8 | // Use our middleware, which contains the MongoDB connection and PassportJS
9 | handler.use(middleware);
10 |
11 | // Use this isAuthorized middleware on any API routes that need authorization.
12 | // If PassportJS's "isAuthenticated" returns false, anything below it will not run.
13 |
14 | // See ./lib/middleware/authenticate.js for more information
15 | handler.use(isAuthorized);
16 |
17 | // If the user is authenticated, get the user data from MongoDB and return it.
18 | // In your page props, you can check for failedAuth because
19 | // handler.use(isAuthorized) automatically returns that boolean.
20 |
21 | // If it's not set, PassportJS authenticated correctly and you can do what ever you need
22 | // knowing the user is logged in.
23 | // In this example we're just going to look up the user in Mongo and return it because we
24 | // haven't stored anything else.
25 | handler.get(async (req, res) => {
26 | let user = await findUserByEmail({
27 | email: req.user.email
28 | });
29 |
30 | res.status(200).json({
31 | protectedData: {
32 | user
33 | }
34 | });
35 | });
36 |
37 | export default handler;
38 |
--------------------------------------------------------------------------------
/pages/api/register.js:
--------------------------------------------------------------------------------
1 | import nextConnect from 'next-connect';
2 | import { createUser, findUserByEmail } from 'mongodbModels/users';
3 |
4 | import connectDB from 'middleware/mongodb';
5 |
6 | const handler = nextConnect();
7 |
8 | handler.use(connectDB);
9 |
10 | handler.post(async (req, res, next) => {
11 | const { email, password } = req.body;
12 |
13 | if (!email && !password) {
14 | res.status(400).json({ status: 409, failedAuth: false, redirectTo: '/login', errorMessage: 'Invalid Entry' });
15 | return;
16 | }
17 |
18 | // Try to find existing user by email
19 | const userExists = await findUserByEmail({ email });
20 |
21 | if (userExists) {
22 | // User exists return with an error message
23 | res.status(200).json({ status: 409, failedAuth: false, redirectTo: '/login', errorMessage: 'User Exists' });
24 | return;
25 | }
26 |
27 | // No user exists with that email, create the user
28 | const user = await createUser({ email, password });
29 |
30 | res.status(200).json({ status: 200 });
31 | });
32 |
33 | export default handler;
34 |
--------------------------------------------------------------------------------
/pages/api/user.js:
--------------------------------------------------------------------------------
1 | import nextConnect from 'next-connect';
2 | import middleware from 'middleware';
3 | import { isAuthorized } from 'middleware/authenticate';
4 |
5 | const handler = nextConnect();
6 |
7 | // Use our middleware, which contains the MongoDB connection and PassportJS
8 | handler.use(middleware);
9 |
10 | // Use this isAuthorized middleware on any API routes that need authorization.
11 | // If PassportJS's "isAuthenticated" returns false, anything below it will not run.
12 |
13 | // See ./lib/middleware/authenticate.js for more information
14 | handler.use(isAuthorized);
15 |
16 | handler.get(async (req, res) => {
17 | res.status(200).json({ user: req.user });
18 | });
19 |
20 | export default handler;
21 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useContext } from 'react';
2 | import Link from 'next/link';
3 | import { GlobalContext } from 'lib/state';
4 |
5 | const Index = () => {
6 | const context = useContext(GlobalContext);
7 | const {
8 | state: { userData }
9 | } = context;
10 |
11 | return (
12 |
33 | );
34 | };
35 |
36 | // Wrap our private page in withRouter and withAuth.
37 | // Any page that has withAuth also requires withRouter.
38 | // withAuth will check the state to see if a user is defined.
39 | // If not, it'll redirect to the login page automatically.
40 |
41 | // See ./lib/customHooks/index.js for more information.
42 |
43 | export default withRouter(withAuth(Private));
44 |
--------------------------------------------------------------------------------
/pages/protected-example/index.js:
--------------------------------------------------------------------------------
1 | import { fetchWithCreds } from 'lib/fetch';
2 | import Link from 'next/link';
3 | // This is an example of using a basic page with getInitialProps,
4 | // and a protected route.
5 |
6 | // See pages/api/protected-route.js for more information.
7 | const User = ({ failedAuth, protectedData }) => {
8 | if (!failedAuth) {
9 | console.log('protectedData', protectedData);
10 | }
11 |
12 | return (
13 |
14 | {failedAuth && (
15 | <>
16 |
17 | login
18 |
19 | >
20 | )}
21 |
22 | {!failedAuth && <>Check the console for the initial props in "protectedData">}
23 |