Skip to content

Commit

Permalink
Add example with Magic authentication (#11810)
Browse files Browse the repository at this point in the history
* Add example with Magic and Passport.js

* Tweaked wording on README

* Fixed lint error

* Fixed prettier error

* Update examples/magic/README.md

Removed Download manually section from README

Co-Authored-By: Joe Haddad <[email protected]>

* Removed dependency on passport and express + cleanup

* Changed ZEIT brand to Vercel

* Updated readme instructions and secrets

* Renamed example

* Changed db comment

Co-authored-by: Joe Haddad <[email protected]>
Co-authored-by: Luis Alvarez <[email protected]>
  • Loading branch information
3 people authored Apr 22, 2020
1 parent 7d2ff81 commit 91adb86
Show file tree
Hide file tree
Showing 21 changed files with 549 additions and 0 deletions.
2 changes: 2 additions & 0 deletions examples/with-magic/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MAGIC_PUBLISHABLE_KEY=
MAGIC_SECRET_KEY=
2 changes: 2 additions & 0 deletions examples/with-magic/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
.now
53 changes: 53 additions & 0 deletions examples/with-magic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Magic Example

This example show how to use [Magic](https://magic.link) with Next.js. The example features cookie-based, passwordless authentication with email-based magic links.

The example shows how to do a login and logout; and to get the user info using a hook with [SWR](https://swr.now.sh).

A DB is not included. But you can add any DB you like!.

The login cookie is `httpOnly`, meaning it can only be accessed by the API, and it's encrypted using [@hapi/iron](https://hapi.dev/family/iron) for more security.

## Deploy your own

Deploy the example using [Vercel Now](https://vercel.com/docs/now-cli#commands/overview/basic-usage):

[![Deploy with Vercel Now](https://vercel.com/button)](https://vercel.com/new/project?template=https://github.com/zeit/next.js/tree/canary/examples/with-magic)

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

## Configuration

Login to the [Magic Dashboard](https://dashboard.magic.link/) and get the keys of your application

![Magic Dashboard](https://gblobscdn.gitbook.com/assets%2F-M1XNjqusnKyXZc7t7qQ%2F-M3HsSftOAghkNs-ttU3%2F-M3HsllfdwdDmeFXBK3U%2Fdashboard-pk.png?alt=media&token=4d6e7543-ae20-4355-951c-c6421b8f1b5f)

Next, copy the `.env.example` file in this directory to .env (which will be ignored by Git):

```bash
cp .env.example .env
```

Then set each variable on `.env`:

- `MAGIC_PUBLISHABLE_KEY` should look like `pk_test_abc` or `pk_live_ABC`
- `MAGIC_SECRET_KEY` should look like `sk_test_ABC` or `sk_live_ABC`

To deploy on Vercel, you need to set the environment variables with **Now Secrets** using [Now CLI](https://vercel.com/download) ([Documentation](https://vercel.com/docs/now-cli#commands/secrets)).

Install [Now CLI](https://vercel.com/download), log in to your account from the CLI, and run the following commands to add the environment variables. Replace `<MAGIC_PUBLISHABLE_KEY>` and `<MAGIC_SECRET_KEY>` with the corresponding strings in `.env`:

```bash
now secrets add next_example_magic_publishable_key <MAGIC_PUBLISHABLE_KEY>
now secrets add next_example_magic_secret_key <MAGIC_SECRET_KEY>
```
56 changes: 56 additions & 0 deletions examples/with-magic/components/form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const Form = ({ errorMessage, onSubmit }) => (
<form onSubmit={onSubmit}>
<label>
<span>Email</span>
<input type="email" name="email" required />
</label>

<div className="submit">
<button type="submit">Sign Up / Login</button>
</div>

{errorMessage && <p className="error">{errorMessage}</p>}

<style jsx>{`
form,
label {
display: flex;
flex-flow: column;
}
label > span {
font-weight: 600;
}
input {
padding: 8px;
margin: 0.3rem 0 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.submit {
display: flex;
justify-content: flex-end;
align-items: center;
justify-content: space-between;
}
.submit > a {
text-decoration: none;
}
.submit > button {
padding: 0.5rem 1rem;
cursor: pointer;
background: #fff;
border: 1px solid #ccc;
border-radius: 4px;
}
.submit > button:hover {
border-color: #888;
}
.error {
color: brown;
margin: 1rem 0 0;
}
`}</style>
</form>
)

export default Form
67 changes: 67 additions & 0 deletions examples/with-magic/components/header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Link from 'next/link'
import { useUser } from '../lib/hooks'

const Header = () => {
const user = useUser()

return (
<header>
<nav>
<ul>
<li>
<Link href="/">
<a>Home</a>
</Link>
</li>
{user ? (
<>
<li>
<Link href="/profile">
<a>Profile</a>
</Link>
</li>
<li>
<a href="/api/logout">Logout</a>
</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;
}
header {
color: #fff;
background-color: #333;
}
`}</style>
</header>
)
}

export default Header
57 changes: 57 additions & 0 deletions examples/with-magic/components/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Head from 'next/head'
import Header from './header'

const Layout = props => (
<>
<Head>
<title>Magic</title>
<link rel="icon" href="/favicon.ico" />
</Head>

<Header />

<main>
<div className="container">{props.children}</div>
</main>

<footer>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by <img src="/vercel.svg" alt="Vercel Logo" />
</a>
</footer>

<style jsx global>{`
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
color: #333;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, Noto Sans, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
.container {
max-width: 42rem;
margin: 0 auto;
padding: 2rem 1.25rem;
}
footer {
width: 100%;
height: 100px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
}
`}</style>
</>
)

export default Layout
39 changes: 39 additions & 0 deletions examples/with-magic/lib/auth-cookies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { serialize, parse } from 'cookie'

const TOKEN_NAME = 'token'
const MAX_AGE = 60 * 60 * 8 // 8 hours

export function setTokenCookie(res, token) {
const cookie = serialize(TOKEN_NAME, token, {
maxAge: MAX_AGE,
expires: new Date(Date.now() + MAX_AGE * 1000),
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
})
res.setHeader('Set-Cookie', cookie)
}

export function removeTokenCookie(res) {
const cookie = serialize(TOKEN_NAME, '', {
maxAge: -1,
path: '/',
})

res.setHeader('Set-Cookie', cookie)
}

export function parseCookies(req) {
// For API Routes we don't need to parse the cookies.
if (req.cookies) return req.cookies

// For pages we do need to parse the cookies.
const cookie = req.headers?.cookie
return parse(cookie || '')
}

export function getTokenCookie(req) {
const cookies = parseCookies(req)
return cookies[TOKEN_NAME]
}
31 changes: 31 additions & 0 deletions examples/with-magic/lib/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect } from 'react'
import Router from 'next/router'
import useSWR from 'swr'

const fetcher = url =>
fetch(url)
.then(r => r.json())
.then(data => {
return { user: data?.user || null }
})

export function useUser({ redirectTo, redirectIfFound } = {}) {
const { data, error } = useSWR('/api/user', fetcher)
const user = data?.user
const finished = Boolean(data)
const hasUser = Boolean(user)

useEffect(() => {
if (!redirectTo || !finished) return
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !hasUser) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && hasUser)
) {
Router.push(redirectTo)
}
}, [redirectTo, redirectIfFound, finished, hasUser])

return error ? null : user
}
14 changes: 14 additions & 0 deletions examples/with-magic/lib/iron.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Iron from '@hapi/iron'
import { getTokenCookie } from './auth-cookies'

// Use an environment variable here instead of a hardcoded value for production
const TOKEN_SECRET = 'this-is-a-secret-value-with-at-least-32-characters'

export function encryptSession(session) {
return Iron.seal(session, TOKEN_SECRET, Iron.defaults)
}

export async function getSession(req) {
const token = getTokenCookie(req)
return token && Iron.unseal(token, TOKEN_SECRET, Iron.defaults)
}
3 changes: 3 additions & 0 deletions examples/with-magic/lib/magic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { Magic } = require('@magic-sdk/admin')

export const magic = new Magic(process.env.MAGIC_SECRET_KEY)
16 changes: 16 additions & 0 deletions examples/with-magic/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const nextEnv = require('next-env')
const dotenvLoad = require('dotenv-load')

dotenvLoad()

const withNextEnv = nextEnv()

module.exports = withNextEnv({
env: {
MAGIC_PUBLISHABLE_KEY: process.env.MAGIC_PUBLISHABLE_KEY,
MAGIC_SECRET_KEY: process.env.MAGIC_SECRET_KEY,
},
devIndicators: {
autoPrerender: false,
},
})
12 changes: 12 additions & 0 deletions examples/with-magic/now.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"env": {
"MAGIC_PUBLISHABLE_KEY": "@next_example_magic_publishable_key",
"MAGIC_SECRET_KEY": "@next_example_magic_secret_key"
},
"build": {
"env": {
"MAGIC_PUBLISHABLE_KEY": "@next_example_magic_publishable_key",
"MAGIC_SECRET_KEY": "@next_example_magic_secret_key"
}
}
}
21 changes: 21 additions & 0 deletions examples/with-magic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "with-magic",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@hapi/iron": "6.0.0",
"@magic-sdk/admin": "1.0.0",
"cookie": "0.4.0",
"dotenv-load": "^2.0.0",
"magic-sdk": "1.0.1",
"next": "latest",
"next-env": "^1.1.1",
"react": "latest",
"react-dom": "latest",
"swr": "0.1.16"
},
"license": "ISC"
}
Loading

0 comments on commit 91adb86

Please sign in to comment.