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

Added with-cookie-auth-redux example #4011

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions examples/with-cookie-auth-redux/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[![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-redux)

# With Cookie Authentication and Redux example

## 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-redux with-cookie-auth-redux-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-redux
cd with-cookie-auth-redux
```

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

This example adds cookie based authentication to a Next.js application. It is based from
the [redux example](https://github.com/zeit/next.js/tree/canary/examples/with-redux-wrapper) and
uses the redux store to maintain the current logged in user.

The first time the page loads, it calls an `/api/whoami` API from the server side with the session cookie.
If the user is logged in, the API returns the user data and populates the redux store.

When the application is ready on the client side, the pages must use the `withAuth` higher order component
to automatically make themselves private. If anonymous user navigates from a public page to a private page, he's
redirected to a login page.

`withAuth` HOC has an optional "permissions" parameter if you want to fine tune the user access.

An special `PUBLIC` permission is required to make a public page knows about the current logged in user.

For simplicity and readability, Reducers, Actions, and Store creators are all in the same file: store.js

No styles were used for this example.
113 changes: 113 additions & 0 deletions examples/with-cookie-auth-redux/components/withAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { Component } from 'react'
import Router from 'next/router'
import { whoAmI } from '../store'
export const PUBLIC = 'PUBLIC'

/**
* Higher order component for Next.js `pages` components.
*
* NOTE: depends of redux store. So you must use the `withRedux` HOC before this one.
*
* Example:
*
* ```
* export default withRedux(initStore, mapStateToProps)(
* withAuth([PUBLIC])(MyPage)
* )
* ```
*
* Or using redux compose function:
*
* ```
* export default compose(
* withRedux(initStore, mapStateToProps),
* withAuth()
* )(Private)
* ```
*
* It reads the user from the redux store or calls whoami API to verify current logged in user.
*
* To make a page public you have to pass PUBLIC as an element of the `permissions` parameter.
* This is required to be able to show current logged in user from the first server render.
*
* @param permissions: array of permissions required to render this page. Use PUBLIC to make the page public.
* @returns function(ChildComponent) React component to be wrapped. Must be a `page` component.
*/
export default (permissions = []) => ChildComponent => class withAuth extends Component {
static redirectToLogin (context) {
const { isServer, req, res } = context
if (isServer) {
res.writeHead(302, { Location: `/login?next=${req.originalUrl}` })
res.end()
} else {
Router.push(`/login?next=${context.asPath}`)
}
}

static redirectTo404 (context) {
const { isServer, res } = context
if (isServer) {
res.writeHead(302, { Location: '/notfound' })
res.end()
} else {
Router.push('/notfound')
}
}

static userHasPermission (user) {
const userGroups = user.groups || []
let userHasPerm = true
// go here only if we have specific permission requirements
if (permissions.length > 0) {
// will deny perm if user is missing at least one permission
for (let i = 0, l = permissions.length; i < l; i++) {
if (userGroups.indexOf(permissions[i]) === -1) {
userHasPerm = false
break
}
}
}
return userHasPerm
}

static async getInitialProps (context) {
// public page passes the permission `PUBLIC` to this function
const isPublicPage = permissions.indexOf(PUBLIC) !== -1
const { isServer, store, req } = context
let user = null

if (isServer) {
// happens on page first load
const { cookie } = req.headers
user = await store.dispatch(whoAmI(cookie))
} else {
// happens on client side navigation
user = store.getState().user
}

if (user) {
// mean user is logged in so we verify permissions
if (!isPublicPage) {
if (!this.userHasPermission(user)) {
this.redirectTo404(context)
}
}
} else {
// anonymous user
if (!isPublicPage) {
this.redirectToLogin(context)
}
}

if (typeof ChildComponent.getInitialProps === 'function') {
const initProps = await ChildComponent.getInitialProps(context)
return initProps
}

return {}
}

render () {
return <ChildComponent {...this.props} />
}
}
24 changes: 24 additions & 0 deletions examples/with-cookie-auth-redux/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "with-cookie-auth-redux",
"version": "1.0.0",
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"axios": "^0.18.0",
"body-parser": "^1.18.2",
"cookie-parser": "^1.4.3",
"express": "^4.16.3",
"next": "latest",
"next-redux-wrapper": "^1.0.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-redux": "^5.0.1",
"redux": "^3.6.0",
"redux-devtools-extension": "^2.13.2",
"redux-thunk": "^2.1.0"
},
"license": "ISC"
}
50 changes: 50 additions & 0 deletions examples/with-cookie-auth-redux/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react'
import Link from 'next/link'
import withRedux from 'next-redux-wrapper'
import { compose } from 'redux'
import { initStore, logout } from '../store'
import withAuth, { PUBLIC } from '../components/withAuth'

class Index extends React.Component {
handleLogout = (e) => {
e.preventDefault()
const { dispatch } = this.props
dispatch(logout())
}

render () {
const { user } = this.props
const name = user ? `${user.firstName} ${user.lastName}` : 'Anonymous'
return (
<div>
<h1>Hello {name}!</h1>
<div>
<Link href='/private'>
<a>Link to a private page</a>
</Link>
</div>
<div>
<Link href='/private-perm-required'>
<a>Link to a private page with specific permission requirement</a>
</Link>
</div>
{ user === null
? <Link href='/login'>
<a>Login</a>
</Link>
: <a href='/logout' onClick={this.handleLogout}>Logout</a> }
</div>
)
}
}

const mapStateToProps = (state) => {
return {
user: state.user
}
}

export default compose(
withRedux(initStore, mapStateToProps),
withAuth([PUBLIC])
)(Index)
55 changes: 55 additions & 0 deletions examples/with-cookie-auth-redux/pages/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { Component } from 'react'
import withRedux from 'next-redux-wrapper'
import { initStore, login } from '../store'

class Login extends Component {
state = {
username: '',
password: '',
errorMessage: ''
}

handleOnChange = (e) => {
this.setState({
[e.target.name]: e.target.value
})
}

handleLoginSubmit = (e) => {
e.preventDefault()
const { dispatch } = this.props
const payload = {
username: this.state.username,
password: this.state.password
}
dispatch(login(payload))
.catch(err => {
this.setState({errorMessage: err.response.data.message})
})
}

render () {
return (
<div>
<form onSubmit={this.handleLoginSubmit}>
<div>
<label>Username</label>
<input type='text' name='username' onChange={this.handleOnChange} />
</div>
<div>
<label>Password</label>
<input type='password' name='password' onChange={this.handleOnChange} />
</div>
<div>
<button>Login</button>
</div>
<small>Username: "user@example.com", password: "changeme"</small>
<br />
<small>Staff username: "staff@example.com", password: "changeme"</small>
</form>
</div>
)
}
}

export default withRedux(initStore)(Login)
29 changes: 29 additions & 0 deletions examples/with-cookie-auth-redux/pages/private-perm-required.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react'
import withRedux from 'next-redux-wrapper'
import { compose } from 'redux'
import { initStore } from '../store'
import withAuth from '../components/withAuth'

class PrivatePermRequired extends React.Component {
render () {
const { user } = this.props
const name = `${user.firstName} ${user.lastName}`
return (
<div>
<h1>Hello {name}!</h1>
<p>This content is for "STAFF" users only.</p>
</div>
)
}
}

const mapStateToProps = (state) => {
return {
user: state.user
}
}

export default compose(
withRedux(initStore, mapStateToProps),
withAuth(['STAFF'])
)(PrivatePermRequired)
29 changes: 29 additions & 0 deletions examples/with-cookie-auth-redux/pages/private.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react'
import withRedux from 'next-redux-wrapper'
import { compose } from 'redux'
import { initStore } from '../store'
import withAuth from '../components/withAuth'

class Private extends React.Component {
render () {
const { user } = this.props
const name = `${user.firstName} ${user.lastName}`
return (
<div>
<h1>Hello {name}!</h1>
<p>This content is available for logged in users only.</p>
</div>
)
}
}

const mapStateToProps = (state) => {
return {
user: state.user
}
}

export default compose(
withRedux(initStore, mapStateToProps),
withAuth()
)(Private)
Loading