Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(example): add CRUD example with next-connect and passport #11359

Merged
merged 31 commits into from
Apr 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
df27cad
Add with-next-connect example
hoangvvo Mar 26, 2020
8b19ec5
Update README
hoangvvo Mar 26, 2020
ec49bb1
Fix code and lint
hoangvvo Mar 26, 2020
5146663
Fix typo
hoangvvo Mar 26, 2020
998585b
Run prettier
hoangvvo Mar 26, 2020
09e49d6
Include username
hoangvvo Mar 26, 2020
e575c87
Merge branch 'canary' into example-next-connect-passport
hoangvvo Mar 26, 2020
20f4a62
Merge branch 'canary' into example-next-connect-passport
hoangvvo Mar 26, 2020
ed1c60f
Rename example
hoangvvo Mar 27, 2020
742c4fc
Match with-passport styling
hoangvvo Mar 28, 2020
dfb7147
Add comments in code
hoangvvo Mar 28, 2020
5fc7289
Merge branch 'canary' into example-next-connect-passport
hoangvvo Mar 28, 2020
b56040d
Run prettier
hoangvvo Mar 28, 2020
d76831f
Rewrite example
hoangvvo Mar 28, 2020
721e3de
Add some comments
hoangvvo Mar 28, 2020
71bab68
Update README.md
hoangvvo Mar 30, 2020
d4c8c36
Merge branch 'canary' into example-next-connect-passport
hoangvvo Mar 30, 2020
f3b78b1
keys -> secret
hoangvvo Mar 30, 2020
1899025
Merge branch 'canary' into example-next-connect-passport
hoangvvo Mar 30, 2020
af8442f
Merge branch 'canary' into example-next-connect-passport
hoangvvo Apr 7, 2020
f878e11
Updated package.json and readme
Apr 8, 2020
f962cf1
UX changes
Apr 8, 2020
3a4c5b3
Securely encrypt cookie with @hapi/iron
hoangvvo Apr 8, 2020
f9eef14
Merge branch 'canary' into example-next-connect-passport
hoangvvo Apr 8, 2020
39cdbef
Update README
hoangvvo Apr 8, 2020
4d7350a
Merge branch 'example-next-connect-passport' of https://github.com/ho…
hoangvvo Apr 8, 2020
e3c34e3
Abstract db related actions and update README
hoangvvo Apr 9, 2020
fa3b16a
security: add note on password hashing
hoangvvo Apr 9, 2020
4af41b3
Merge branch 'canary' into example-next-connect-passport
hoangvvo Apr 9, 2020
ac72ed8
remove unused dep
hoangvvo Apr 9, 2020
9d83442
Updated readme
Apr 13, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions examples/with-passport-and-next-connect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# next-connect and Passport

