diff --git a/examples/with-cookie-auth/README.md b/examples/with-cookie-auth/README.md new file mode 100644 index 0000000000000..22d679a2b8140 --- /dev/null +++ b/examples/with-cookie-auth/README.md @@ -0,0 +1,43 @@ +[![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 + +### Using `create-next-app` + +Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: + +``` +npm i -g create-next-app +create-next-app --example with-cookie-auth with-cookie-auth-app +``` + +### Download manually + +Download the example [or clone the repo](https://github.com/zeit/next.js): + +```bash +curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-cookie-auth +cd with-cookie-auth +``` + +Install it and run: + +```bash +npm install +npm run dev +``` + +Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)) + +```bash +now +``` + +## The idea behind the example + +The goal is to authenticate users and store basic info safely in a signed cookie. +Certain pages can only be accessed by a logged-in user. + +> Use any email from https://jsonplaceholder.typicode.com/users to login, user website is the password. +> e.g. Lucio_Hettinger@annie.ca / demarco.info diff --git a/examples/with-cookie-auth/components/Layout.js b/examples/with-cookie-auth/components/Layout.js new file mode 100644 index 0000000000000..5501f786da6cf --- /dev/null +++ b/examples/with-cookie-auth/components/Layout.js @@ -0,0 +1,45 @@ +import React from 'react' +import Link from 'next/link' +import { processLogout } from '../lib/auth' + +const Layout = ({ auth, children }) => { + const { user = {} } = auth || {} + return ( +
+ Welcome, {user.name || 'Guest'}! + + {children} + +
+ ) +} + +export default Layout diff --git a/examples/with-cookie-auth/components/LoginForm.js b/examples/with-cookie-auth/components/LoginForm.js new file mode 100644 index 0000000000000..501f2f38a91c3 --- /dev/null +++ b/examples/with-cookie-auth/components/LoginForm.js @@ -0,0 +1,50 @@ +import React from 'react' +import Router from 'next/router' +import { processLogin } from '../lib/auth' + +class LoginForm extends React.PureComponent { + state = { + email: '', + password: '', + error: undefined + } + + onChange = ({ target: { name, value } }) => { + this.setState((state) => ({ + [name]: value + })) + } + + onSubmit = (e) => { + e.preventDefault() + const { email, password } = this.state + this.setState({ error: undefined }) + processLogin({ email, password }) + .then(() => Router.push('/profile')) + .catch(this.setError) + } + + setError = (err) => { + console.warn({err}) + const error = (err.response && err.response.data) || err.message + this.setState({ error }) + } + + render () { + const { email, password, error } = this.state + return ( +
+
+
+
+ +
+ {error && ( +
{error}
+ )} +
+ ) + } +} + +export default LoginForm diff --git a/examples/with-cookie-auth/lib/auth.js b/examples/with-cookie-auth/lib/auth.js new file mode 100644 index 0000000000000..93f84adc333f6 --- /dev/null +++ b/examples/with-cookie-auth/lib/auth.js @@ -0,0 +1,97 @@ +import Router from 'next/router' +import axios from 'axios' + +axios.defaults.withCredentials = true + +const WINDOW_USER_SCRIPT_VARIABLE = `__USER__` + +const decode = ({token}) => { + if (!token) { + return {} + } + const {email, type} = token || {} + return {user: {email, type}} +} + +export const getUserScript = (user) => { + const json = JSON.stringify(user) + return `${WINDOW_USER_SCRIPT_VARIABLE} = ${json};` +} + +export const getServerSideToken = (req) => { + const {signedCookies} = req + + if (!signedCookies) { + return {} + } + try { + return decode(signedCookies) + } catch (parseError) { + return {} + } +} + +export const getClientSideToken = () => { + if (typeof window !== 'undefined') { + const user = window[WINDOW_USER_SCRIPT_VARIABLE] || {} + return {user} + } + return {user: {}} +} + +const getRedirectPath = (userType) => { + switch (userType) { + case 'authenticated': + return '/profile' + default: + return '/login' + } +} + +const redirect = (res, path) => { + if (res) { + res.redirect(302, path) + res.finished = true + return {} + } + Router.replace(path) + return {} +} + +export const authInitialProps = (redirectIfAuth, secured) => async ({req, res}) => { + const auth = req ? getServerSideToken(req) : getClientSideToken() + const current = req ? req.url : window.location.pathname + const user = auth.user + const isAnonymous = !user || user.type !== 'authenticated' + if (secured && isAnonymous && current !== '/login') { + return redirect(res, '/login') + } + if (!isAnonymous && redirectIfAuth) { + const path = getRedirectPath(user.type) + if (current !== path) { + return redirect(res, path) + } + } + return {auth} +} + +export const getProfile = async () => { + const response = await axios.get('/api/profile') + return response.data +} + +export const processLogin = async ({email, password}) => { + const response = await axios.post('/api/login', {email, password}) + const {data} = response + if (typeof window !== 'undefined') { + window[WINDOW_USER_SCRIPT_VARIABLE] = data || {} + } +} + +export const processLogout = async () => { + if (typeof window !== 'undefined') { + window[WINDOW_USER_SCRIPT_VARIABLE] = {} + } + await axios.post('/api/logout') + Router.push('/login') +} diff --git a/examples/with-cookie-auth/package.json b/examples/with-cookie-auth/package.json new file mode 100644 index 0000000000000..f36468d67ed7a --- /dev/null +++ b/examples/with-cookie-auth/package.json @@ -0,0 +1,18 @@ +{ + "name": "with-cookie-auth", + "version": "1.0.0", + "scripts": { + "dev": "node server.js", + "build": "next build", + "start": "NODE_ENV=production node server.js" + }, + "dependencies": { + "axios": "^0.18.0", + "cookie-parser": "1.4.3", + "express": "^4.16.2", + "next": "latest", + "react": "^16.0.0", + "react-dom": "^16.0.0" + }, + "license": "ISC" +} diff --git a/examples/with-cookie-auth/pages/_document.js b/examples/with-cookie-auth/pages/_document.js new file mode 100644 index 0000000000000..e558b8e3e4080 --- /dev/null +++ b/examples/with-cookie-auth/pages/_document.js @@ -0,0 +1,24 @@ +import Document, { Head, Main, NextScript } from 'next/document' +import { getServerSideToken, getUserScript } from '../lib/auth' + +export default class extends Document { + static async getInitialProps (ctx) { + const props = await Document.getInitialProps(ctx) + const info = getServerSideToken(ctx.req) + return { ...props, ...info } + } + + render () { + const { user = {} } = this.props + return ( + + + +
+