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

Add an example for Fauna using cookie based auth (round 2) #9986

Merged
merged 18 commits into from Jan 15, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 15 additions & 11 deletions examples/with-cookie-auth-fauna/README.md
Expand Up @@ -6,7 +6,7 @@

Download [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) to bootstrap the example:

```
```bash
npm i -g create-next-app
create-next-app --example with-cookie-auth-fauna with-cookie-auth-fauna-app
```
Expand All @@ -22,18 +22,16 @@ cd with-cookie-auth-fauna

### Run locally

First, you'll need to create an account on [Fauna](https://fauna.com/), then you'll be able to create a database and add the following:
First, you'll need to create an account on [Fauna](https://fauna.com/), then follow these steps:

- `User` Collection
- `users_by_email` index
- server key
1. In the [FaunaDB Console](https://dashboard.fauna.com/), click "New Database". Name it whatever you like and click "Save".
2. Click "New Collection", name it `User`, leave "Create collection index" checked, and click "Save".
3. Now go to "Indexes" in the left sidebar, and click "New Index". Select the `User` collection, call it `users_by_email`, and in the "terms" field type `data.email`. Select the "Unique" checkbox and click "Save". This will create an index that allows looking up users by their email, which we will use to log a user in.
4. Next, go to "Security" in the sidebar, then click "New Key". Create a new key with the `Server` role, call it `server-key`, and click "Save". Your key's secret will be displayed, copy that value and paste it as the value for `FAUNA_SERVER_KEY` in the `.env` file at the project root. Keep this key safely as it has privileged access to your database.

For more information on how to do this, please refer to the [User Authentication Tutorial in Fauna](https://app.fauna.com/tutorials/authentication), or follow the steps below:
> For more information, read the [User Authentication Tutorial in Fauna](https://app.fauna.com/tutorials/authentication).

1. In the FaunaDB Console, click "New Database". Name it whatever you like and click save.
2. Click "New Collection", name it `User`, leave "Create collection index" checked, and save.
3. Now go to "Indexes" in the left sidebar, and click "New Index". Select the `User` collection, call it `users_by_email`, and in the "terms" field type `data.email`. Select the "Unique" checkbox and click save. This will create an index that allows looking up users by their email, which we will use to log a user in.
4. Next, go to "Security" in the sidebar, and create key with the `Server` role and call it `server-key`. Your key's secret will be displayed, copy that value and paste it as the value for `FAUNA_SERVER_KEY` in the `.env` file at the project root. Keep this key safely as it has privileged access to your database.
> **Add `.env` to `.gitignore`**, files with secrets should never be in the cloud, we have it here for the sake of the example.

Now, install it and run:

Expand All @@ -47,7 +45,13 @@ yarn dev

### Deploy

Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)):
We'll use [now](https://zeit.co/now) to deploy our app, first we need to add the server key as a secret using [now secrets](https://zeit.co/docs/v2/serverless-functions/env-and-secrets/?query=secrets#adding-secrets), like so:

```bash
now secrets add fauna-secret-key "ENTER YOUR FAUNA SERVER KEY"
```

Then deploy it to the cloud:

```bash
now
Expand Down
7 changes: 7 additions & 0 deletions examples/with-cookie-auth-fauna/now.json
@@ -0,0 +1,7 @@
{
"build": {
"env": {
"FAUNA_SERVER_KEY": "@fauna-secret-key"
}
}
}
3 changes: 1 addition & 2 deletions examples/with-cookie-auth-fauna/package.json
Expand Up @@ -6,12 +6,11 @@
"start": "next start"
},
"dependencies": {
"cookie": "^0.4.0",
"dotenv": "8.2.0",
"faunadb": "2.10.0",
"isomorphic-unfetch": "^3.0.0",
"js-cookie": "^2.2.0",
"next": "^9.0.1",
"cookie": "^0.4.0",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
Expand Down
10 changes: 5 additions & 5 deletions examples/with-cookie-auth-fauna/pages/api/login.js
Expand Up @@ -14,16 +14,16 @@ export default async (req, res) => {
password,
})
)

if (!loginRes.secret) {
throw new Error('No secret present in login query response.')
}
var cookieSerialized = serializeFaunaCookie(loginRes.secret)

const cookieSerialized = serializeFaunaCookie(loginRes.secret)

