├── .gitignore ├── README.md ├── assets ├── css │ ├── semantic.min.css │ └── seshsource.css ├── fonts │ ├── brand-icons.eot │ ├── brand-icons.svg │ ├── brand-icons.ttf │ ├── brand-icons.woff │ ├── brand-icons.woff2 │ ├── icons.eot │ ├── icons.svg │ ├── icons.ttf │ ├── icons.woff │ ├── icons.woff2 │ ├── outline-icons.eot │ ├── outline-icons.svg │ ├── outline-icons.ttf │ ├── outline-icons.woff │ └── outline-icons.woff2 └── images │ └── flags.png ├── components ├── drawer.js ├── drawerMenu.js └── header.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── dashboard.js ├── events │ ├── create.js │ ├── manage.js │ └── profile.js ├── index.js └── login.js ├── server.js ├── store.js └── utils ├── AuthGate.js ├── AuthService.js ├── Cookies.js ├── SeshSourceApi.js └── withAuth.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextJS OAuth2 Authentication Example 2 | 3 | NextJS app that uses an external OAuth2 API to login users through a callback system, ultimately storing a JWT in a cookie. 4 | 5 | > Material UI is from an old project, you can just cut those chunks out if you want. 6 | 7 | ## Tools 8 | 9 | * NextJS 10 | * ReactJS 11 | * Material UI 12 | * NodeJS 13 | * Express 14 | 15 | ## Development 16 | 17 | `npm run dev` 18 | 19 | Deploys an Express server, configured in the `server.js` file in project root, and builds the project using Next. 20 | 21 | ### User Accounts 22 | 23 | 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. 24 | 25 | ## Deployment 26 | 27 | `npm run build` 28 | 29 | ## Todo 30 | 31 | * [✅] - Dynamic routing using Express 32 | * [✅] - Login Authentication using OAuth2.0 / JWT tokens 33 | * [✅] - Protected/Authenticated Routes using HOCs (supporting SSR!) 34 | * [✅] - ENV files implemented using dotenv 35 | * [✅] - OAuth2 callback login using Express 36 | * [✅] - CSRF middleware protection for forms 37 | * [✅] - Cookie parser added -------------------------------------------------------------------------------- /assets/css/seshsource.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin:0; 3 | } -------------------------------------------------------------------------------- /assets/fonts/brand-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/fonts/brand-icons.eot -------------------------------------------------------------------------------- /assets/fonts/brand-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/fonts/brand-icons.ttf -------------------------------------------------------------------------------- /assets/fonts/brand-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/fonts/brand-icons.woff -------------------------------------------------------------------------------- /assets/fonts/brand-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/fonts/brand-icons.woff2 -------------------------------------------------------------------------------- /assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/fonts/icons.eot -------------------------------------------------------------------------------- /assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/fonts/icons.woff -------------------------------------------------------------------------------- /assets/fonts/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/fonts/icons.woff2 -------------------------------------------------------------------------------- /assets/fonts/outline-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/fonts/outline-icons.eot -------------------------------------------------------------------------------- /assets/fonts/outline-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 19 | 22 | 25 | 28 | 31 | 34 | 37 | 40 | 43 | 46 | 49 | 52 | 55 | 58 | 61 | 64 | 67 | 70 | 73 | 76 | 79 | 82 | 85 | 88 | 91 | 94 | 97 | 100 | 103 | 106 | 109 | 112 | 115 | 118 | 121 | 124 | 127 | 130 | 133 | 136 | 139 | 142 | 145 | 148 | 151 | 154 | 157 | 160 | 163 | 166 | 169 | 172 | 175 | 178 | 181 | 184 | 187 | 190 | 193 | 196 | 199 | 202 | 205 | 208 | 211 | 214 | 217 | 220 | 223 | 226 | 229 | 232 | 235 | 238 | 241 | 244 | 247 | 250 | 253 | 256 | 259 | 262 | 265 | 268 | 271 | 274 | 277 | 280 | 283 | 286 | 289 | 292 | 295 | 298 | 301 | 304 | 307 | 310 | 313 | 316 | 319 | 322 | 325 | 328 | 331 | 334 | 337 | 340 | 343 | 346 | 349 | 352 | 355 | 358 | 361 | 364 | 365 | 366 | 367 | -------------------------------------------------------------------------------- /assets/fonts/outline-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/fonts/outline-icons.ttf -------------------------------------------------------------------------------- /assets/fonts/outline-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/fonts/outline-icons.woff -------------------------------------------------------------------------------- /assets/fonts/outline-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/fonts/outline-icons.woff2 -------------------------------------------------------------------------------- /assets/images/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/assets/images/flags.png -------------------------------------------------------------------------------- /components/drawer.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisryosuke/nextjs-oauth2-cookie-auth/66b9d1852a41b4f0b2a307e5451b7583f5c3335d/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 classNames from 'classnames'; 4 | import Link from 'next/link' 5 | import { withStyles } from '@material-ui/core/styles'; 6 | import Drawer from '@material-ui/core/Drawer'; 7 | import AppBar from '@material-ui/core/AppBar'; 8 | import Toolbar from '@material-ui/core/Toolbar'; 9 | import List from '@material-ui/core/List'; 10 | import ListItem from '@material-ui/core/ListItem'; 11 | import ListItemText from '@material-ui/core/ListItemText'; 12 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 13 | import Typography from '@material-ui/core/Typography'; 14 | import Divider from '@material-ui/core/Divider'; 15 | import IconButton from '@material-ui/core/IconButton'; 16 | import MenuIcon from '@material-ui/icons/Menu'; 17 | import AccountCircle from '@material-ui/icons/AccountCircle'; 18 | import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; 19 | import ChevronRightIcon from '@material-ui/icons/ChevronRight'; 20 | import DraftsIcon from '@material-ui/icons/Drafts'; 21 | import InboxIcon from '@material-ui/icons/Inbox'; 22 | import MenuItem from '@material-ui/core/MenuItem'; 23 | import Menu from '@material-ui/core/Menu'; 24 | 25 | const drawerWidth = 240; 26 | 27 | const styles = theme => ({ 28 | root: { 29 | flexGrow: 1, 30 | zIndex: 1, 31 | position: 'relative', 32 | display: 'flex', 33 | }, 34 | appBar: { 35 | zIndex: theme.zIndex.drawer + 1, 36 | transition: theme.transitions.create(['width', 'margin'], { 37 | easing: theme.transitions.easing.sharp, 38 | duration: theme.transitions.duration.leavingScreen, 39 | }), 40 | }, 41 | appBarShift: { 42 | marginLeft: drawerWidth, 43 | width: `calc(100% - ${drawerWidth}px)`, 44 | transition: theme.transitions.create(['width', 'margin'], { 45 | easing: theme.transitions.easing.sharp, 46 | duration: theme.transitions.duration.enteringScreen, 47 | }), 48 | }, 49 | menuButton: { 50 | marginLeft: 12, 51 | marginRight: 36, 52 | }, 53 | hide: { 54 | display: 'none', 55 | }, 56 | drawerPaper: { 57 | position: 'relative', 58 | whiteSpace: 'nowrap', 59 | width: drawerWidth, 60 | transition: theme.transitions.create('width', { 61 | easing: theme.transitions.easing.sharp, 62 | duration: theme.transitions.duration.enteringScreen, 63 | }), 64 | }, 65 | drawerPaperClose: { 66 | overflowX: 'hidden', 67 | transition: theme.transitions.create('width', { 68 | easing: theme.transitions.easing.sharp, 69 | duration: theme.transitions.duration.leavingScreen, 70 | }), 71 | width: theme.spacing.unit * 7, 72 | [theme.breakpoints.up('sm')]: { 73 | width: theme.spacing.unit * 9, 74 | }, 75 | }, 76 | }); 77 | 78 | class Header extends React.Component { 79 | state = { 80 | open: false, 81 | anchorEl: null, 82 | }; 83 | 84 | handleDrawerOpen = () => { 85 | this.setState({ open: true }); 86 | }; 87 | 88 | handleDrawerClose = () => { 89 | this.setState({ open: false }); 90 | }; 91 | 92 | handleMenu = event => { 93 | this.setState({ 94 | anchorEl: event.currentTarget 95 | }); 96 | }; 97 | 98 | handleClose = () => { 99 | this.setState({ 100 | anchorEl: null 101 | }); 102 | }; 103 | 104 | render() { 105 | const { classes, theme, loggedIn, user } = this.props; 106 | const { auth, anchorEl } = this.state; 107 | const open = Boolean(anchorEl); 108 | 109 | return ( 110 | 111 | 115 | 116 | 122 | 123 | 124 | 125 | SeshSource 126 | 127 | {user && ( 128 |
129 | 135 | 136 | 137 | 151 | Profile 152 | My account 153 | 154 |
155 | )} 156 |
157 |
158 | 165 |
166 | 167 | {theme.direction === 'rtl' ? : } 168 | 169 |
170 | 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 | Header.propTypes = { 213 | classes: PropTypes.object.isRequired, 214 | theme: PropTypes.object.isRequired, 215 | }; 216 | 217 | export default withStyles(styles, { withTheme: true })(Header); -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withCSS = require('@zeit/next-css') 2 | module.exports = withCSS() -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-oauth2-cookie-auth", 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 | "@material-ui/core": "^1.3.0", 11 | "@material-ui/icons": "^1.1.0", 12 | "@zeit/next-css": "^0.2.0", 13 | "cookie-parser": "^1.4.3", 14 | "csurf": "^1.9.0", 15 | "dotenv": "^6.0.0", 16 | "express": "^4.16.3", 17 | "isomorphic-unfetch": "^2.0.0", 18 | "js-cookie": "^2.2.0", 19 | "moment": "^2.22.2", 20 | "next": "6.0.3", 21 | "react": "16.4.1", 22 | "react-cookie": "^2.2.0", 23 | "react-dom": "16.4.1" 24 | }, 25 | "license": "ISC" 26 | } 27 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import App, {Container} from 'next/app' 2 | import React from 'react' 3 | import { CookiesProvider } from 'react-cookie'; 4 | 5 | class MyApp extends App { 6 | render () { 7 | const {Component, pageProps, reduxStore, persistor} = this.props 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | } 16 | } 17 | 18 | export default MyApp 19 | -------------------------------------------------------------------------------- /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 '../components/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/create.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Router from 'next/router' 3 | import Api from '../../utils/SeshSourceApi' 4 | import withAuth from '../../utils/withAuth' 5 | import moment from 'moment'; 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 | 12 | import Header from '../../components/header'; 13 | 14 | import '../../assets/css/seshsource.css' 15 | 16 | const styles = theme => ({ 17 | root: { 18 | flexGrow: 1, 19 | zIndex: 1, 20 | position: 'relative', 21 | display: 'flex', 22 | }, 23 | toolbar: { 24 | display: 'flex', 25 | alignItems: 'center', 26 | justifyContent: 'flex-end', 27 | padding: '0 8px', 28 | ...theme.mixins.toolbar, 29 | }, 30 | content: { 31 | flexGrow: 1, 32 | backgroundColor: theme.palette.background.default, 33 | padding: theme.spacing.unit * 3, 34 | }, 35 | paper: { 36 | ...theme.mixins.gutters(), 37 | paddingTop: theme.spacing.unit * 2, 38 | paddingBottom: theme.spacing.unit * 2, 39 | }, 40 | textField: { 41 | marginLeft: theme.spacing.unit, 42 | marginRight: theme.spacing.unit, 43 | width: 200, 44 | }, 45 | fullTextField: { 46 | marginLeft: theme.spacing.unit, 47 | marginRight: theme.spacing.unit, 48 | width: '100%', 49 | }, 50 | halfField: { 51 | marginLeft: theme.spacing.unit, 52 | marginRight: theme.spacing.unit, 53 | width: '48%', 54 | }, 55 | cityField: { 56 | marginLeft: theme.spacing.unit, 57 | marginRight: theme.spacing.unit, 58 | width: '22%', 59 | }, 60 | button: { 61 | margin: theme.spacing.unit, 62 | display: 'block', 63 | width: '100%' 64 | }, 65 | }); 66 | 67 | class CreateEvent extends React.Component { 68 | 69 | constructor(props) { 70 | super(props); 71 | this.state = { 72 | title: '', 73 | start_date: '2017-05-24T10:30', 74 | end_date: '2017-05-24T10:30', 75 | street_address: '', 76 | city: '', 77 | state: '', 78 | email: '', 79 | } 80 | 81 | this.handleChange = this.handleChange.bind(this); 82 | this.handleSubmit = this.handleSubmit.bind(this); 83 | } 84 | 85 | handleChange(event) { 86 | this.setState({ 87 | [event.target.name]: event.target.value 88 | }); 89 | } 90 | 91 | handleSubmit(event) { 92 | event.preventDefault(); 93 | 94 | const SeshApi = new Api(this.props.auth); 95 | 96 | // Destructure to copy the state object (instead of altering it) 97 | let data = { ...this.state }; 98 | 99 | // Grab the dates and convert to PHP format 100 | let { start_date, end_date } = data; 101 | data.start_date = moment(start_date).format('YYYY-MM-DD HH:mm:ss'); 102 | data.end_date = moment(end_date).format('YYYY-MM-DD HH:mm:ss'); 103 | 104 | // Create the post! 105 | let results = SeshApi.create(data); 106 | 107 | // If successful, clear the form for a new post 108 | Router.push('/events/manage'); 109 | } 110 | 111 | setDate = (dateTime) => this.setState({ dateTime }) 112 | 113 | render() { 114 | const { classes, theme, loggedIn } = this.props; 115 | const user = this.props.auth.getProfile() 116 | return ( 117 |
118 |
119 |
120 |
121 | 122 | 123 | New Event 124 | 125 |
126 | 135 | 136 | 148 | 149 | 161 | 162 | 170 | 171 | 179 | 180 | 188 | 189 | 197 | 198 | 207 | 208 |
209 | {'You think water moves fast? You should see ice.'} 210 |
211 |
212 | ) 213 | } 214 | } 215 | 216 | export default withAuth(withStyles(styles, { withTheme: true })(CreateEvent)); -------------------------------------------------------------------------------- /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 moment from 'moment'; 6 | import fetch from 'isomorphic-unfetch' 7 | import { withStyles } from '@material-ui/core/styles'; 8 | import Paper from '@material-ui/core/Paper'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import TextField from '@material-ui/core/TextField'; 11 | import Button from '@material-ui/core/Button'; 12 | import List from '@material-ui/core/List'; 13 | import ListItem from '@material-ui/core/ListItem'; 14 | import ListItemText from '@material-ui/core/ListItemText'; 15 | 16 | import Header from '../../components/header'; 17 | 18 | import '../../assets/css/seshsource.css' 19 | 20 | const styles = theme => ({ 21 | root: { 22 | flexGrow: 1, 23 | zIndex: 1, 24 | position: 'relative', 25 | display: 'flex', 26 | }, 27 | toolbar: { 28 | display: 'flex', 29 | alignItems: 'center', 30 | justifyContent: 'flex-end', 31 | padding: '0 8px', 32 | ...theme.mixins.toolbar, 33 | }, 34 | content: { 35 | flexGrow: 1, 36 | backgroundColor: theme.palette.background.default, 37 | padding: theme.spacing.unit * 3, 38 | }, 39 | paper: { 40 | ...theme.mixins.gutters(), 41 | paddingTop: theme.spacing.unit * 2, 42 | paddingBottom: theme.spacing.unit * 2, 43 | }, 44 | }); 45 | 46 | class CreateEvent extends React.Component { 47 | static async getInitialProps({ req }) { 48 | let query = await fetch('http://localhost/api/events') 49 | let events = await query.json() 50 | 51 | return { 52 | events: events 53 | } 54 | } 55 | 56 | render() { 57 | const { classes, theme, loggedIn, events } = this.props; 58 | const user = this.props.auth.getProfile() 59 | 60 | const list = events.data.map((event) => { 61 | let startDate = moment(event.start_date).format('MMM Do YYYY, h:mm a') 62 | let url = '/events/' + event.slug; 63 | return ( 64 | 65 | 66 | 67 | 68 | 69 | ) 70 | }) 71 | 72 | return ( 73 |
74 |
75 |
76 |
77 | 78 | 79 | Manage Events 80 | 81 | {list && 82 | 83 | { list } 84 | 85 | } 86 | 87 | {'You think water moves fast? You should see ice.'} 88 |
89 |
90 | ) 91 | } 92 | } 93 | 94 | export default withAuth(withStyles(styles, { withTheme: true })(CreateEvent)); -------------------------------------------------------------------------------- /pages/events/profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Api from '../../utils/SeshSourceApi' 3 | import withAuth from '../../utils/withAuth' 4 | import moment from 'moment'; 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 '../../components/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 EventProfile extends React.Component { 46 | static async getInitialProps({ req, query: { slug } }) { 47 | let query = await fetch('http://localhost/api/events/' + slug) 48 | let event = await query.json() 49 | let organizerQuery = await fetch('http://localhost/api/users/' + event.organizer_id) 50 | let organizer = await organizerQuery.json() 51 | 52 | return { 53 | event, 54 | organizer, 55 | slug 56 | } 57 | } 58 | 59 | render() { 60 | const { classes, theme, loggedIn, event, slug, organizer } = this.props; 61 | const user = this.props.auth.getProfile() 62 | 63 | console.log(organizer); 64 | 65 | return ( 66 |
67 |
68 |
69 |
70 | 71 | 72 | { event.title } 73 | 74 | 75 | 76 | Start Date 77 | 78 | 79 | { moment(event.start_date).format('MM-DD-YYYY') } 80 | 81 | 82 | { moment(event.start_date).format('h:mm a') } 83 | 84 | 85 | 86 | End Date 87 | 88 | 89 | { moment(event.end_date).format('MM-DD-YYYY') } 90 | 91 | 92 | { moment(event.end_date).format('h:mm a') } 93 | 94 | 95 | 96 | 97 | Description 98 | 99 | 100 | { event.description } 101 | 102 | 103 | 104 | 105 | Street Address 106 | 107 | 108 | { event.street_address } 109 | 110 | 111 | 112 | City 113 | 114 | 115 | { event.city } 116 | 117 | 118 | 119 | State 120 | 121 | 122 | { event.state } 123 | 124 | 125 | 126 | Postal Code 127 | 128 | 129 | { event.postal_code } 130 | 131 | 132 | 133 | Country 134 | 135 | 136 | { event.country } 137 | 138 | 139 | 140 | Website 141 | 142 | 143 | { event.website } 144 | 145 | 146 | 147 | Email 148 | 149 | 150 | { event.email } 151 | 152 | 153 | 154 | Organizer 155 | 156 | 157 | { organizer.name } 158 | 159 | 160 | 161 | Organizer Email 162 | 163 | 164 | { organizer.email } 165 | 166 | 167 | 168 |
169 |
170 | ) 171 | } 172 | } 173 | 174 | export default withAuth(withStyles(styles, { withTheme: true })(EventProfile)); -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | 4 | class Index extends React.Component { 5 | static getInitialProps ({ reduxStore, req }) { 6 | const isServer = !!req 7 | // reduxStore.dispatch(serverRenderClock(isServer)) 8 | 9 | return {} 10 | } 11 | 12 | componentDidMount () { 13 | // this.timer = startClock(dispatch) 14 | } 15 | 16 | render () { 17 | return ( 18 |
19 | Login 20 | 21 | Private Dashboard 22 | 23 |
24 | ) 25 | } 26 | } 27 | 28 | export default Index 29 | -------------------------------------------------------------------------------- /pages/login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import compose from 'recompose/compose'; 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 | import Button from '@material-ui/core/Button'; 7 | 8 | import Header from '../components/header'; 9 | 10 | import '../assets/css/seshsource.css' 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 ({ 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 withStyles(styles, { withTheme: true })(Login) -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const express = require('express') 3 | const next = require('next') 4 | var cookieParser = require('cookie-parser') 5 | var bodyParser = require('body-parser') 6 | var csrf = require('csurf') 7 | var fetch = require('isomorphic-unfetch') 8 | var Cookies = require('js-cookie'); 9 | 10 | // Setup CSRF Middleware 11 | var csrfProtection = csrf({ cookie: true }) 12 | // var parseForm = bodyParser.urlencoded({ extended: false }) 13 | 14 | const port = parseInt(process.env.PORT, 10) || 3000 15 | const dev = process.env.NODE_ENV !== 'production' 16 | const app = next({ dev }) 17 | const handle = app.getRequestHandler() 18 | 19 | app.prepare() 20 | .then(() => { 21 | const server = express() 22 | const middlewares = [ 23 | bodyParser.urlencoded(), 24 | cookieParser('sesh-dash'), 25 | csrfProtection 26 | ] 27 | server.use(middlewares) 28 | 29 | server.get('/events/manage', (req, res) => { 30 | return app.render(req, res, '/events/manage', { slug: req.params.slug }) 31 | }) 32 | 33 | server.get('/events/create', (req, res) => { 34 | return app.render(req, res, '/events/create', { slug: req.params.slug }) 35 | }) 36 | 37 | server.get('/events/:slug', (req, res) => { 38 | return app.render(req, res, '/events/profile', { slug: req.params.slug }) 39 | }) 40 | 41 | server.get('/dashboard/', (req, res) => { 42 | return app.render(req, res, '/dashboard') 43 | }) 44 | 45 | // Callback for OAuth2 API 46 | server.get('/token', (req, res) => { 47 | const callback = { 48 | grant_type: 'authorization_code', 49 | client_id: process.env.API_CLIENT_ID, 50 | client_secret: process.env.API_CLIENT_SECRET, 51 | redirect_uri: process.env.API_REDIRECT_URI, 52 | code: req.query.code 53 | } 54 | 55 | // Query API for token 56 | fetch('http://localhost/oauth/token', { 57 | method: 'post', 58 | headers: { 59 | 'Content-Type': 'application/json' 60 | }, 61 | body: JSON.stringify(callback) 62 | }) 63 | .then(r => r.json()) 64 | .then(data => { 65 | // Store JWT from response in cookies 66 | if (req.cookies['seshToken']) 67 | { 68 | res.clearCookie('seshToken') 69 | } 70 | res.cookie('seshToken', data.access_token, { 71 | maxAge: 900000, 72 | httpOnly: true 73 | }); 74 | return res.redirect('/dashboard') 75 | }); 76 | 77 | //Redirect to dashboard after login 78 | // return app.render(req, res, '/dashboard') 79 | }) 80 | 81 | server.get('*', (req, res) => { 82 | return handle(req, res) 83 | }) 84 | 85 | server.listen(port, (err) => { 86 | if (err) throw err 87 | console.log(`> Ready on http://localhost:${port}`) 88 | }) 89 | }) -------------------------------------------------------------------------------- /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/AuthGate.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class AuthGate extends Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | const { cookies } = props; 8 | this.state = { 9 | name: cookies.get('seshToken') || null 10 | }; 11 | } 12 | 13 | componentDidMount() { 14 | console.log(this.state.token); 15 | if (!this.state.token) { 16 | Router.push('/login') 17 | } 18 | this.setState({ 19 | isLoading: false 20 | }) 21 | } 22 | render() { 23 | return ( 24 |
25 | {this.state.isLoading ? ( 26 |
LOADING....
27 | ) : ( 28 | this.props.children 29 | )} 30 |
31 | ) 32 | } 33 | } 34 | 35 | export default withCookies(AuthGate) -------------------------------------------------------------------------------- /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 | setProfile(profile) { 29 | // Saves profile data to localStorage 30 | localStorage.setItem('profile', JSON.stringify(profile)) 31 | } 32 | 33 | getProfile() { 34 | // Retrieves the profile data from localStorage 35 | const profile = localStorage.getItem('profile') 36 | return profile ? JSON.parse(localStorage.profile) : {} 37 | } 38 | 39 | getToken() { 40 | // Retrieves the user token from localStorage 41 | return localStorage.getItem('id_token') 42 | } 43 | 44 | logout() { 45 | // Clear user token and profile data from localStorage 46 | localStorage.removeItem('id_token'); 47 | localStorage.removeItem('profile'); 48 | } 49 | 50 | _checkStatus(response) { 51 | // raises an error in case response status is not a success 52 | if (response.status >= 200 && response.status < 300) { 53 | return response 54 | } else { 55 | var error = new Error(response.statusText) 56 | error.response = response 57 | throw error 58 | } 59 | } 60 | 61 | fetch(url, options) { 62 | // performs api calls sending the required authentication headers 63 | const headers = { 64 | 'Accept': 'application/json', 65 | 'Content-Type': 'application/json' 66 | } 67 | 68 | if (this.loggedIn()) { 69 | headers['Authorization'] = 'Bearer ' + this.getToken() 70 | } 71 | 72 | return fetch(url, { 73 | headers, 74 | ...options 75 | }) 76 | .then(this._checkStatus) 77 | .then(response => response.json()) 78 | } 79 | } -------------------------------------------------------------------------------- /utils/Cookies.js: -------------------------------------------------------------------------------- 1 | import cookie from "js-cookie"; 2 | 3 | export const setCookie = (key, value) => { 4 | if (process.browser) { 5 | cookie.set(key, value, { 6 | expires: 1, 7 | path: "/" 8 | }); 9 | } 10 | }; 11 | 12 | export const removeCookie = key => { 13 | if (process.browser) { 14 | cookie.remove(key, { 15 | expires: 1 16 | }); 17 | } 18 | }; 19 | 20 | export const getCookie = (key, req) => { 21 | return process.browser ? 22 | getCookieFromBrowser(key) : 23 | getCookieFromServer(key, req); 24 | }; 25 | 26 | const getCookieFromBrowser = key => { 27 | console.log('grabbing key from browser') 28 | return cookie.get(key); 29 | }; 30 | 31 | const getCookieFromServer = (key, req) => { 32 | console.log('grabbing key from server') 33 | if (!req.headers.cookie) { 34 | return undefined; 35 | } 36 | const rawCookie = req.headers.cookie 37 | .split(";") 38 | .find(c => c.trim().startsWith(`${key}=`)); 39 | if (!rawCookie) { 40 | return undefined; 41 | } 42 | return rawCookie.split("=")[1]; 43 | }; -------------------------------------------------------------------------------- /utils/SeshSourceApi.js: -------------------------------------------------------------------------------- 1 | export default class SeshSourceApi { 2 | constructor(auth) { 3 | this.domain = 'http://localhost' 4 | this.fetch = this.fetch.bind(this) 5 | this.create = this.create.bind(this) 6 | this.auth = auth; 7 | } 8 | 9 | create(event) { 10 | console.log(event); 11 | // Get a token 12 | return this.fetch(`${this.domain}/api/events/`, { 13 | method: 'POST', 14 | body: JSON.stringify(event) 15 | }).then(res => { 16 | console.log(res); 17 | return Promise.resolve(res) 18 | }) 19 | } 20 | 21 | fetch(url, options) { 22 | // performs api calls sending the required authentication headers 23 | const headers = { 24 | 'Accept': 'application/json', 25 | 'Content-Type': 'application/json' 26 | } 27 | 28 | if (this.auth.loggedIn()) { 29 | headers['Authorization'] = 'Bearer ' + this.auth.getToken() 30 | } 31 | 32 | return fetch(url, { 33 | headers, 34 | ...options 35 | }) 36 | .then(this.auth._checkStatus) 37 | .then(response => response.json()) 38 | } 39 | } -------------------------------------------------------------------------------- /utils/withAuth.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import Router from 'next/router' 3 | import AuthService from './AuthService' 4 | import { getCookie, setCookie } from '../utils/Cookies' 5 | import cookie from "js-cookie"; 6 | 7 | export default function withAuth(AuthComponent) { 8 | const Auth = new AuthService(process.env.API_DOMAIN_URL) 9 | return class Authenticated extends Component { 10 | 11 | static async getInitialProps(ctx) { 12 | const isServer = !!ctx.req 13 | 14 | // Ensures material-ui renders the correct css prefixes server-side 15 | let userAgent 16 | let seshToken 17 | if (!isServer) { 18 | userAgent = navigator.userAgent 19 | seshToken = cookie.get('seshToken'); 20 | } else { 21 | userAgent = ctx.req.headers['user-agent'] 22 | seshToken = getCookie('seshToken', ctx.req); 23 | } 24 | let isLoading = true 25 | console.log(seshToken); 26 | if (!seshToken) { 27 | // ctx.res.writeHead(301, { 28 | // Location: `http://localhost/oauth/authorize/?client_id=4&redirect_uri=http://localhost:3000/token&response_type=code` 29 | // }) 30 | // ctx.res.end() 31 | } else { 32 | setCookie('seshToken', seshToken) 33 | isLoading = false 34 | } 35 | 36 | // Check if Page has a `getInitialProps`; if so, call it. 37 | const pageProps = AuthComponent.getInitialProps && await AuthComponent.getInitialProps(ctx); 38 | // Return props. 39 | return { 40 | ...pageProps, 41 | userAgent, 42 | isLoading, 43 | seshToken 44 | } 45 | } 46 | 47 | constructor(props) { 48 | super(props) 49 | this.state = { 50 | isLoading: props.isLoading, 51 | token: props.seshToken 52 | }; 53 | } 54 | 55 | componentDidMount () { 56 | if (!this.state.token) { 57 | Router.push('/') 58 | } 59 | this.setState({ isLoading: false }) 60 | } 61 | 62 | render() { 63 | return ( 64 |
65 | {this.state.isLoading ? ( 66 |
LOADING....
67 | ) : ( 68 | 69 | )} 70 |
71 | ) 72 | } 73 | } 74 | } 75 | --------------------------------------------------------------------------------