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