insomnia/app/ui/components/editors/auth/o-auth-2.js
2017-06-05 14:06:12 -07:00

403 lines
12 KiB
JavaScript

import React, {PropTypes, PureComponent} from 'react';
import moment from 'moment';
import autobind from 'autobind-decorator';
import OneLineEditor from '../../codemirror/one-line-editor';
import * as misc from '../../../../common/misc';
import {GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_CLIENT_CREDENTIALS, GRANT_TYPE_PASSWORD, GRANT_TYPE_IMPLICIT} from '../../../../network/o-auth-2/constants';
import authorizationUrls from '../../../../datasets/authorization-urls';
import accessTokenUrls from '../../../../datasets/access-token-urls';
import getAccessToken from '../../../../network/o-auth-2/get-token';
import * as models from '../../../../models';
import Link from '../../base/link';
import {trackEvent} from '../../../../analytics/index';
const getAuthorizationUrls = () => authorizationUrls;
const getAccessTokenUrls = () => accessTokenUrls;
@autobind
class OAuth2 extends PureComponent {
constructor (props) {
super(props);
this.state = {
error: '',
loading: false
};
this._handleChangeProperty = misc.debounce(this._handleChangeProperty, 500);
}
async _handleUpdateAccessToken (e) {
const {oAuth2Token} = this.props;
const accessToken = e.target.value;
if (oAuth2Token) {
await models.oAuth2Token.update(this.props.oAuth2Token, {accessToken});
} else {
await models.oAuth2Token.create({accessToken, parentId: this.props.request._id});
}
}
async _handleUpdateRefreshToken (e) {
const {oAuth2Token} = this.props;
const refreshToken = e.target.value;
if (oAuth2Token) {
await models.oAuth2Token.update(this.props.oAuth2Token, {refreshToken});
} else {
await models.oAuth2Token.create({refreshToken, parentId: this.props.request._id});
}
}
async _handleClearTokens () {
const oAuth2Token = await models.oAuth2Token.getByParentId(this.props.request._id);
if (oAuth2Token) {
await models.oAuth2Token.remove(oAuth2Token);
}
}
async _handleRefreshToken () {
// First, clear the state and the current tokens
this.setState({error: '', loading: true});
const {request} = this.props;
try {
const authentication = await this.props.handleRender(request.authentication);
const oAuth2Token = await getAccessToken(request._id, authentication, true);
this.setState({token: oAuth2Token, loading: false});
} catch (err) {
await this._handleClearTokens(); // Clear existing tokens if there's an error
this.setState({error: err.message, loading: false});
}
}
_handleChangeProperty (property, value) {
const {request} = this.props;
const authentication = Object.assign({}, request.authentication, {[property]: value});
this.props.onChange(authentication);
}
_handleChangeClientId (value) {
this._handleChangeProperty('clientId', value);
}
_handleChangeCredentialsInBody (e) {
this._handleChangeProperty('credentialsInBody', e.target.value === 'true');
}
_handleChangeClientSecret (value) {
this._handleChangeProperty('clientSecret', value);
}
_handleChangeAuthorizationUrl (value) {
this._handleChangeProperty('authorizationUrl', value);
}
_handleChangeAccessTokenUrl (value) {
this._handleChangeProperty('accessTokenUrl', value);
}
_handleChangeRedirectUrl (value) {
this._handleChangeProperty('redirectUrl', value);
}
_handleChangeScope (value) {
this._handleChangeProperty('scope', value);
}
_handleChangeState (value) {
this._handleChangeProperty('state', value);
}
_handleChangeUsername (value) {
this._handleChangeProperty('username', value);
}
_handleChangePassword (value) {
this._handleChangeProperty('password', value);
}
_handleChangeGrantType (e) {
trackEvent('OAuth 2', 'Change Grant Type', e.target.value);
this._handleChangeProperty('grantType', e.target.value);
}
renderInputRow (label, property, onChange, handleAutocomplete = null) {
const {handleRender, handleGetRenderContext, request} = this.props;
const id = label.replace(/ /g, '-');
const type = !this.props.showPasswords && property === 'password' ? 'password' : 'text';
return (
<tr key={id}>
<td className="pad-right no-wrap valign-middle">
<label htmlFor={id} className="label--small no-pad">{label}</label>
</td>
<td className="wide">
<div className="form-control form-control--underlined no-margin">
<OneLineEditor
id={id}
type={type}
onChange={onChange}
defaultValue={request.authentication[property] || ''}
render={handleRender}
getAutocompleteConstants={handleAutocomplete}
getRenderContext={handleGetRenderContext}
/>
</div>
</td>
</tr>
);
}
renderSelectRow (label, property, options, onChange) {
const {request} = this.props;
const id = label.replace(/ /g, '-');
const value = request.authentication.hasOwnProperty(property)
? request.authentication[property]
: options[0];
return (
<tr key={id}>
<td className="pad-right no-wrap valign-middle">
<label htmlFor={id} className="label--small no-pad">{label}</label>
</td>
<td className="wide">
<div className="form-control form-control--outlined no-margin">
<select id={id} onChange={onChange} value={value}>
{options.map(({name, value}) => (
<option key={value} value={value}>{name}</option>
))}
</select>
</div>
</td>
</tr>
);
}
renderGrantTypeFields (grantType) {
let fields = null;
const clientId = this.renderInputRow(
'Client ID',
'clientId',
this._handleChangeClientId
);
const clientSecret = this.renderInputRow(
'Client Secret',
'clientSecret',
this._handleChangeClientSecret
);
const authorizationUrl = this.renderInputRow(
'Authorization URL',
'authorizationUrl',
this._handleChangeAuthorizationUrl,
getAuthorizationUrls
);
const accessTokenUrl = this.renderInputRow(
'Access Token URL',
'accessTokenUrl',
this._handleChangeAccessTokenUrl,
getAccessTokenUrls
);
const redirectUri = this.renderInputRow(
'Redirect URL',
'redirectUrl',
this._handleChangeRedirectUrl
);
const state = this.renderInputRow(
'State',
'state',
this._handleChangeState
);
const scope = this.renderInputRow(
'Scope',
'scope',
this._handleChangeScope
);
const username = this.renderInputRow(
'Username',
'username',
this._handleChangeUsername
);
const password = this.renderInputRow(
'Password',
'password',
this._handleChangePassword
);
const credentialsInBody = this.renderSelectRow(
'Client Credentials',
'credentialsInBody',
[
{name: 'As Basic Auth Header (default)', value: 'false'},
{name: 'In Request Body', value: 'true'}
],
this._handleChangeCredentialsInBody
);
if (grantType === GRANT_TYPE_AUTHORIZATION_CODE) {
fields = [
authorizationUrl,
accessTokenUrl,
clientId,
clientSecret,
credentialsInBody,
redirectUri,
scope,
state
];
} else if (grantType === GRANT_TYPE_CLIENT_CREDENTIALS) {
fields = [
accessTokenUrl,
clientId,
clientSecret,
credentialsInBody,
scope
];
} else if (grantType === GRANT_TYPE_PASSWORD) {
fields = [
username,
password,
accessTokenUrl,
clientId,
clientSecret,
credentialsInBody,
scope
];
} else if (grantType === GRANT_TYPE_IMPLICIT) {
fields = [
authorizationUrl,
clientId,
redirectUri,
scope,
state
];
}
return fields;
}
renderExpireAt (token) {
if (!token) {
return null;
}
if (!token.expiresAt) {
return '(never expires)';
}
const expiresAt = new Date(token.expiresAt);
const str = moment(expiresAt).fromNow();
return <span title={expiresAt.toString()}>(expires {str})</span>;
}
render () {
const {request, oAuth2Token: tok} = this.props;
const {loading, error} = this.state;
const expireLabel = this.renderExpireAt(tok);
return (
<div className="pad">
<table>
<tbody>
{this.renderSelectRow('Grant Type', 'grantType', [
{name: 'Authorization Code', value: GRANT_TYPE_AUTHORIZATION_CODE},
{name: 'Implicit', value: GRANT_TYPE_IMPLICIT},
{name: 'Resource Owner Password Credentials', value: GRANT_TYPE_PASSWORD},
{name: 'Client Credentials', value: GRANT_TYPE_CLIENT_CREDENTIALS}
], this._handleChangeGrantType)}
{this.renderGrantTypeFields(request.authentication.grantType)}
</tbody>
</table>
<div className="pad-top-sm">
{/* Handle major errors */}
{error && (
<p className="notice warning margin-bottom">
{error}
</p>
)}
{/* Handle minor errors */}
{(tok && tok.error) && (
<div className="notice error margin-bottom">
<h2 className="no-margin-top txt-lg force-wrap">
{tok.error}
</h2>
<p>
{tok.errorDescription || 'no description provided'}
{tok.errorUri && (
<span>&nbsp;
<Link href={tok.errorUri} title={tok.errorUri}>
<i className="fa fa-question-circle"/>
</Link>
</span>
)}
</p>
</div>
)}
<div className="form-control form-control--outlined">
<label>
<small>
Refresh Token
</small>
<input value={(tok && tok.refreshToken) || ''}
placeholder="n/a"
onChange={this._handleUpdateRefreshToken}/>
</label>
</div>
<div className="form-control form-control--outlined">
<label>
<small>
Access Token {tok ? <em>{expireLabel}</em> : null}
</small>
<input value={(tok && tok.accessToken) || ''}
placeholder="n/a"
onChange={this._handleUpdateAccessToken}/>
</label>
</div>
<div className="pad-top text-right">
{tok && (
<button className="btn btn--clicky" onClick={this._handleClearTokens}>
Clear Tokens
</button>
)}
&nbsp;&nbsp;
<button className="btn btn--clicky"
onClick={this._handleRefreshToken}
disabled={loading}>
{loading
? (tok ? 'Refreshing...' : 'Fetching...')
: (tok ? 'Refresh Token' : 'Fetch Tokens')
}
</button>
</div>
</div>
</div>
);
}
}
OAuth2.propTypes = {
handleRender: PropTypes.func.isRequired,
handleGetRenderContext: PropTypes.func.isRequired,
handleUpdateSettingsShowPasswords: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
request: PropTypes.object.isRequired,
showPasswords: PropTypes.bool.isRequired,
// Optional
oAuth2Token: PropTypes.object
};
export default OAuth2;