res.setHeader('Set-Cookie', cookieSerialized)
res.status(200).json({ email })
} catch (error) {
const { response } = error
return response
? res.status(response.status).json({ message: response.statusText })
: res.status(400).json({ message: error.message })
res.status(400).send(error.message)
}
}
5 changes: 2 additions & 3 deletions examples/with-cookie-auth-fauna/pages/api/logout.js
Expand Up @@ -7,8 +7,7 @@ export default async (req, res) => {
const faunaSecret = cookies[FAUNA_SECRET_COOKIE]
if (!faunaSecret) {
// Already logged out.
res.status(200).json({})
return
return res.status(200).end()
}
// Invalidate secret (ie. logout from Fauna).
await faunaClient(faunaSecret).query(q.Logout(false))
Expand All @@ -21,5 +20,5 @@ export default async (req, res) => {
path: '/',
})
res.setHeader('Set-Cookie', cookieSerialized)
res.status(200).json({})
res.status(200).end()
}
1 change: 1 addition & 0 deletions examples/with-cookie-auth-fauna/pages/api/profile.js
Expand Up @@ -10,6 +10,7 @@ export const profileApi = async faunaSecret => {
export default async (req, res) => {
const cookies = cookie.parse(req.headers.cookie ?? '')
const faunaSecret = cookies[FAUNA_SECRET_COOKIE]

if (!faunaSecret) {
return res.status(401).send('Auth cookie missing.')
}
Expand Down
26 changes: 15 additions & 11 deletions examples/with-cookie-auth-fauna/pages/api/signup.js
Expand Up @@ -9,36 +9,40 @@ export default async (req, res) => {
throw new Error('Email and password must be provided.')
}
console.log(`email: ${email} trying to create user.`)
let createRes

let user

try {
createRes = await serverClient.query(
user = await serverClient.query(
q.Create(q.Collection('User'), {
credentials: { password },
data: { email },
})
)
} catch (err) {
console.error('ERR', err)
} catch (error) {
console.error('Fauna create user error:', error)
throw new Error('User already exists.')
}
if (!createRes.ref) {

if (!user.ref) {
throw new Error('No ref present in create query response.')
}

const loginRes = await serverClient.query(
q.Login(createRes.ref, {
q.Login(user.ref, {
password,
})
)

if (!loginRes.secret) {
throw new Error('No secret present in login query response.')
}
var cookieSerialized = serializeFaunaCookie(loginRes.secret)

const cookieSerialized = serializeFaunaCookie(loginRes.secret)

res.setHeader('Set-Cookie', cookieSerialized)
res.status(200).json({ email })
} catch (error) {
const { response } = error
return response
? res.status(response.status).json({ message: response.statusText })
: res.status(400).json({ message: error.message })
res.status(400).send(error.message)
}
}
35 changes: 10 additions & 25 deletions examples/with-cookie-auth-fauna/pages/login.js
@@ -1,27 +1,21 @@
import React, { useState } from 'react'
import Router from 'next/router'
import Layout from '../components/layout'
import { login } from '../utils/auth'

const signin = async (email, password) => {
const url = '/api/login'
const response = await fetch(url, {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})

if (response.status === 200) {
const { email } = await response.json()
login({ email })
Router.push('/profile')
} else {
console.log('Login failed.')
// https://github.com/developit/unfetch#caveats
const { message } = await response.json()
let error = new Error(message || response.statusText)
throw error
if (response.status !== 200) {
throw new Error(await response.text())
}

const data = await response.json()

login({ email: data.email })
}

function Login() {
Expand All @@ -33,25 +27,16 @@ function Login() {

async function handleSubmit(event) {
event.preventDefault()
setUserData(Object.assign({}, userData, { error: '' }))
setUserData({ ...userData, error: '' })

const email = userData.email
const password = userData.password

try {
await signin(email, password)
} catch (error) {
console.error(
'You have an error in your code or there are Network issues.',
error
)

const { response } = error
setUserData(
Object.assign({}, userData, {
error: response ? response.statusText : error.message,
})
)
console.error(error)
setUserData({ ...userData, error: error.message })
}
}

Expand Down
30 changes: 16 additions & 14 deletions examples/with-cookie-auth-fauna/pages/profile.js
@@ -1,9 +1,9 @@
import React from 'react'
import Layout from '../components/layout'
import { withAuthSync } from '../utils/auth'
import cookie from 'cookie'
import Router from 'next/router'
import { withAuthSync } from '../utils/auth'
import { FAUNA_SECRET_COOKIE } from '../utils/fauna-auth'
import { profileApi } from './api/profile'
import Layout from '../components/layout'

const Profile = props => {
const { userId } = props
Expand All @@ -22,13 +22,14 @@ const Profile = props => {
}

Profile.getInitialProps = async ctx => {
if (ctx.req) {
const cookies = cookie.parse(ctx.req.headers.cookie ?? '')
if (typeof window === 'undefined') {
const { req, res } = ctx
const cookies = cookie.parse(req.headers.cookie ?? '')
const faunaSecret = cookies[FAUNA_SECRET_COOKIE]

if (!faunaSecret) {
ctx.res.writeHead(302, { Location: '/login' })
ctx.res.end()
res.writeHead(302, { Location: '/login' })
res.end()
return {}
}

Expand All @@ -37,17 +38,18 @@ Profile.getInitialProps = async ctx => {
return { userId: profileInfo }
}

const response = await fetch('/api/profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
const data = await response.json()
const response = await fetch('/api/profile')

if (response.status === 401) {
Router.push('/login')
return {}
}
if (response.status !== 200) {
throw new Error(data.message || response.statusText)
throw new Error(await response.text())
}

const data = await response.json()

return { userId: data.userId }
}

Expand Down
35 changes: 11 additions & 24 deletions examples/with-cookie-auth-fauna/pages/signup.js
@@ -1,8 +1,6 @@
import React, { useState } from 'react'
import fetch from 'isomorphic-unfetch'
import Layout from '../components/layout'
import { login } from '../utils/auth'
import Router from 'next/router'

function Signup() {
const [userData, setUserData] = useState({
Expand All @@ -13,39 +11,28 @@ function Signup() {

async function handleSubmit(event) {
event.preventDefault()
setUserData(Object.assign({}, userData, { error: '' }))
setUserData({ ...userData, error: '' })

const email = userData.email
const password = userData.password
const url = '/api/signup'

try {
const response = await fetch(url, {
const response = await fetch('/api/signup', {
method: 'POST',

headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (response.status === 200) {
const { email } = await response.json()
login({ email })
Router.push('/profile')
} else {
console.log('Signup failed.')
const { message } = await response.json()
let error = new Error(message ? message : response.statusText)
throw error

if (response.status !== 200) {
throw new Error(await response.text())
}

const data = await response.json()

login({ email: data.email })
} catch (error) {
console.error(
'You have an error in your code or there are Network issues.',
error
)
setUserData(
Object.assign({}, userData, {
error: error.message,
})
)
console.error(error)
setUserData({ ...userData, error: error.message })
}
}

Expand Down