56 | )
57 | }
58 | }
59 |
60 | export default withAuth(withStyles(styles, { withTheme: true })(Dashboard));
--------------------------------------------------------------------------------
/README.nextjs.redux.md:
--------------------------------------------------------------------------------
1 | [](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-redux)
2 |
3 | # Redux example
4 |
5 | ## How to use
6 |
7 | ### Using `create-next-app`
8 |
9 | Execute [`create-next-app`](https://github.com/segmentio/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example:
10 |
11 | ```bash
12 | npx create-next-app --example with-redux with-redux-app
13 | # or
14 | yarn create next-app --example with-redux with-redux-app
15 | ```
16 |
17 | ### Download manually
18 |
19 | Download the example [or clone the repo](https://github.com/zeit/next.js):
20 |
21 | ```bash
22 | curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-redux
23 | cd with-redux
24 | ```
25 |
26 | Install it and run:
27 |
28 | ```bash
29 | npm install
30 | npm run dev
31 | # or
32 | yarn
33 | yarn dev
34 | ```
35 |
36 | Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
37 |
38 | ```bash
39 | now
40 | ```
41 |
42 | ## The idea behind the example
43 |
44 | This example shows how to integrate Redux in Next.js.
45 |
46 | Usually splitting your app state into `pages` feels natural but sometimes you'll want to have global state for your app. This is an example on how you can use redux that also works with Next.js's universal rendering approach.
47 |
48 | In the first example we are going to display a digital clock that updates every second. The first render is happening in the server and then the browser will take over. To illustrate this, the server rendered clock will have a different background color (black) than the client one (grey).
49 |
50 | The Redux `Provider` is implemented in `pages/_app.js`. Since the `MyApp` component is wrapped in `withReduxStore` the redux store will be automatically initialized and provided to `MyApp`, which in turn passes it off to `react-redux`'s `Provider` component.
51 |
52 | All pages have access to the redux store using `connect` from `react-redux`.
53 |
54 | On the server side every request initializes a new store, because otherwise different user data can be mixed up. On the client side the same store is used, even between page changes.
55 |
56 | The example under `components/counter.js`, shows a simple incremental counter implementing a common Redux pattern of mapping state to props. Again, the first render is happening in the server and instead of starting the count at 0, it will dispatch an action in redux that starts the count at 1. This continues to highlight how each navigation triggers a server render first and then a client render when switching pages on the client side
57 |
58 | For simplicity and readability, Reducers, Actions, and Store creators are all in the same file: `store.js`
59 |
--------------------------------------------------------------------------------
/utils/AuthService.js:
--------------------------------------------------------------------------------
1 | export default class AuthService {
2 | constructor(domain) {
3 | this.domain = domain || 'http://localhost'
4 | this.fetch = this.fetch.bind(this)
5 | this.login = this.login.bind(this)
6 | this.getProfile = this.getProfile.bind(this)
7 | }
8 |
9 | login(email, password) {
10 | // Get a token
11 | return this.fetch(`${this.domain}/api/token`, {
12 | method: 'POST',
13 | body: JSON.stringify({
14 | email,
15 | password
16 | })
17 | }).then(res => {
18 | this.setToken(res)
19 | return this.fetch(`${this.domain}/api/user`, {
20 | method: 'GET'
21 | })
22 | }).then(res => {
23 | this.setProfile(res)
24 | return Promise.resolve(res)
25 | })
26 | }
27 |
28 | loggedIn() {
29 | // Checks if there is a saved token and it's still valid
30 | const token = this.getToken()
31 | // return !!token && !isTokenExpired(token) // handwaiving here
32 | return !!token // handwaiving here
33 | }
34 |
35 | setProfile(profile) {
36 | // Saves profile data to localStorage
37 | localStorage.setItem('profile', JSON.stringify(profile))
38 | }
39 |
40 | getProfile() {
41 | // Retrieves the profile data from localStorage
42 | const profile = localStorage.getItem('profile')
43 | return profile ? JSON.parse(localStorage.profile) : {}
44 | }
45 |
46 | setToken(tokenResponse) {
47 | // Saves user token to localStorage
48 | localStorage.setItem('id_token', tokenResponse.accessToken)
49 | localStorage.setItem('token', tokenResponse.token)
50 | }
51 |
52 | getToken() {
53 | // Retrieves the user token from localStorage
54 | return localStorage.getItem('id_token')
55 | }
56 |
57 | logout() {
58 | // Clear user token and profile data from localStorage
59 | localStorage.removeItem('id_token');
60 | localStorage.removeItem('profile');
61 | }
62 |
63 | _checkStatus(response) {
64 | // raises an error in case response status is not a success
65 | if (response.status >= 200 && response.status < 300) {
66 | return response
67 | } else {
68 | var error = new Error(response.statusText)
69 | error.response = response
70 | throw error
71 | }
72 | }
73 |
74 | fetch(url, options) {
75 | // performs api calls sending the required authentication headers
76 | const headers = {
77 | 'Accept': 'application/json',
78 | 'Content-Type': 'application/json'
79 | }
80 |
81 | if (this.loggedIn()) {
82 | headers['Authorization'] = 'Bearer ' + this.getToken()
83 | }
84 |
85 | return fetch(url, {
86 | headers,
87 | ...options
88 | })
89 | .then(this._checkStatus)
90 | .then(response => response.json())
91 | }
92 | }
--------------------------------------------------------------------------------
/pages/events/manage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Link from 'next/link'
3 | import Api from '../../utils/SeshSourceApi'
4 | import withAuth from '../../utils/withAuth'
5 | import fetch from 'isomorphic-unfetch'
6 | import { withStyles } from '@material-ui/core/styles';
7 | import Paper from '@material-ui/core/Paper';
8 | import Typography from '@material-ui/core/Typography';
9 | import TextField from '@material-ui/core/TextField';
10 | import Button from '@material-ui/core/Button';
11 | import List from '@material-ui/core/List';
12 | import ListItem from '@material-ui/core/ListItem';
13 | import ListItemText from '@material-ui/core/ListItemText';
14 |
15 | import Header from '../../containers/header';
16 |
17 | import '../../assets/css/seshsource.css'
18 |
19 | const styles = theme => ({
20 | root: {
21 | flexGrow: 1,
22 | zIndex: 1,
23 | position: 'relative',
24 | display: 'flex',
25 | },
26 | toolbar: {
27 | display: 'flex',
28 | alignItems: 'center',
29 | justifyContent: 'flex-end',
30 | padding: '0 8px',
31 | ...theme.mixins.toolbar,
32 | },
33 | content: {
34 | flexGrow: 1,
35 | backgroundColor: theme.palette.background.default,
36 | padding: theme.spacing.unit * 3,
37 | },
38 | paper: {
39 | ...theme.mixins.gutters(),
40 | paddingTop: theme.spacing.unit * 2,
41 | paddingBottom: theme.spacing.unit * 2,
42 | },
43 | });
44 |
45 | class CreateEvent extends React.Component {
46 | static async getInitialProps({ req }) {
47 | let query = await fetch('http://localhost/api/events')
48 | let events = await query.json()
49 |
50 | return {
51 | events: events
52 | }
53 | }
54 |
55 | render() {
56 | const { classes, theme, loggedIn, events } = this.props;
57 | const user = this.props.auth.getProfile()
58 |
59 | const list = events.data.map((event) => {
60 | let url = '/events/' + event.slug;
61 | console.log(url);
62 | return (
63 |
64 |
65 |
66 |
67 |
68 | )
69 | })
70 |
71 | return (
72 |
73 |
74 |
75 |
76 |
77 |
78 | Manage Events
79 |
80 | {list &&
81 |
82 | { list }
83 |
84 | }
85 |
86 | {'You think water moves fast? You should see ice.'}
87 |
88 |
89 | )
90 | }
91 | }
92 |
93 | export default withAuth(withStyles(styles, { withTheme: true })(CreateEvent));
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NextJS Redux Authentication Boilerplate
2 |
3 | Shows how to use NextJS with Redux to create an authenticated web app that uses an OAuth2 API. This is essentially a fork of the NextJS Redux example, with `redux-persist` and authentication implemented.
4 |
5 | > There is some minimal use of Material UI. Feel free to drop it out and just use regular elements. This is also ready with `next-css` so you can import any CSS file into your component 👍
6 |
7 | ## Highlights
8 |
9 | * The use of `compose()` from 'recompose/compose' to combine multiple component HOCs like `connect()`, and Material UI's `withStyles()`.
10 | * Redux state is persisted by `redux-perist`
11 | * Example of dynamic routing and passing URL params in `server.js`
12 |
13 | ## How it works
14 |
15 | ### Redux + Redux-Persist
16 |
17 | The way **NextJS** renders content is by using "page" components from the pages directory, and placing them in a wrapper component, as well as rendering them into a document (`react-dom` style). And **Redux** requires you to wrap your application in a **Provider** using React's Context API, and which you later connect your components to using **Consumers**.
18 |
19 | The way we wrap our app in NextJS is by creating a `_app.js` file in the pages directory. Here we use render props to pass through our route component that's getting rendered with it's props. And in that render prop component, we can add wrap any other components around our route/page - like our Redux store -- and our `` from `redux-persist` that holds our app in place until the Redux store is rehydrated. The `` accepts a Loading component that displays while the app is rehydrating the store, I left it `null`.
20 |
21 | ### Authentication
22 |
23 | There are Redux actions, reducers, and constants in place for all the necessary Authentication services (logging in a user, logging out, etc). This was based off the `react-redux-registration-login-example-master` by [Jason Watmore](http://jasonwatmore.com/post/2017/09/16/react-redux-user-registration-and-login-tutorial-example). When the user is logged in by dispatching the login action (`dispatch(userActions.login(username, password));`), whatever user data is transferred by the API as a response is stored in the Redux store under `authentication.user`.
24 |
25 |
26 | ## Stack
27 |
28 | * NextJS
29 | * ReactJS
30 | * Redux
31 | * NodeJS
32 | * Express
33 | * Isomorphic-Fetch (for SSR API calls)
34 |
35 | ## Development
36 |
37 | `npm run dev`
38 |
39 | Deploys an Express server, configured in the `server.js` file in project root, and builds the project using Next.
40 |
41 | > I highly recommend getting the Redux DevTools extension to browse the Redux store and state changes easily.
42 |
43 | ### Admin / Organizer Access
44 |
45 | This app uses JWT-style authentication and expects an access token that gets stored in localStorage and Redux for use in authenticated API calls later (through Redux actions or otherwise).
46 |
47 | > This is currently designed to use Laravel's Passport OAuth2 API, but it can be fit to any API that sends back a token.
48 |
49 | Spin up a development server, create a new account, and use those login details in this app. `AuthService` class assumes dev server is located at `http://localhost/`, but also accepts any URL when you make a "new" class (`new AuthService('http://localhost:4849')`). See the [seshsource-api](https://github.com/whoisryosuke/seshsource-api) repo for more details.
50 |
51 | ## Deployment
52 |
53 | `npm run build`
54 |
55 | ## Todo
56 |
57 | * [✅] - Redux implemented with NextJS
58 | * [✅] - Redux store persisted across reloads (redux-persist)
59 | * [✅] - Dynamic routing using Express
60 | * [✅] - Login Authentication using OAuth2.0 / JWT tokens
61 | * [✅] - Protected/Authenticated Routes using HOCs (supporting SSR!)
--------------------------------------------------------------------------------
/actions/user.actions.js:
--------------------------------------------------------------------------------
1 | import { userConstants } from '../constants';
2 | import AuthService from '../utils/AuthService';
3 | // import { alertActions } from './';
4 | import Router from 'next/router';
5 |
6 | export const userActions = {
7 | login,
8 | logout,
9 | loggedIn,
10 | register,
11 | getAll,
12 | delete: _delete
13 | };
14 |
15 | const auth = new AuthService('http://localhost')
16 |
17 |
18 | function login(username, password) {
19 | return dispatch => {
20 | dispatch(request({ username }));
21 |
22 | auth.login(username, password)
23 | .then(
24 | user => {
25 | dispatch(success(user));
26 | Router.push('/dashboard');
27 | },
28 | error => {
29 | dispatch(failure(error.toString()));
30 | // dispatch(alertActions.error(error.toString()));
31 | }
32 | );
33 | };
34 |
35 | function request(user) { return { type: userConstants.LOGIN_REQUEST, user } }
36 | function success(user) { return { type: userConstants.LOGIN_SUCCESS, user } }
37 | function failure(error) { return { type: userConstants.LOGIN_FAILURE, error } }
38 | }
39 |
40 | function loggedIn() {
41 | return dispatch => {
42 | dispatch(request());
43 | dispatch(success(auth.loggedIn()));
44 | };
45 | function request() { return { type: userConstants.TOKEN_REQUEST } }
46 | function success(status) { return { type: userConstants.TOKEN_SUCCESS, status } }
47 | }
48 |
49 | function logout() {
50 | auth.logout();
51 | return { type: userConstants.LOGOUT };
52 | }
53 |
54 | function register(user) {
55 | return dispatch => {
56 | dispatch(request(user));
57 |
58 | auth.register(user)
59 | .then(
60 | user => {
61 | dispatch(success());
62 | Router.push('/login');
63 | // dispatch(alertActions.success('Registration successful'));
64 | },
65 | error => {
66 | dispatch(failure(error.toString()));
67 | // dispatch(alertActions.error(error.toString()));
68 | }
69 | );
70 | };
71 |
72 | function request(user) { return { type: userConstants.REGISTER_REQUEST, user } }
73 | function success(user) { return { type: userConstants.REGISTER_SUCCESS, user } }
74 | function failure(error) { return { type: userConstants.REGISTER_FAILURE, error } }
75 | }
76 |
77 | function getAll() {
78 | return dispatch => {
79 | dispatch(request());
80 |
81 | auth.getAll()
82 | .then(
83 | users => dispatch(success(users)),
84 | error => dispatch(failure(error.toString()))
85 | );
86 | };
87 |
88 | function request() { return { type: userConstants.GETALL_REQUEST } }
89 | function success(users) { return { type: userConstants.GETALL_SUCCESS, users } }
90 | function failure(error) { return { type: userConstants.GETALL_FAILURE, error } }
91 | }
92 |
93 | // prefixed function name with underscore because delete is a reserved word in javascript
94 | function _delete(id) {
95 | return dispatch => {
96 | dispatch(request(id));
97 |
98 | auth.delete(id)
99 | .then(
100 | user => dispatch(success(id)),
101 | error => dispatch(failure(id, error.toString()))
102 | );
103 | };
104 |
105 | function request(id) { return { type: userConstants.DELETE_REQUEST, id } }
106 | function success(id) { return { type: userConstants.DELETE_SUCCESS, id } }
107 | function failure(id, error) { return { type: userConstants.DELETE_FAILURE, id, error } }
108 | }
--------------------------------------------------------------------------------
/pages/login.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 | import compose from 'recompose/compose';
4 | import { userActions } from '../actions';
5 | import { withStyles } from '@material-ui/core/styles';
6 | import Paper from '@material-ui/core/Paper';
7 | import Typography from '@material-ui/core/Typography';
8 | import Button from '@material-ui/core/Button';
9 |
10 | import Header from '../containers/header';
11 |
12 | const styles = theme => ({
13 | root: {
14 | flexGrow: 1,
15 | zIndex: 1,
16 | position: 'relative',
17 | display: 'flex',
18 | },
19 | toolbar: {
20 | display: 'flex',
21 | alignItems: 'center',
22 | justifyContent: 'flex-end',
23 | padding: '0 8px',
24 | ...theme.mixins.toolbar,
25 | },
26 | content: {
27 | flexGrow: 1,
28 | backgroundColor: theme.palette.background.default,
29 | padding: theme.spacing.unit * 3,
30 | paddingTop: '4rem'
31 | },
32 | paper: {
33 | ...theme.mixins.gutters(),
34 | paddingTop: theme.spacing.unit * 2,
35 | paddingBottom: theme.spacing.unit * 2,
36 | }
37 | });
38 |
39 | class Login extends React.Component {
40 | static getInitialProps ({ reduxStore, req }) {
41 | return {}
42 | }
43 |
44 | constructor(props) {
45 | super(props);
46 | this.state = {
47 | username: '',
48 | password: ''
49 | }
50 |
51 | this.handleChange = this.handleChange.bind(this);
52 | this.handleSubmit = this.handleSubmit.bind(this);
53 | }
54 |
55 | componentDidMount () {
56 | const {dispatch} = this.props
57 | }
58 |
59 | handleChange(event) {
60 | this.setState({
61 | [event.target.name]: event.target.value
62 | });
63 | }
64 |
65 | handleSubmit(event) {
66 | event.preventDefault();
67 | const {dispatch} = this.props
68 |
69 | // dispatch login here
70 | // login(dispatch, form.data)
71 | dispatch(userActions.login(this.state.username, this.state.password));
72 | }
73 |
74 | render () {
75 | const { classes, theme, loggedIn } = this.props;
76 | return (
77 |
78 |
79 |
80 |
81 |
82 |
106 |
107 |
108 |
109 | )
110 | }
111 | }
112 |
113 | // export default connect()(withStyles(styles, { withTheme: true })(Login))
114 |
115 | export default compose(
116 | withStyles(styles, {
117 | name: 'Login',
118 | withTheme: true
119 | }),
120 | connect(state => ({
121 | uiTheme: state.theme,
122 | })),
123 | )(Login);
124 |
--------------------------------------------------------------------------------
/components/header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import compose from 'recompose/compose';
4 | import classNames from 'classnames';
5 | import {connect} from 'react-redux'
6 | import Link from 'next/link'
7 | import { withStyles } from '@material-ui/core/styles';
8 | import Drawer from '@material-ui/core/Drawer';
9 | import AppBar from '@material-ui/core/AppBar';
10 | import Toolbar from '@material-ui/core/Toolbar';
11 | import List from '@material-ui/core/List';
12 | import ListItem from '@material-ui/core/ListItem';
13 | import ListItemText from '@material-ui/core/ListItemText';
14 | import ListItemIcon from '@material-ui/core/ListItemIcon';
15 | import Typography from '@material-ui/core/Typography';
16 | import Divider from '@material-ui/core/Divider';
17 | import IconButton from '@material-ui/core/IconButton';
18 | import MenuIcon from '@material-ui/icons/Menu';
19 | import AccountCircle from '@material-ui/icons/AccountCircle';
20 | import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
21 | import ChevronRightIcon from '@material-ui/icons/ChevronRight';
22 | import DraftsIcon from '@material-ui/icons/Drafts';
23 | import InboxIcon from '@material-ui/icons/Inbox';
24 | import MenuItem from '@material-ui/core/MenuItem';
25 | import Menu from '@material-ui/core/Menu';
26 |
27 | const drawerWidth = 240;
28 |
29 | const styles = theme => ({
30 | root: {
31 | flexGrow: 1,
32 | zIndex: 1,
33 | position: 'relative',
34 | display: 'flex',
35 | },
36 | appBar: {
37 | zIndex: theme.zIndex.drawer + 1,
38 | transition: theme.transitions.create(['width', 'margin'], {
39 | easing: theme.transitions.easing.sharp,
40 | duration: theme.transitions.duration.leavingScreen,
41 | }),
42 | },
43 | appBarShift: {
44 | marginLeft: drawerWidth,
45 | width: `calc(100% - ${drawerWidth}px)`,
46 | transition: theme.transitions.create(['width', 'margin'], {
47 | easing: theme.transitions.easing.sharp,
48 | duration: theme.transitions.duration.enteringScreen,
49 | }),
50 | },
51 | menuButton: {
52 | marginLeft: 12,
53 | marginRight: 36,
54 | },
55 | hide: {
56 | display: 'none',
57 | },
58 | drawerPaper: {
59 | position: 'relative',
60 | whiteSpace: 'nowrap',
61 | width: drawerWidth,
62 | transition: theme.transitions.create('width', {
63 | easing: theme.transitions.easing.sharp,
64 | duration: theme.transitions.duration.enteringScreen,
65 | }),
66 | },
67 | drawerPaperClose: {
68 | overflowX: 'hidden',
69 | transition: theme.transitions.create('width', {
70 | easing: theme.transitions.easing.sharp,
71 | duration: theme.transitions.duration.leavingScreen,
72 | }),
73 | width: theme.spacing.unit * 7,
74 | [theme.breakpoints.up('sm')]: {
75 | width: theme.spacing.unit * 9,
76 | },
77 | },
78 | });
79 |
80 | class Header extends React.Component {
81 | state = {
82 | open: false,
83 | anchorEl: null,
84 | };
85 |
86 | handleDrawerOpen = () => {
87 | this.setState({ open: true });
88 | };
89 |
90 | handleDrawerClose = () => {
91 | this.setState({ open: false });
92 | };
93 |
94 | handleMenu = event => {
95 | this.setState({
96 | anchorEl: event.currentTarget
97 | });
98 | };
99 |
100 | handleClose = () => {
101 | this.setState({
102 | anchorEl: null
103 | });
104 | };
105 |
106 | render() {
107 | const { classes, theme, loggedIn, user } = this.props;
108 | console.log(this.props);
109 | const { auth, anchorEl } = this.state;
110 | const open = Boolean(anchorEl);
111 |
112 | return (
113 |
114 |
118 |
119 |
125 |
126 |
127 |
128 | SeshSource
129 |
130 | {user && (
131 |