diff --git a/examples/with-cookie-auth/README.md b/examples/with-cookie-auth/README.md index c7c24f3b..662916ad 100644 --- a/examples/with-cookie-auth/README.md +++ b/examples/with-cookie-auth/README.md @@ -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) + # Example app utilizing cookie-based authentication ## 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 ``` -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 +cd api npm install 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)) ```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. -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. -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. diff --git a/examples/with-cookie-auth/api/index.js b/examples/with-cookie-auth/api/index.js new file mode 100644 index 00000000..3f6df9fb --- /dev/null +++ b/examples/with-cookie-auth/api/index.js @@ -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 diff --git a/examples/with-cookie-auth/api/package.json b/examples/with-cookie-auth/api/package.json index 176802b2..08a9ec58 100644 --- a/examples/with-cookie-auth/api/package.json +++ b/examples/with-cookie-auth/api/package.json @@ -12,7 +12,7 @@ }, "scripts": { "start": "micro", - "dev": "micro-dev" + "dev": "micro-dev . -p 3001" }, "keywords": [], "author": "", diff --git a/examples/with-cookie-auth/www/package.json b/examples/with-cookie-auth/www/package.json index 06d2f2ef..93a6343d 100644 --- a/examples/with-cookie-auth/www/package.json +++ b/examples/with-cookie-auth/www/package.json @@ -1,7 +1,7 @@ { "name": "with-cookie-auth", "scripts": { - "dev": "next", + "dev": "node server.js", "build": "next build", "start": "next start" }, @@ -12,5 +12,8 @@ "next-cookies": "^1.0.4", "react": "^16.7.0", "react-dom": "^16.7.0" + }, + "devDependencies": { + "http-proxy": "^1.17.0" } } diff --git a/examples/with-cookie-auth/www/pages/login.js b/examples/with-cookie-auth/www/pages/login.js index b9edc0e3..717643eb 100644 --- a/examples/with-cookie-auth/www/pages/login.js +++ b/examples/with-cookie-auth/www/pages/login.js @@ -1,12 +1,15 @@ import { Component } from 'react' +import fetch from 'isomorphic-unfetch' import Layout from '../components/layout' import { login } from '../utils/auth' class Login extends Component { static getInitialProps ({ req }) { + const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http' + const apiUrl = process.browser - ? `https://${window.location.host}/api/login.js` - : `https://${req.headers.host}/api/login.js` + ? `${protocol}://${window.location.host}/api/login.js` + : `${protocol}://${req.headers.host}/api/login.js` return { apiUrl } } @@ -27,9 +30,30 @@ class Login extends Component { event.preventDefault() const username = this.state.username 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 () { diff --git a/examples/with-cookie-auth/www/pages/profile.js b/examples/with-cookie-auth/www/pages/profile.js index 95ad4cc3..f29a605c 100644 --- a/examples/with-cookie-auth/www/pages/profile.js +++ b/examples/with-cookie-auth/www/pages/profile.js @@ -1,9 +1,10 @@ import Router from 'next/router' import fetch from 'isomorphic-unfetch' +import nextCookie from 'next-cookies' 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 return ( @@ -36,13 +37,15 @@ const Profile = withAuthSync(props => { `} ) -}) +} 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 - ? `https://${window.location.host}/api/profile.js` - : `https://${ctx.req.headers.host}/api/profile.js` + ? `${protocol}://${window.location.host}/api/profile.js` + : `${protocol}://${ctx.req.headers.host}/api/profile.js` const redirectOnError = () => process.browser @@ -70,4 +73,4 @@ Profile.getInitialProps = async ctx => { } } -export default Profile +export default withAuthSync(Profile) diff --git a/examples/with-cookie-auth/www/server.js b/examples/with-cookie-auth/www/server.js new file mode 100644 index 00000000..8916f81d --- /dev/null +++ b/examples/with-cookie-auth/www/server.js @@ -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') + }) +}) diff --git a/examples/with-cookie-auth/www/utils/auth.js b/examples/with-cookie-auth/www/utils/auth.js index 38b002ee..cba089ae 100644 --- a/examples/with-cookie-auth/www/utils/auth.js +++ b/examples/with-cookie-auth/www/utils/auth.js @@ -2,33 +2,10 @@ import { Component } from 'react' import Router from 'next/router' import nextCookie from 'next-cookies' import cookie from 'js-cookie' -import fetch from 'isomorphic-unfetch' -export const login = async ({ username, url }) => { - 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() - 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 login = async ({ token }) => { + cookie.set('token', token, { expires: 1 }) + Router.push('/profile') } export const logout = () => { @@ -38,13 +15,30 @@ export const logout = () => { Router.push('/login') } -export function withAuthSync (WrappedComponent) { - return class extends Component { +// Gets the display name of a JSX component for dev tools +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) { super(props) this.syncLogout = this.syncLogout.bind(this) } + componentDidMount () { window.addEventListener('storage', this.syncLogout) } @@ -65,9 +59,8 @@ export function withAuthSync (WrappedComponent) { return } } -} -export default ctx => { +export const auth = ctx => { const { token } = nextCookie(ctx) if (ctx.req && !token) {