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 8 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
44 changes: 44 additions & 0 deletions examples/with-next-connect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# User CRUD using 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). It features cookie based authentication using [Passport.js](http://www.passportjs.org/).

The example shows how to do a login and logout. It also allows authenticated user to create, edit, and remove posts. It utilizes [SWR](https://swr.now.sh/).

## 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-next-connect)

## How to use

### Using `create-next-app`

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-next-connect with-next-connect-app
# or
yarn create next-app --example with-next-connect with-next-connect-app
```

### Download manually

Download the example:

```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-next-connect
cd with-next-connect
```

Install it and run:

```bash
npm install
npm run dev
# or
yarn
yarn dev
```

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)).
79 changes: 79 additions & 0 deletions examples/with-next-connect/assets/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
*,
:after,
:before {
box-sizing: border-box;
}

a {
text-decoration: none !important;
cursor: pointer;
color: #0070f3;
}

a:hover {
color: #0366d6;
}

body {
margin: 0;
padding: 0;
color: #111;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
}

main {
padding: 1rem;
max-width: 1040px;
margin: 0 auto;
}

label {
display: flex;
margin-bottom: 0.5rem;
align-items: center;
width: 100%;
}

form {
margin-bottom: 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

input,
textarea {
font-family: monospace;
flex: 1 1 0%;
margin-left: 0.5rem;
box-shadow: none;
width: 100%;
color: #000;
background-color: transparent;
border: 1px solid #d8d8d8;
border-radius: 5px;
outline: 0px;
padding: 10px 25px;
}

button {
display: block;
margin-bottom: 0.5rem;
color: #fff;
border-radius: 5px;
border: none;
background-color: #000;
cursor: pointer;
transition: all 0.2s ease 0s;
padding: 10px 25px;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
}

button:hover,
button:active {
transform: translate3d(0px, -1px, 0px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
67 changes: 67 additions & 0 deletions examples/with-next-connect/components/Layout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Link from 'next/link'
import { useUser } from '../lib/hooks'

function Navbar() {
const [user, { mutate }] = useUser()
async function handleLogout() {
await fetch('/api/logout')
mutate(null)
}

return (
<>
<style jsx>{`
nav {
max-width: 1040px;
margin: auto;
padding: 1rem 2rem;
border-bottom: 1px solid #d8d8d8;
}
nav div {
float: right;
}
nav div a {
font-size: 0.9rem;
margin-left: 1rem;
}
nav h1 {
font-size: 1rem;
color: #444;
margin: 0;
font-weight: 700;
float: left;
}
nav:after {
content: '';
clear: both;
display: table;
}
`}</style>
<nav>
<Link href="/">
<a>Next.js CRUD</a>
</Link>
<div>
{!user ? (
<Link href="/login">
<a>Sign in</a>
</Link>
) : (
<a role="button" onClick={handleLogout}>
Logout
</a>
)}
</div>
</nav>
</>
)
}

export default function Layout({ children }) {
return (
<>
<Navbar />
<main>{children}</main>
</>
)
}
15 changes: 15 additions & 0 deletions examples/with-next-connect/lib/hooks.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import useSWR from 'swr'

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

export function usePost() {
const { data, mutate } = useSWR('/api/posts', fetcher)
const posts = data?.posts
return [posts, { mutate }]
}

export function useUser() {
const { data, mutate } = useSWR('/api/user', fetcher)
const user = data?.user
return [user, { mutate }]
}
32 changes: 32 additions & 0 deletions examples/with-next-connect/lib/passport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import passport from 'passport'
import LocalStrategy from 'passport-local'

passport.serializeUser(function(user, done) {
// In practice, we do not serialize the whole user object but just the id
// done(null, user.id)
done(null, user)
})

passport.deserializeUser(function(id, done) {
// In practice, id will just be an id. In this case, it is the whole user object as shown in passport.serializeUser
// It is to used to query the database
// db.findUserById(id).then((user) => {
// done(null, user);
// });
done(null, id)
})

passport.use(
new LocalStrategy((username, password, done) => {
// Here you should lookup for the user in your DB and compare the password
if (password === 'hackme') {
// return the user if the password is correct
done(null, { username })
} else {
// return null if the password is incorrect
done(null, null)
}
})
)

export default passport
15 changes: 15 additions & 0 deletions examples/with-next-connect/middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import cookieSession from 'cookie-session'
import nextConnect from 'next-connect'
import passport from '../lib/passport'

const auth = nextConnect()
.use(
cookieSession({
name: 'session',
keys: ['hackme'],
})
)
.use(passport.initialize())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait does this mean Passport doesn't have to be used with Express? That's very interesting 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not at all. It can totally be used with good ol' connect, except passport uses res.redirect helper which is only available in express.

next-connect is pretty much connect (hence its name), but with router, sub-application. Yet, it is even 10 times smaller than connect in install size.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty nice, in that case the with-passport example deserves an update, I want to get rid of Express for the smallest thing in the world!, that can work with the auth plugins for passport like google, github, etc, what do you think is the best alternative?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe our guide in https://nextjs.org/docs/api-routes/api-middlewares will do just fine. Can I give it a try? 😄

.use(passport.session())

export default auth
20 changes: 20 additions & 0 deletions examples/with-next-connect/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "with-next-connect",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"license": "MIT",
"dependencies": {
"cookie-session": "1.4.0",
"nanoid": "2.1.11",
"next": "9.3.1",
"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"
}
}
10 changes: 10 additions & 0 deletions examples/with-next-connect/pages/_app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Layout from '../components/Layout'
import '../assets/style.css'

export default function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
15 changes: 15 additions & 0 deletions examples/with-next-connect/pages/api/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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) => {
if (req.user) {
// For demo purpose only. See pages/api/posts/
req.session.posts = []
}
res.json({ user: req.user })
})

export default handler
11 changes: 11 additions & 0 deletions examples/with-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
35 changes: 35 additions & 0 deletions examples/with-next-connect/pages/api/posts/[id].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import nextConnect from 'next-connect'
import auth from '../../../middleware/auth'

const handler = nextConnect()

handler
.use(auth)
.put((req, res) => {
if (!req.user) {
res.status(401).send('unauthenticated')
return
}
const { id } = req.query
const { content } = req.body
// Here you will edit the post in the database
// db.updatePostByIdAndUserId(id, req.user.id, content);
// We will use req.session for demo purpose
const editingPost = req.session.posts.find(post => post.id === id)
editingPost.content = content
res.json({ posts: req.session.posts })
})
.delete((req, res) => {
if (!req.user) {
res.status(401).send('unauthenticated')
return
}
const { id } = req.query
// Here you will edit the post in the database
// db.deletePostByIdAndUserId(id, req.user.id, content);
// We will use req.session for demo purpose
req.session.posts = req.session.posts.filter(post => post.id !== id)
res.json({ posts: req.session.posts })
})

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

const handler = nextConnect()

handler
.use(auth)
.get((req, res) => {
if (!req.user) res.json({ posts: [] })
// Here you will read all posts to the database
// db.findPostsByUserId(req.user.id, post)
// We will use req.session for demo purpose
res.json({ posts: req.session.posts })
})
.post((req, res) => {
const { content } = req.body
if (!req.user) {
res.status(401).send('unauthenticated')
return
}
const post = {
id: nanoid(),
content,
}
// Here you will add the posts to the database
// db.createPost(req.user.id, post)
// We will use req.session for demo purpose
req.session.posts = req.session.posts.concat([post])
res.json({ posts: req.session.posts })
})

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

const handler = nextConnect()
handler.use(auth).get((req, res) => {
res.json({ user: req.user })
})

export default handler
Loading