diff options
Diffstat (limited to 'frontend/src/auth.js')
-rw-r--r-- | frontend/src/auth.js | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/frontend/src/auth.js b/frontend/src/auth.js new file mode 100644 index 0000000..0ed1e1a --- /dev/null +++ b/frontend/src/auth.js @@ -0,0 +1,185 @@ +import React from 'react'; +import { Redirect, generatePath } from 'react-router-dom'; +import { isExpired } from 'react-jwt'; + +const UserContext = React.createContext(); + +class Login extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + async componentDidMount() { + let url = await this.get_oauth_url(); + this.setState({url: url}); + window.location = url; + } + render() { + if (this.state.url) { + return <div>Redirecting to <a href={this.state.url}>{this.state.url}</a></div>; + } + return <div>Logging in…</div>; + } + async get_oauth_url() { + let redirect_uri = window.location.origin + generatePath('/oauth-callback'); + let response = await fetch('/api/auth/request', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: 'redirect_uri=' + encodeURIComponent(redirect_uri), + }); + let data = await response.json(); + if (response.ok && data.url) { + return data.url; + } + } +} + +class Logout extends React.Component { + static contextType = UserContext; + constructor(props) { + super(props); + this.state = {done: false}; + } + async componentDidMount() { + await this.context.logout(); + this.setState({done: true}); + } + render() { + if (this.state.done) { + return <Redirect to='/' />; + } + return <div>Logging out…</div>; + } +} + +class OauthCallback extends React.Component { + static contextType = UserContext; + constructor(props) { + super(props); + this.state = { + error: null, + done: false, + }; + } + async componentDidMount() { + this.setState({error: null}); + let params = new URLSearchParams(this.props.location.search); + let error = params.get('error'); + if (error) { + this.setState({error: params.get('error_description')}); + return; + } + let redirect_uri = window.location.origin + generatePath('/oauth-callback'); + let response = await fetch('/api/auth/response', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: 'code=' + encodeURIComponent(params.get('code')) + '&state=' + encodeURIComponent(params.get('state')) + + '&redirect_uri=' + encodeURIComponent(redirect_uri), + }); + let data = await response.json(); + if (!response.ok) { + this.setState({error: data.message}); + return; + } + const user = await this.context.get_user(); + if (user) { + this.setState({error: null, done: true}); + window.flash({header: 'Logged in', text: 'You are now logged in as ' + user.username + '.'}); + } else { + window.flash({header: 'Login failed', text: 'Something failed during authentication.'}); + } + } + render() { + if (this.state.error) { + return this.state.error; + } + if (this.state.done) { + return <Redirect to='/' />; + } + return 'Logging in…'; + } +} + +class AuthenticationProvider extends React.Component { + constructor(props) { + super(props); + this.state = { + username: null, + is_logged_in: false, + access_token: null, + }; + this.logout = this.logout.bind(this); + this.get_user = this.get_user.bind(this); + this.get_token = this.get_token.bind(this); + } + async componentDidMount() { + await this.get_user(); + } + render() { + return <UserContext.Provider value={{ + username: this.state.username, + is_logged_in: this.state.is_logged_in, + get_user: this.get_user, + logout: this.logout, + get_token: this.get_token}}> + {this.props.children} + </UserContext.Provider>; + } + async logout() { + const was_logged_in = this.state.is_logged_in; + await fetch('/api/auth/logout', { + method: 'POST', + }); + this.setState({username: null, is_logged_in: false, access_token: null}); + if (was_logged_in) { + window.flash({header: 'Logged out', text: 'You are now logged out.'}); + } + } + async get_user() { + let token = await this.get_token(); + if (!token) { + return; + } + let response = await fetch('/api/user',{ + headers: { + Authorization: 'Bearer ' + token, + } + }); + if (response.ok) { + let data = await response.json(); + this.setState({username: data.username, is_logged_in: true}); + return data; + } else { + await this.logout(); + } + } + async refresh() { + let response = await fetch('/api/auth/refresh', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: '', + }); + if (response.ok) { + let data = await response.json(); + return data.access_token; + } + } + async get_token() { + let access_token = this.state.access_token; + let is_expired = !access_token || isExpired(access_token); + if (is_expired) { + access_token = await this.refresh(); + this.setState({access_token: access_token}); + } + return access_token; + } +} + +export { UserContext, Login, Logout, OauthCallback, AuthenticationProvider }; |