Skip to content

Commit f0026a1

Browse files
committed
Add authentication and profile page
1 parent fcd7155 commit f0026a1

File tree

5 files changed

+269
-8
lines changed

5 files changed

+269
-8
lines changed

src/components/App.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import React, { Component } from 'react'
2+
import { Switch, Route, Redirect } from 'react-router'
3+
import { Link } from 'react-router-dom'
4+
import PropTypes from 'prop-types'
5+
26
import logo from '../logo.svg'
37
import StarCount from './StarCount'
48
import TableOfContents from './TableOfContents'
59
import Section from './Section'
6-
import { Switch, Route, Redirect } from 'react-router'
10+
import CurrentUser from './CurrentUser'
11+
import Profile from './Profile'
12+
import withAuth from '../lib/withAuth'
713

814
const Book = () => (
915
<div>
@@ -14,20 +20,37 @@ const Book = () => (
1420

1521
class App extends Component {
1622
render() {
23+
const { logout, ...authProps } = this.props
24+
1725
return (
1826
<div className="App">
1927
<header className="App-header">
2028
<StarCount />
21-
<img src={logo} className="App-logo" alt="logo" />
22-
<h1 className="App-title">The GraphQL Guide</h1>
29+
<Link className="App-home-link" to="/">
30+
<img src={logo} className="App-logo" alt="logo" />
31+
<h1 className="App-title">The GraphQL Guide</h1>
32+
</Link>
33+
<CurrentUser {...authProps} />
2334
</header>
2435
<Switch>
2536
<Route exact path="/" render={() => <Redirect to="/Preface" />} />
37+
<Route
38+
exact
39+
path="/me"
40+
render={() => <Profile logout={logout} {...authProps} />}
41+
/>
2642
<Route component={Book} />
2743
</Switch>
2844
</div>
2945
)
3046
}
3147
}
3248

33-
export default App
49+
App.propTypes = {
50+
user: PropTypes.object,
51+
login: PropTypes.func.isRequired,
52+
logout: PropTypes.func.isRequired,
53+
loading: PropTypes.bool.isRequired
54+
}
55+
56+
export default withAuth(App)

src/components/CurrentUser.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
import { Link } from 'react-router-dom'
4+
5+
const CurrentUser = ({ user, login, loading }) => {
6+
const User = () => (
7+
<Link to="/me" className="User">
8+
<img src={user.photo} alt={user.firstName} />
9+
{user.firstName}
10+
</Link>
11+
)
12+
13+
let content
14+
15+
if (user) {
16+
content = <User />
17+
} else if (loading) {
18+
content = <div className="Spinner" />
19+
} else {
20+
content = <button onClick={login}>Sign in</button>
21+
}
22+
23+
return <div className="CurrentUser">{content}</div>
24+
}
25+
26+
CurrentUser.propTypes = {
27+
user: PropTypes.shape({
28+
firstName: PropTypes.string.isRequired,
29+
photo: PropTypes.string.isRequired
30+
}),
31+
login: PropTypes.func.isRequired,
32+
loading: PropTypes.bool.isRequired
33+
}
34+
35+
export default CurrentUser

src/components/Profile.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
4+
const Profile = ({ user, login, logout, loading }) => {
5+
if (loading) {
6+
return (
7+
<main className="Profile">
8+
<div className="Spinner" />
9+
</main>
10+
)
11+
} else if (!user) {
12+
return (
13+
<main className="Profile">
14+
<button onClick={login} className="Profile-login">
15+
Sign in
16+
</button>
17+
</main>
18+
)
19+
} else {
20+
return (
21+
<main className="Profile">
22+
<div className="Profile-header-wrapper">
23+
<header className="Profile-header">
24+
<h1>{user.name}</h1>
25+
</header>
26+
</div>
27+
<div className="Profile-content">
28+
<dl>
29+
<dt>Email</dt>
30+
<dd>
31+
<code>{user.email}</code>
32+
</dd>
33+
34+
<dt>Membership level</dt>
35+
<dd>
36+
<code>{user.hasPurchased || 'GUEST'}</code>
37+
</dd>
38+
39+
<dt>OAuth Github account</dt>
40+
<dd>
41+
<a
42+
href="https://github.com/settings/applications"
43+
target="_blank"
44+
rel="noopener noreferrer"
45+
>
46+
<code>{user.username}</code>
47+
</a>
48+
</dd>
49+
</dl>
50+
51+
<button className="Profile-logout" onClick={logout}>
52+
Sign out
53+
</button>
54+
</div>
55+
</main>
56+
)
57+
}
58+
}
59+
60+
Profile.propTypes = {
61+
user: PropTypes.shape({
62+
name: PropTypes.string.isRequired,
63+
email: PropTypes.string.isRequired,
64+
hasPurchased: PropTypes.string
65+
}),
66+
login: PropTypes.func.isRequired,
67+
logout: PropTypes.func.isRequired,
68+
loading: PropTypes.bool.isRequired
69+
}
70+
71+
export default Profile

src/index.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import React from 'react'
22
import ReactDOM from 'react-dom'
3-
import './index.css'
4-
import App from './components/App'
5-
import registerServiceWorker from './registerServiceWorker'
63
import { ApolloClient } from 'apollo-client'
74
import { ApolloProvider } from 'react-apollo'
85
import { InMemoryCache } from 'apollo-cache-inmemory'
@@ -11,11 +8,36 @@ import { WebSocketLink } from 'apollo-link-ws'
118
import { createHttpLink } from 'apollo-link-http'
129
import { getMainDefinition } from 'apollo-utilities'
1310
import { BrowserRouter } from 'react-router-dom'
11+
import { setContext } from 'apollo-link-context'
12+
import { getAuthToken } from 'auth0-helpers'
13+
14+
import './index.css'
15+
import registerServiceWorker from './registerServiceWorker'
16+
import App from './components/App'
1417

1518
const httpLink = createHttpLink({
1619
uri: 'https://api.graphql.guide/graphql'
1720
})
1821

22+
const authLink = setContext(async (_, { headers }) => {
23+
const token = await getAuthToken({
24+
doLoginIfTokenExpired: true
25+
})
26+
27+
if (token) {
28+
return {
29+
headers: {
30+
...headers,
31+
authorization: `Bearer ${token}`
32+
}
33+
}
34+
} else {
35+
return { headers }
36+
}
37+
})
38+
39+
const authedHttpLink = authLink.concat(httpLink)
40+
1941
const wsLink = new WebSocketLink({
2042
uri: `wss://api.graphql.guide/subscriptions`,
2143
options: {
@@ -29,7 +51,7 @@ const link = split(
2951
return kind === 'OperationDefinition' && operation === 'subscription'
3052
},
3153
wsLink,
32-
httpLink
54+
authedHttpLink
3355
)
3456

3557
const cache = new InMemoryCache()

src/lib/withAuth.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
import wrapDisplayName from 'recompose/wrapDisplayName'
4+
import { graphql } from 'react-apollo'
5+
import gql from 'graphql-tag'
6+
import auth0 from 'auth0-js'
7+
import { initAuthHelpers, login, logout } from 'auth0-helpers'
8+
9+
const client = new auth0.WebAuth({
10+
domain: 'graphql.auth0.com',
11+
clientID: '8fErnZoF3hbzQ2AbMYu5xcS0aVNzQ0PC',
12+
responseType: 'token',
13+
audience: 'https://api.graphql.guide',
14+
scope: 'openid profile guide'
15+
})
16+
17+
initAuthHelpers({
18+
client,
19+
usePopup: true,
20+
authOptions: {
21+
connection: 'github',
22+
owp: true,
23+
popupOptions: { height: 623 } // make tall enough for content
24+
},
25+
checkSessionOptions: {
26+
redirect_uri: window.location.origin
27+
},
28+
onError: e => console.error(e)
29+
})
30+
31+
const USER_QUERY = gql`
32+
query UserQuery {
33+
currentUser {
34+
firstName
35+
name
36+
username
37+
email
38+
photo
39+
hasPurchased
40+
}
41+
}
42+
`
43+
44+
const withUser = graphql(USER_QUERY, {
45+
props: ({ ownProps, data: { currentUser, loading, refetch } }) => ({
46+
currentUser,
47+
loading,
48+
refetch,
49+
ownProps
50+
})
51+
})
52+
53+
function withAuth(BaseComponent) {
54+
class WithAuthWrapper extends React.Component {
55+
constructor(props) {
56+
super(props)
57+
58+
this.state = {
59+
loggingIn: false
60+
}
61+
}
62+
63+
login = () => {
64+
this.setState({ loggingIn: true })
65+
login({
66+
onCompleted: (e, t) => {
67+
e && console.log(e)
68+
this.props.refetch()
69+
this.setState({ loggingIn: false })
70+
}
71+
})
72+
}
73+
74+
logout = () => {
75+
logout()
76+
this.props.refetch()
77+
}
78+
79+
render() {
80+
const { currentUser, loading, ownProps } = this.props
81+
82+
return (
83+
<BaseComponent
84+
user={currentUser}
85+
loading={loading || this.state.loggingIn}
86+
login={this.login}
87+
logout={this.logout}
88+
{...ownProps}
89+
/>
90+
)
91+
}
92+
}
93+
94+
WithAuthWrapper.propTypes = {
95+
currentUser: PropTypes.shape({
96+
firstName: PropTypes.string.isRequired,
97+
name: PropTypes.string.isRequired,
98+
username: PropTypes.string.isRequired,
99+
email: PropTypes.string.isRequired,
100+
photo: PropTypes.string.isRequired,
101+
hasPurchased: PropTypes.string
102+
}),
103+
loading: PropTypes.bool.isRequired
104+
}
105+
106+
WithAuthWrapper.displayName = wrapDisplayName(BaseComponent, 'withAuth')
107+
return withUser(WithAuthWrapper)
108+
}
109+
110+
export default withAuth

0 commit comments

Comments
 (0)