1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Add setup to run example with cookie authentication locally (#6101)

* extract request login from auth

* add clarification that the monorepo is for deploy in Now only and fix typo

* Refactor HOC

- add authorization to HOC
- add displayName to HOC
- remove unnecessary `run`s in local routing
This commit is contained in:
Juan Olvera 2019-02-11 07:17:43 -06:00 committed by Tim Neutkens
parent 2ab1ae7f61
commit 80cb91ec87
8 changed files with 160 additions and 47 deletions

View file

@ -1,4 +1,5 @@
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-cookie-auth) [![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-cookie-auth)
# Example app utilizing cookie-based authentication # Example app utilizing cookie-based authentication
## How to use ## How to use
@ -21,13 +22,33 @@ curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2
cd with-cookie-auth cd with-cookie-auth
``` ```
Install it and run: ### Run locally
The repository is setup as a [monorepo](https://zeit.co/examples/monorepo/) so you can deploy it easily running `now` inside the project folder. However, you can't run it the same way locally (yet).
These files make it easier to run the application locally and aren't needed for production:
- `/api/index.js` runs the API server on port `3001` and imports the `login` and `profile` microservices.
- `/www/server.js` runs the Next.js app with a custom server proxying the authentication requests to the API server. We use this so we don't modify the logic on the application and we don't have to deal with CORS if we use domains while testing.
Install and run the API server:
```bash ```bash
cd api
npm install npm install
npm run dev npm run dev
``` ```
Then run the Next.js app:
```bash
cd ../www
npm install
npm run dev
```
### Deploy
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)) Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
```bash ```bash
@ -40,8 +61,8 @@ In this example, we authenticate users and store a token in a cookie. The exampl
This example is backend agnostic and uses [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic-unfetch) to do the API calls on the client and the server. This example is backend agnostic and uses [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic-unfetch) to do the API calls on the client and the server.
The repo includes a minimal passwordless backend built with [Micro](https://www.npmjs.com/package/micro) and it logs the user in with a GitHub username and saves the user id from the API call as token. The repo includes a minimal passwordless backend built with [Micro](https://www.npmjs.com/package/micro) that logs the user in with a GitHub username and saves the user id from the API call as token.
Session is syncronized across tabs. If you logout your session gets logged out on all the windows as well. We use the HOC `withAuthSync` for this. Session is syncronized across tabs. If you logout your session gets logged out on all the windows as well. We use the HOC `withAuthSync` for this.
The helper function `auth` helps to retrieve the token across pages and redirects the user if no token was found. The helper function `auth` helps to retrieve the token across pages and redirects the user if not token was found.

View file

@ -0,0 +1,20 @@
const { send } = require('micro')
const login = require('./login')
const profile = require('./profile')
const dev = async (req, res) => {
switch (req.url) {
case '/api/profile.js':
await profile(req, res)
break
case '/api/login.js':
await login(req, res)
break
default:
send(res, 404, '404. Not found.')
break
}
}
module.exports = dev

View file

@ -12,7 +12,7 @@
}, },
"scripts": { "scripts": {
"start": "micro", "start": "micro",
"dev": "micro-dev" "dev": "micro-dev . -p 3001"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",

View file

@ -1,7 +1,7 @@
{ {
"name": "with-cookie-auth", "name": "with-cookie-auth",
"scripts": { "scripts": {
"dev": "next", "dev": "node server.js",
"build": "next build", "build": "next build",
"start": "next start" "start": "next start"
}, },
@ -12,5 +12,8 @@
"next-cookies": "^1.0.4", "next-cookies": "^1.0.4",
"react": "^16.7.0", "react": "^16.7.0",
"react-dom": "^16.7.0" "react-dom": "^16.7.0"
},
"devDependencies": {
"http-proxy": "^1.17.0"
} }
} }

View file

