├── README.md ├── README.nextjs.redux.md ├── actions ├── index.js └── user.actions.js ├── components ├── drawer.js ├── drawerMenu.js └── header.js ├── constants ├── index.js └── user.constants.js ├── containers └── header.js ├── lib └── with-redux-store.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── dashboard.js ├── events │ └── manage.js ├── index.js └── login.js ├── reducers ├── authentication.reducer.js ├── index.js └── users.reducer.js ├── server.js ├── store.js └── utils ├── AuthService.js └── withAuth.js /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!) -------------------------------------------------------------------------------- /README.nextjs.redux.md: -------------------------------------------------------------------------------- 1 | [![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-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 | -------------------------------------------------------------------------------- /actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './user.actions'; 2 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /components/drawer.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-redux-authentication-boilerplate/5d3137d267feb68e1870d63989d5677b7823a303/components/drawer.js -------------------------------------------------------------------------------- /components/drawerMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import List from '@material-ui/core/List'; 5 | import ListItem from '@material-ui/core/ListItem'; 6 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 7 | import ListItemText from '@material-ui/core/ListItemText'; 8 | import Divider from '@material-ui/core/Divider'; 9 | import InboxIcon from '@material-ui/icons/Inbox'; 10 | import DraftsIcon from '@material-ui/icons/Drafts'; 11 | 12 | const styles = theme => ({ 13 | root: { 14 | width: '100%', 15 | maxWidth: 360, 16 | backgroundColor: theme.palette.background.paper, 17 | }, 18 | }); 19 | 20 | function drawerMenu(props) { 21 | const { classes } = props; 22 | return ( 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | ); 49 | } 50 | 51 | drawerMenu.propTypes = { 52 | classes: PropTypes.object.isRequired, 53 | }; 54 | 55 | export default withStyles(styles)(drawerMenu); -------------------------------------------------------------------------------- /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 |
132 | 138 | 139 | 140 | 154 | Profile 155 | My account 156 | 157 |
158 | )} 159 |
160 |
161 | 168 |
169 | 170 | {theme.direction === 'rtl' ? : } 171 | 172 |
173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 |
210 |
211 | ); 212 | } 213 | } 214 | 215 | Header.propTypes = { 216 | classes: PropTypes.object.isRequired, 217 | theme: PropTypes.object.isRequired, 218 | }; 219 | 220 | function mapStateToProps (state) { 221 | console.log(state); 222 | const { authentication: { user }, theme: uiTheme } = state 223 | return {user, uiTheme} 224 | } 225 | 226 | // export default withStyles(styles, { withTheme: true })(Header); 227 | 228 | // export default compose( 229 | // withStyles(styles, { 230 | // name: 'Header', 231 | // withTheme: true 232 | // }), 233 | // connect(state => ({ 234 | // uiTheme: state.theme, 235 | // })), 236 | // )(Header); 237 | 238 | export default compose( 239 | withStyles(styles, { 240 | name: 'Header', 241 | withTheme: true 242 | }), 243 | connect(mapStateToProps), 244 | )(Header); -------------------------------------------------------------------------------- /constants/index.js: -------------------------------------------------------------------------------- 1 | export * from './user.constants'; -------------------------------------------------------------------------------- /constants/user.constants.js: -------------------------------------------------------------------------------- 1 | export const userConstants = { 2 | REGISTER_REQUEST: 'USERS_REGISTER_REQUEST', 3 | REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS', 4 | REGISTER_FAILURE: 'USERS_REGISTER_FAILURE', 5 | 6 | LOGIN_REQUEST: 'USERS_LOGIN_REQUEST', 7 | LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS', 8 | LOGIN_FAILURE: 'USERS_LOGIN_FAILURE', 9 | 10 | TOKEN_REQUEST: 'USERS_TOKEN_REQUEST', 11 | TOKEN_SUCCESS: 'USERS_TOKEN_SUCCESS', 12 | TOKEN_FAILURE: 'USERS_TOKEN_FAILURE', 13 | 14 | LOGOUT: 'USERS_LOGOUT', 15 | 16 | GETALL_REQUEST: 'USERS_GETALL_REQUEST', 17 | GETALL_SUCCESS: 'USERS_GETALL_SUCCESS', 18 | GETALL_FAILURE: 'USERS_GETALL_FAILURE', 19 | 20 | DELETE_REQUEST: 'USERS_DELETE_REQUEST', 21 | DELETE_SUCCESS: 'USERS_DELETE_SUCCESS', 22 | DELETE_FAILURE: 'USERS_DELETE_FAILURE' 23 | }; 24 | -------------------------------------------------------------------------------- /containers/header.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | import Header from '../components/header' 4 | 5 | function mapStateToProps (state) { 6 | const { authentication } = state 7 | const { loggedIn } = authentication; 8 | return { 9 | loggedIn 10 | } 11 | } 12 | 13 | export default connect(mapStateToProps)(Header) 14 | -------------------------------------------------------------------------------- /lib/with-redux-store.js: -------------------------------------------------------------------------------- 1 | import App, {Container} from 'next/app' 2 | import {Provider} from 'react-redux' 3 | import {initializeStore} from '../store' 4 | import { persistStore } from 'redux-persist' 5 | 6 | const isServer = typeof window === 'undefined' 7 | const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__' 8 | 9 | function getOrCreateStore(initialState) { 10 | // Always make a new store if server, otherwise state is shared between requests 11 | if (isServer) { 12 | return initializeStore(initialState) 13 | } 14 | 15 | // Store in global variable if client 16 | if (!window[__NEXT_REDUX_STORE__]) { 17 | window[__NEXT_REDUX_STORE__] = initializeStore(initialState) 18 | } 19 | return window[__NEXT_REDUX_STORE__] 20 | } 21 | 22 | export default (App) => { 23 | return class Redux extends React.Component { 24 | static async getInitialProps (appContext) { 25 | const reduxStore = getOrCreateStore() 26 | 27 | // Provide the store to getInitialProps of pages 28 | appContext.ctx.reduxStore = reduxStore 29 | 30 | let appProps = {} 31 | if (App.getInitialProps) { 32 | appProps = await App.getInitialProps(appContext) 33 | } 34 | 35 | return { 36 | ...appProps, 37 | initialReduxState: reduxStore.getState() 38 | } 39 | } 40 | 41 | constructor(props) { 42 | super(props) 43 | this.reduxStore = getOrCreateStore(props.initialReduxState) 44 | this.persistor = persistStore(this.reduxStore); 45 | } 46 | 47 | render() { 48 | return 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withCSS = require('@zeit/next-css') 2 | module.exports = withCSS() -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-redux-authentication-boilerplate", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "node server.js", 6 | "build": "next build", 7 | "start": "NODE_ENV=production node server.js" 8 | }, 9 | "dependencies": { 10 | "@zeit/next-css": "^0.2.0", 11 | "express": "^4.16.3", 12 | "isomorphic-unfetch": "^2.0.0", 13 | "next": "6.0.3", 14 | "react": "16.4.1", 15 | "react-dom": "16.4.1", 16 | "react-redux": "^5.0.1", 17 | "redux": "^3.6.0", 18 | "redux-devtools-extension": "^2.13.2", 19 | "redux-persist": "^5.10.0", 20 | "redux-thunk": "^2.1.0" 21 | }, 22 | "license": "ISC" 23 | } 24 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import App, {Container} from 'next/app' 2 | import React from 'react' 3 | import { PersistGate } from 'redux-persist/lib/integration/react'; 4 | import withReduxStore from '../lib/with-redux-store' 5 | import { Provider } from 'react-redux' 6 | 7 | class MyApp extends App { 8 | render () { 9 | const {Component, pageProps, reduxStore, persistor} = this.props 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | } 21 | 22 | export default withReduxStore(MyApp) 23 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Head, Main, NextScript } from 'next/document' 2 | 3 | export default class MyDocument extends Document { 4 | static async getInitialProps(ctx) { 5 | const initialProps = await Document.getInitialProps(ctx) 6 | return { ...initialProps } 7 | } 8 | 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | {/* Use minimum-scale=1 to enable GPU rasterization */} 15 | 22 | 23 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | ) 34 | } 35 | } -------------------------------------------------------------------------------- /pages/dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import withAuth from '../utils/withAuth' 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import Paper from '@material-ui/core/Paper'; 5 | import Typography from '@material-ui/core/Typography'; 6 | 7 | import Header from '../containers/header'; 8 | 9 | import '../assets/css/seshsource.css' 10 | 11 | const styles = theme => ({ 12 | root: { 13 | flexGrow: 1, 14 | zIndex: 1, 15 | position: 'relative', 16 | display: 'flex', 17 | }, 18 | toolbar: { 19 | display: 'flex', 20 | alignItems: 'center', 21 | justifyContent: 'flex-end', 22 | padding: '0 8px', 23 | ...theme.mixins.toolbar, 24 | }, 25 | content: { 26 | flexGrow: 1, 27 | backgroundColor: theme.palette.background.default, 28 | padding: theme.spacing.unit * 3, 29 | }, 30 | paper: { 31 | ...theme.mixins.gutters(), 32 | paddingTop: theme.spacing.unit * 2, 33 | paddingBottom: theme.spacing.unit * 2, 34 | } 35 | }); 36 | 37 | class Dashboard extends React.Component { 38 | render() { 39 | const { classes, theme, loggedIn } = this.props; 40 | const user = this.props.auth.getProfile() 41 | return ( 42 |
43 |
44 |
45 |
46 | 47 | 48 | Recent Events 49 | 50 | 51 | List of recent events here 52 | 53 | 54 |
55 |
56 | ) 57 | } 58 | } 59 | 60 | export default withAuth(withStyles(styles, { withTheme: true })(Dashboard)); -------------------------------------------------------------------------------- /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)); -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import {connect} from 'react-redux' 4 | 5 | class Index extends React.Component { 6 | static getInitialProps ({ reduxStore, req }) { 7 | const isServer = !!req 8 | // reduxStore.dispatch(serverRenderClock(isServer)) 9 | 10 | return {} 11 | } 12 | 13 | componentDidMount () { 14 | const {dispatch} = this.props 15 | // this.timer = startClock(dispatch) 16 | } 17 | 18 | render () { 19 | return ( 20 |
21 | 22 | Login 23 | 24 | 25 | Private Dashboard 26 | 27 |
28 | ) 29 | } 30 | } 31 | 32 | export default connect()(Index) 33 | -------------------------------------------------------------------------------- /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 |
83 |
84 | 85 | 91 |
92 | 93 |
94 | 95 | 101 |
102 | 105 |
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 | -------------------------------------------------------------------------------- /reducers/authentication.reducer.js: -------------------------------------------------------------------------------- 1 | import { userConstants } from '../constants'; 2 | 3 | // let user = JSON.parse(localStorage.getItem('id_token')); 4 | // const initialState = user ? { loggedIn: true, user } : {}; 5 | const initialState = {}; 6 | 7 | export function authentication(state = initialState, action) { 8 | switch (action.type) { 9 | case userConstants.LOGIN_REQUEST: 10 | return { 11 | loggingIn: true, 12 | user: action.user 13 | }; 14 | case userConstants.LOGIN_SUCCESS: 15 | return { 16 | loggedIn: true, 17 | user: action.user 18 | }; 19 | case userConstants.LOGIN_FAILURE: 20 | return {}; 21 | case userConstants.LOGOUT: 22 | return {}; 23 | default: 24 | return state 25 | } 26 | } -------------------------------------------------------------------------------- /reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import { authentication } from './authentication.reducer'; 4 | import { users } from './users.reducer'; 5 | 6 | const rootReducer = combineReducers({ 7 | authentication, 8 | users 9 | }); 10 | 11 | export default rootReducer; -------------------------------------------------------------------------------- /reducers/users.reducer.js: -------------------------------------------------------------------------------- 1 | import { userConstants } from '../constants'; 2 | 3 | export function users(state = {}, action) { 4 | switch (action.type) { 5 | case userConstants.GETALL_REQUEST: 6 | return { 7 | loading: true 8 | }; 9 | case userConstants.GETALL_SUCCESS: 10 | return { 11 | items: action.users 12 | }; 13 | case userConstants.GETALL_FAILURE: 14 | return { 15 | error: action.error 16 | }; 17 | case userConstants.DELETE_REQUEST: 18 | // add 'deleting:true' property to user being deleted 19 | return { 20 | ...state, 21 | items: state.items.map(user => 22 | user.id === action.id 23 | ? { ...user, deleting: true } 24 | : user 25 | ) 26 | }; 27 | case userConstants.DELETE_SUCCESS: 28 | // remove deleted user from state 29 | return { 30 | items: state.items.filter(user => user.id !== action.id) 31 | }; 32 | case userConstants.DELETE_FAILURE: 33 | // remove 'deleting:true' property and add 'deleteError:[error]' property to user 34 | return { 35 | ...state, 36 | items: state.items.map(user => { 37 | if (user.id === action.id) { 38 | // make copy of user without 'deleting:true' property 39 | const { deleting, ...userCopy } = user; 40 | // return copy of user with 'deleteError:[error]' property 41 | return { ...userCopy, deleteError: action.error }; 42 | } 43 | 44 | return user; 45 | }) 46 | }; 47 | default: 48 | return state 49 | } 50 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const next = require('next') 3 | 4 | const port = parseInt(process.env.PORT, 10) || 3000 5 | const dev = process.env.NODE_ENV !== 'production' 6 | const app = next({ dev }) 7 | const handle = app.getRequestHandler() 8 | 9 | app.prepare() 10 | .then(() => { 11 | const server = express() 12 | 13 | // server.get('/events/manage', (req, res) => { 14 | // return app.render(req, res, '/events/manage', { slug: req.params.slug }) 15 | // }) 16 | 17 | server.get('*', (req, res) => { 18 | return handle(req, res) 19 | }) 20 | 21 | server.listen(port, (err) => { 22 | if (err) throw err 23 | console.log(`> Ready on http://localhost:${port}`) 24 | }) 25 | }) -------------------------------------------------------------------------------- /store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import { persistReducer } from 'redux-persist' 3 | import { composeWithDevTools } from 'redux-devtools-extension' 4 | import thunkMiddleware from 'redux-thunk' 5 | import storage from 'redux-persist/lib/storage' // defaults to localStorage for web and AsyncStorage for react-native 6 | 7 | import rootReducer from './reducers'; 8 | 9 | const persistConfig = { 10 | key: 'root', 11 | storage, 12 | } 13 | 14 | const persistedReducer = persistReducer(persistConfig, rootReducer) 15 | 16 | const exampleInitialState = {} 17 | 18 | // A create store function for `withReduxStore` context wrapper 19 | export function initializeStore (initialState = exampleInitialState) { 20 | return createStore(persistedReducer, initialState, composeWithDevTools(applyMiddleware(thunkMiddleware))) 21 | } 22 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /utils/withAuth.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import Router from 'next/router' 3 | import AuthService from './AuthService' 4 | 5 | export default function withAuth(AuthComponent) { 6 | const Auth = new AuthService('http://localhost') 7 | return class Authenticated extends Component { 8 | 9 | static async getInitialProps(ctx) { 10 | // Ensures material-ui renders the correct css prefixes server-side 11 | let userAgent 12 | if (process.browser) { 13 | userAgent = navigator.userAgent 14 | } else { 15 | userAgent = ctx.req.headers['user-agent'] 16 | } 17 | 18 | // Check if Page has a `getInitialProps`; if so, call it. 19 | const pageProps = AuthComponent.getInitialProps && await AuthComponent.getInitialProps(ctx); 20 | // Return props. 21 | return { ...pageProps, userAgent } 22 | } 23 | 24 | constructor(props) { 25 | super(props) 26 | this.state = { 27 | isLoading: true 28 | }; 29 | } 30 | 31 | componentDidMount () { 32 | if (!Auth.loggedIn()) { 33 | Router.push('/') 34 | } 35 | this.setState({ isLoading: false }) 36 | } 37 | 38 | render() { 39 | return ( 40 |
41 | {this.state.isLoading ? ( 42 |
LOADING....
43 | ) : ( 44 | 45 | )} 46 |
47 | ) 48 | } 49 | } 50 | } 51 | --------------------------------------------------------------------------------