├── .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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------