@ -1,12 +1,15 @@
import { Component } from 'react' import { Component } from 'react'
import fetch from 'isomorphic-unfetch'
import Layout from '../components/layout' import Layout from '../components/layout'
import { login } from '../utils/auth' import { login } from '../utils/auth'
class Login extends Component { class Login extends Component {
static getInitialProps ({ req }) { static getInitialProps ({ req }) {
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'
const apiUrl = process.browser const apiUrl = process.browser
? `https://${window.location.host}/api/login.js` ? `${protocol}://${window.location.host}/api/login.js`
: `https://${req.headers.host}/api/login.js` : `${protocol}://${req.headers.host}/api/login.js`
return { apiUrl } return { apiUrl }
} }
@ -27,9 +30,30 @@ class Login extends Component {
event.preventDefault() event.preventDefault()
const username = this.state.username const username = this.state.username
const url = this.props.apiUrl const url = this.props.apiUrl
login({ username, url }).catch(() =>
this.setState({ error: 'Login failed.' }) try {
) const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
})
if (response.ok) {
const { token } = await response.json()
login({ token })
} else {
console.log('Login failed.')
// https://github.com/developit/unfetch#caveats
let error = new Error(response.statusText)
error.response = response
return Promise.reject(error)
}
} catch (error) {
console.error(
'You have an error in your code or there are Network issues.',
error
)
throw new Error(error)
}
} }
render () { render () {

View file

@ -1,9 +1,10 @@
import Router from 'next/router' import Router from 'next/router'
import fetch from 'isomorphic-unfetch' import fetch from 'isomorphic-unfetch'
import nextCookie from 'next-cookies'
import Layout from '../components/layout' import Layout from '../components/layout'
import auth, { withAuthSync } from '../utils/auth' import { withAuthSync } from '../utils/auth'
const Profile = withAuthSync(props => { const Profile = props => {
const { name, login, bio, avatarUrl } = props.data const { name, login, bio, avatarUrl } = props.data
return ( return (
@ -36,13 +37,15 @@ const Profile = withAuthSync(props => {
`}</style> `}</style>
</Layout> </Layout>
) )
}) }
Profile.getInitialProps = async ctx => { Profile.getInitialProps = async ctx => {
const token = auth(ctx) const { token } = nextCookie(ctx)
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'
const apiUrl = process.browser const apiUrl = process.browser
? `https://${window.location.host}/api/profile.js` ? `${protocol}://${window.location.host}/api/profile.js`
: `https://${ctx.req.headers.host}/api/profile.js` : `${protocol}://${ctx.req.headers.host}/api/profile.js`
const redirectOnError = () => const redirectOnError = () =>
process.browser process.browser
@ -70,4 +73,4 @@ Profile.getInitialProps = async ctx => {
} }
} }
export default Profile export default withAuthSync(Profile)

View file

@ -0,0 +1,49 @@
const { createServer } = require('http')
const httpProxy = require('http-proxy')
const { parse } = require('url')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
const proxy = httpProxy.createProxyServer()
const target = 'http://localhost:3001'
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true)
const { pathname, query } = parsedUrl
switch (pathname) {
case '/':
app.render(req, res, '/', query)
break
case '/login':
app.render(req, res, '/login', query)
break
case '/api/login.js':
proxy.web(req, res, { target }, error => {
console.log('Error!', error)
})
break
case '/profile':
app.render(req, res, '/profile', query)
break
case '/api/profile.js':
proxy.web(req, res, { target }, error => console.log('Error!', error))
break
default:
handle(req, res, parsedUrl)
break
}
}).listen(3000, err => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})

View file

@ -2,33 +2,10 @@ import { Component } from 'react'
import Router from 'next/router' import Router from 'next/router'
import nextCookie from 'next-cookies' import nextCookie from 'next-cookies'
import cookie from 'js-cookie' import cookie from 'js-cookie'
import fetch from 'isomorphic-unfetch'
export const login = async ({ username, url }) => { export const login = async ({ token }) => {
try { cookie.set('token', token, { expires: 1 })
const response = await fetch(url, { Router.push('/profile')
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
})
if (response.ok) {
const { token } = await response.json()
cookie.set('token', token, { expires: 1 })
Router.push('/profile')
} else {
console.log('Login failed.')
// https://github.com/developit/unfetch#caveats
let error = new Error(response.statusText)
error.response = response
return Promise.reject(error)
}
} catch (error) {
console.error(
'You have an error in your code or there are Network issues.',
error
)
throw new Error(error)
}
} }
export const logout = () => { export const logout = () => {
@ -38,13 +15,30 @@ export const logout = () => {
Router.push('/login') Router.push('/login')
} }
export function withAuthSync (WrappedComponent) { // Gets the display name of a JSX component for dev tools
return class extends Component { const getDisplayName = Component =>
Component.displayName || Component.name || 'Component'
export const withAuthSync = WrappedComponent =>
class extends Component {
static displayName = `withAuthSync(${getDisplayName(WrappedComponent)})`
static async getInitialProps (ctx) {
const token = auth(ctx)
const componentProps =
WrappedComponent.getInitialProps &&
(await WrappedComponent.getInitialProps(ctx))
return { ...componentProps, token }
}
constructor (props) { constructor (props) {
super(props) super(props)
this.syncLogout = this.syncLogout.bind(this) this.syncLogout = this.syncLogout.bind(this)
} }
componentDidMount () { componentDidMount () {
window.addEventListener('storage', this.syncLogout) window.addEventListener('storage', this.syncLogout)
} }
@ -65,9 +59,8 @@ export function withAuthSync (WrappedComponent) {
return <WrappedComponent {...this.props} /> return <WrappedComponent {...this.props} />
} }
} }
}
export default ctx => { export const auth = ctx => {
const { token } = nextCookie(ctx) const { token } = nextCookie(ctx)
if (ctx.req && !token) { if (ctx.req && !token) {