This example creates a basic [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) app using [next-connect](https://github.com/hoangvvo/next-connect) and cookie-based authentication with [Passport.js](http://www.passportjs.org/). The cookie is securely encrypted using [@hapi/iron](https://github.com/hapijs/iron).

The example shows how to do a sign up, login, logout, and account deactivation. It utilizes [SWR](https://swr.now.sh/) to fetch the API.

For demo purpose, the users database is stored in the cookie session. You need to replace it with an actual database to store users in [db.js](lib/db.js).

In production, you must use a password hashing library, such as [argon2](https://github.com/ranisalt/node-argon2) or [bcrypt](https://www.npmjs.com/package/bcrypt).

## Deploy your own

Deploy the example using [ZEIT Now](https://zeit.co/now):

[![Deploy with ZEIT Now](https://zeit.co/button)](https://zeit.co/import/project?template=https://github.com/zeit/next.js/tree/canary/examples/with-passport-and-next-connect)

## How to use

Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:

```bash
npx create-next-app --example with-passport-and-next-connect with-passport-and-next-connect-app
# or
yarn create next-app --example with-passport-and-next-connect with-passport-and-next-connect-app
```

Deploy it to the cloud with [ZEIT Now](https://zeit.co/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
80 changes: 80 additions & 0 deletions examples/with-passport-and-next-connect/components/Navbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import Link from 'next/link'
import { useUser } from '../lib/hooks'

export default function Navbar() {
const [user, { mutate }] = useUser()

async function handleLogout() {
await fetch('/api/logout')
mutate({ user: null })
}

return (
<header>
<nav>
<ul>
<li>
<Link href="/">
<a>Home</a>
</Link>
</li>
{user ? (
<>
<li>
<Link href="/profile">
<a>Profile</a>
</Link>
</li>
<li>
<a role="button" onClick={handleLogout}>
Logout
</a>
</li>
</>
) : (
<>
<li>
<Link href="/signup">
<a>Sign up</a>
</Link>
</li>
<li>
<Link href="/login">
<a>Login</a>
</Link>
</li>
</>
)}
</ul>
</nav>
<style jsx>{`
nav {
max-width: 42rem;
margin: 0 auto;
padding: 0.2rem 1.25rem;
}
ul {
display: flex;
list-style: none;
margin-left: 0;
padding-left: 0;
}
li {
margin-right: 1rem;
}
li:first-child {
margin-left: auto;
}
a {
color: #fff;
text-decoration: none;
cursor: pointer;
}
header {
color: #fff;
background-color: #333;
}
`}</style>
</header>
)
}
32 changes: 32 additions & 0 deletions examples/with-passport-and-next-connect/lib/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export function getAllUsers(req) {
// For demo purpose only. You are not likely to have to return all users.
return req.session.users
}

export function createUser(req, user) {
// Here you should insert the user into the database
// await db.createUser(user)
req.session.users.push(user)
}

export function findUserByUsername(req, username) {
// Here you find the user based on id/username in the database
// const user = await db.findUserById(id)
return req.session.users.find(user => user.username === username)
}

export function updateUserByUsername(req, username, update) {
// Here you update the user based on id/username in the database
// const user = await db.updateUserById(id, update)
const user = req.session.users.find(u => u.username === username)
Object.assign(user, update)
return user
}

export function deleteUser(req, username) {
// Here you should delete the user in the database
// await db.deleteUser(req.user)
req.session.users = req.session.users.filter(
user => user.username !== req.user.username
)
}
11 changes: 11 additions & 0 deletions examples/with-passport-and-next-connect/lib/hooks.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import useSWR from 'swr'

export const fetcher = url => fetch(url).then(r => r.json())

export function useUser() {
const { data, mutate } = useSWR('/api/user', fetcher)
// if data is not defined, the query has not completed
const loading = !data
const user = data?.user
return [user, { mutate, loading }]
}
33 changes: 33 additions & 0 deletions examples/with-passport-and-next-connect/lib/passport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import passport from 'passport'
import LocalStrategy from 'passport-local'
import { findUserByUsername } from './db'

passport.serializeUser(function(user, done) {
// serialize the username into session
done(null, user.username)
})

passport.deserializeUser(function(req, id, done) {
// deserialize the username back into user object
const user = findUserByUsername(req, id)
done(null, user)
})

passport.use(
new LocalStrategy(
{ passReqToCallback: true },
(req, username, password, done) => {
// Here you lookup the user in your DB and compare the password/hashed password
const user = findUserByUsername(req, username)
// Security-wise, if you hashed the password earlier, you must verify it
// if (!user || await argon2.verify(user.password, password))
if (!user || user.password !== password) {
done(null, null)
} else {
done(null, user)
}
}
)
)

export default passport
32 changes: 32 additions & 0 deletions examples/with-passport-and-next-connect/lib/session.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { parse, serialize } from 'cookie'
import Iron from '@hapi/iron'

export default function session({ name, secret, cookie: cookieOpts }) {
return async (req, res, next) => {
const cookie = req.headers?.cookie ? parse(req.headers.cookie) : null
let unsealed
if (cookie?.[name]) {
try {
// the cookie needs to be unsealed using the password `secret`
unsealed = await Iron.unseal(cookie[name], secret, Iron.defaults)
} catch (e) {
// To cookie is invalid, do nothing
}
}

// Initialize the session
req.session = unsealed || {}

// We are proxying res.end to commit the session cookie
const oldEnd = res.end
res.end = async function resEndProxy(...args) {
if (res.finished || res.writableEnded || res.headersSent) return
// sealing the cookie to be sent to client
const sealed = await Iron.seal(req.session, secret, Iron.defaults)
res.setHeader('Set-Cookie', serialize(name, sealed, cookieOpts))
oldEnd.apply(this, args)
}

next()
}
}
28 changes: 28 additions & 0 deletions examples/with-passport-and-next-connect/middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import nextConnect from 'next-connect'
import passport from '../lib/passport'
import session from '../lib/session'

const auth = nextConnect()
.use(
session({
name: 'sess',
secret: 'some_not_random_password_that_is_at_least_32_characters', // This should be kept securely, preferably in env vars
cookie: {
maxAge: 60 * 60 * 8, // 8 hours,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
},
})
)
.use((req, res, next) => {
// Initialize mocked database
// Remove this after you add your own database
req.session.users = req.session.users || []
next()
})
.use(passport.initialize())
.use(passport.session())

export default auth
20 changes: 20 additions & 0 deletions examples/with-passport-and-next-connect/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "with-passport-and-next-connect",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@hapi/iron": "6.0.0",
"cookie": "0.4.0",
"next": "latest",
"next-connect": "^0.6.1",
"passport": "^0.4.1",
"passport-local": "^1.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"swr": "^0.1.18"
},
"license": "ISC"
}
15 changes: 15 additions & 0 deletions examples/with-passport-and-next-connect/pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Navbar from '../components/Navbar'
import '../styles.css'

export default function MyApp({ Component, pageProps }) {
return (
<>
<Navbar />
<main>
<div className="container">
<Component {...pageProps} />
</div>
</main>
</>
)
}
11 changes: 11 additions & 0 deletions examples/with-passport-and-next-connect/pages/api/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import nextConnect from 'next-connect'
import auth from '../../middleware/auth'
import passport from '../../lib/passport'

const handler = nextConnect()

handler.use(auth).post(passport.authenticate('local'), (req, res) => {
res.json({ user: req.user })
})

export default handler
11 changes: 11 additions & 0 deletions examples/with-passport-and-next-connect/pages/api/logout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import nextConnect from 'next-connect'
import auth from '../../middleware/auth'

const handler = nextConnect()

handler.use(auth).get((req, res) => {
req.logOut()
res.status(204).end()
})

export default handler
36 changes: 36 additions & 0 deletions examples/with-passport-and-next-connect/pages/api/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import nextConnect from 'next-connect'
import auth from '../../middleware/auth'
import { deleteUser, updateUserByUsername } from '../../lib/db'

const handler = nextConnect()

handler
.use(auth)
.get((req, res) => {
// You do not generally want to return the whole user object
// because it may contain sensitive field such as !!password!! Only return what needed
// const { name, username, favoriteColor } = req.user
// res.json({ user: { name, username, favoriteColor } })
res.json({ user: req.user })
})
.use((req, res, next) => {
// handlers after this (PUT, DELETE) all require an authenticated user
// This middleware to check if user is authenticated before continuing
if (!req.user) {
res.status(401).send('unauthenticated')
} else {
next()
}
})
.put((req, res) => {
const { name } = req.body
const user = updateUserByUsername(req, req.user.username, { name })
res.json({ user })
})
.delete((req, res) => {
deleteUser(req)
req.logOut()
res.status(204).end()
})

export default handler
38 changes: 38 additions & 0 deletions examples/with-passport-and-next-connect/pages/api/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import nextConnect from 'next-connect'
import auth from '../../middleware/auth'
import { getAllUsers, createUser, findUserByUsername } from '../../lib/db'

const handler = nextConnect()

handler
.use(auth)
.get((req, res) => {
// For demo purpose only. You will never have an endpoint which returns all users.
// Remove this in production
res.json({ users: getAllUsers(req) })
})
.post((req, res) => {
const { username, password, name } = req.body
if (!username || !password || !name) {
return res.status(400).send('Missing fields')
}
// Here you check if the username has already been used
const usernameExisted = !!findUserByUsername(req, username)
if (usernameExisted) {
return res.status(409).send('The username has already been used')
}
const user = { username, password, name }
// Security-wise, you must hash the password before saving it
// const hashedPass = await argon2.hash(password);
// const user = { username, password: hashedPass, name }
createUser(req, user)
req.logIn(user, err => {
if (err) throw err
// Log the signed up user in
res.status(201).json({
user,
})
})
})

export default handler
Loading