-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #433 from smacker/add_router
add client-side router
- Loading branch information
Showing
12 changed files
with
323 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,107 +1,116 @@ | ||
import React, { Component, ReactElement } from 'react'; | ||
import Token from './services/token'; | ||
import * as api from './api'; | ||
import React, { Component } from 'react'; | ||
import { | ||
BrowserRouter as Router, | ||
Route, | ||
Redirect, | ||
Link, | ||
RouteProps, | ||
RouteComponentProps | ||
} from 'react-router-dom'; | ||
import { User } from './services/auth'; | ||
import Auth from './services/auth'; | ||
import Loader from './components/Loader'; | ||
import Callback from './Callback'; | ||
import './App.css'; | ||
|
||
function Loader() { | ||
return <div>loading...</div>; | ||
function Login() { | ||
return ( | ||
<header className="App-header"> | ||
<a className="App-link" href={Auth.loginUrl}> | ||
Login using Github | ||
</a> | ||
</header> | ||
); | ||
} | ||
|
||
interface ErrorProps { | ||
errors: string[]; | ||
function Logout() { | ||
Auth.logout(); | ||
|
||
return <Redirect to="/" />; | ||
} | ||
|
||
function Errors({ errors }: ErrorProps) { | ||
return <div>{errors.join(',')}</div>; | ||
interface IndexProps { | ||
user: User; | ||
} | ||
|
||
function Login() { | ||
function Index({ user }: IndexProps) { | ||
return ( | ||
<header className="App-header"> | ||
<a className="App-link" href={api.loginUrl}> | ||
Login using Github | ||
</a> | ||
Hello {user.name}! <Link to="/logout">Logout</Link> | ||
</header> | ||
); | ||
} | ||
|
||
interface HelloProps { | ||
name: string; | ||
interface PrivateRouteState { | ||
isAuthenticated: boolean | undefined; | ||
} | ||
|
||
function Hello({ name }: HelloProps) { | ||
return <header className="App-header">Hello {name}!</header>; | ||
interface PrivateRouteComponentProps<P> extends RouteComponentProps { | ||
user: User | null; | ||
} | ||
|
||
interface AppState { | ||
// we need undefined state for initial render | ||
loggedIn: boolean | undefined; | ||
name: string; | ||
errors: string[]; | ||
interface PrivateRouteProps extends RouteProps { | ||
component: | ||
| React.ComponentType<PrivateRouteComponentProps<any>> | ||
| React.ComponentType<any>; | ||
} | ||
|
||
class App extends Component<{}, AppState> { | ||
constructor(props: {}) { | ||
super(props); | ||
|
||
this.fetchState = this.fetchState.bind(this); | ||
function PrivateRoute({ component, ...rest }: PrivateRouteProps) { | ||
class CheckAuthComponent extends Component< | ||
RouteComponentProps, | ||
PrivateRouteState | ||
> { | ||
constructor(props: RouteComponentProps) { | ||
super(props); | ||
|
||
this.state = { | ||
loggedIn: undefined, | ||
name: '', | ||
errors: [] | ||
}; | ||
} | ||
|
||
componentDidMount() { | ||
// TODO: add router and use it instead of this "if" | ||
if (window.location.pathname === '/callback') { | ||
api | ||
.callback(window.location.search) | ||
.then(resp => { | ||
Token.set(resp.token); | ||
window.history.replaceState({}, '', '/'); | ||
}) | ||
.then(this.fetchState) | ||
.catch(errors => this.setState({ errors })); | ||
return; | ||
this.state = { isAuthenticated: undefined }; | ||
} | ||
|
||
if (!Token.exists()) { | ||
this.setState({ loggedIn: false }); | ||
return; | ||
componentDidMount() { | ||
Auth.isAuthenticated | ||
.then(ok => this.setState({ isAuthenticated: ok })) | ||
.catch(() => this.setState({ isAuthenticated: false })); | ||
} | ||
|
||
// ignore error here, just ask user to re-login | ||
// it would cover all cases like expired token, changes on backend and so on | ||
this.fetchState().catch(err => console.error(err)); | ||
} | ||
|
||
fetchState() { | ||
return api | ||
.me() | ||
.then(resp => this.setState({ loggedIn: true, name: resp.name })) | ||
.catch(err => { | ||
this.setState({ loggedIn: false }); | ||
|
||
throw err; | ||
}); | ||
render() { | ||
if (!component) { | ||
return null; | ||
} | ||
|
||
if (this.state.isAuthenticated === true) { | ||
const Component = component; | ||
return <Component {...this.props} user={Auth.user} />; | ||
} | ||
|
||
if (this.state.isAuthenticated === false) { | ||
return ( | ||
<Redirect | ||
to={{ | ||
pathname: '/login', | ||
state: { from: this.props.location } | ||
}} | ||
/> | ||
); | ||
} | ||
|
||
return <Loader />; | ||
} | ||
} | ||
|
||
render() { | ||
const { loggedIn, name, errors } = this.state; | ||
|
||
let content: ReactElement<any>; | ||
if (errors.length) { | ||
content = <Errors errors={errors} />; | ||
} else if (typeof loggedIn === 'undefined') { | ||
content = <Loader />; | ||
} else { | ||
content = loggedIn ? <Hello name={name} /> : <Login />; | ||
} | ||
return <Route {...rest} component={CheckAuthComponent} />; | ||
} | ||
|
||
return <div className="App">{content}</div>; | ||
} | ||
function AppRouter() { | ||
return ( | ||
<Router> | ||
<div className="App"> | ||
<PrivateRoute path="/" exact component={Index} /> | ||
<Route path="/login" component={Login} /> | ||
<Route path="/logout" component={Logout} /> | ||
<Route path="/callback" component={Callback} /> | ||
</div> | ||
</Router> | ||
); | ||
} | ||
|
||
export default App; | ||
export default AppRouter; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import React, { Component } from 'react'; | ||
import { Redirect } from 'react-router-dom'; | ||
import * as H from 'history'; | ||
import Auth from './services/auth'; | ||
import * as api from './api'; | ||
import Loader from './components/Loader'; | ||
import Errors from './components/Errors'; | ||
|
||
interface CallbackProps { | ||
location: H.Location; | ||
} | ||
|
||
interface CallbackState { | ||
success: boolean; | ||
errors: string[]; | ||
} | ||
|
||
class Callback extends Component<CallbackProps, CallbackState> { | ||
constructor(props: CallbackProps) { | ||
super(props); | ||
|
||
this.state = { | ||
success: false, | ||
errors: [] | ||
}; | ||
} | ||
|
||
componentDidMount() { | ||
Auth.callback(this.props.location.search) | ||
.then(() => this.setState({ success: true })) | ||
.catch(errors => this.setState({ errors })); | ||
} | ||
|
||
render() { | ||
const { errors, success } = this.state; | ||
|
||
if (errors.length) { | ||
return <Errors errors={errors} />; | ||
} | ||
|
||
if (success) { | ||
const { from } = this.props.location.state || { from: { pathname: '/' } }; | ||
return <Redirect to={from} />; | ||
} | ||
|
||
return <Loader />; | ||
} | ||
} | ||
|
||
export default Callback; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import React from 'react'; | ||
|
||
interface ErrorProps { | ||
errors: string[]; | ||
} | ||
|
||
function Errors({ errors }: ErrorProps) { | ||
return <div>{errors.join(',')}</div>; | ||
} | ||
|
||
export default Errors; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import React from 'react'; | ||
|
||
function Loader() { | ||
return <div>loading...</div>; | ||
} | ||
|
||
export default Loader; |
Oops, something went wrong.