Skip to content
This repository has been archived by the owner on Sep 12, 2022. It is now read-only.

New Feature - Create and manage "Personal Access Tokens" (API Tokens) from "Settings Page" #789

Merged
merged 19 commits into from
Aug 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
### Changed
- Fix format script and format codebase ([#782](https://github.com/cyverse/troposphere/pull/782))
- Travis will also check that the code is formatted from now on

### Added
- Add ability to create, edit, and delete "Personal Access Tokens" from the advanced section on the "settings" view ([#789](https://github.com/cyverse/troposphere/pull/789))
## [v33-0](https://github.com/cyverse/troposphere/compare/v32-0...v33-0) - 2018-08-06
### Changed
- Suggest adopting a changelog format
Expand Down Expand Up @@ -64,7 +65,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
([#750](https://github.com/cyverse/troposphere/pull/750))
- Solves problem where requests were being 'approved' while the resources
were not being updated

### Changed
- Change ./manage.py maintenance to be non-interactive
([#769](https://github.com/cyverse/troposphere/pull/769))
Expand Down
58 changes: 58 additions & 0 deletions troposphere/static/js/actions/APITokenActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import APITokenConstants from "constants/APITokenConstants";
import APIToken from "models/APIToken";
import Utils from "./Utils";

export default {
create: name => {
if (!name) throw new Error("Missing Token name");
let apiToken = new APIToken({
name
});

let promise = Promise.resolve(apiToken.save());
promise.then(() =>
Utils.dispatch(APITokenConstants.ADD_TOKEN, {apiToken})
);
promise.catch(response =>
Utils.displayError({title: "Error creating token", response})
);
return promise;
},
update: (apiToken, newAttributes) => {
let prevAttributes = Object.assign({}, apiToken.attributes);
apiToken.set(newAttributes);
// Update token optimistically
Utils.dispatch(APITokenConstants.UPDATE_TOKEN, {apiToken});
let promise = Promise.resolve(
apiToken.save(newAttributes, {patch: true})
);
promise.then(() =>
Utils.dispatch(APITokenConstants.UPDATE_TOKEN, {apiToken})
);
promise.catch(response => {
Utils.displayError({
title: "Token could not be saved",
response
});
apiToken.set(prevAttributes);
Utils.dispatch(APITokenConstants.UPDATE_TOKEN, {apiToken});
});
return promise;
},
destroy: apiToken => {
// Destroy token optimistically
Utils.dispatch(APITokenConstants.REMOVE_TOKEN, {apiToken});
let promise = Promise.resolve(apiToken.destroy());
promise.then(() =>
Utils.dispatch(APITokenConstants.REMOVE_TOKEN, {apiToken})
);
promise.catch(response => {
Utils.dispatch(APITokenConstants.ADD_TOKEN, {apiToken});
Utils.displayError({
title: "Error deleting token",
response
});
});
return promise;
}
};
1 change: 1 addition & 0 deletions troposphere/static/js/bootstrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ stores.AllocationSourceStore = require("stores/AllocationSourceStore");
import actions from "actions";

actions.AccountActions = require("actions/AccountActions");
actions.APITokenActions = require("actions/APITokenActions");
actions.BadgeActions = require("actions/BadgeActions");
actions.GroupActions = require("actions/GroupActions");
actions.ExternalLinkActions = require("actions/ExternalLinkActions");
Expand Down
12 changes: 12 additions & 0 deletions troposphere/static/js/collections/APITokenCollection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Backbone from "backbone";

import APIToken from "models/APIToken";
import globals from "globals";

export default Backbone.Collection.extend({
model: APIToken,
url: globals.API_V2_ROOT + "/access_tokens",
parse: function(data) {
return data.results;
}
});
183 changes: 183 additions & 0 deletions troposphere/static/js/components/modals/api_token/APITokenCreate.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import React from "react";
import {RaisedButton, CircularProgress} from "material-ui";
import WarningIcon from "material-ui/svg-icons/alert/warning";

import BootstrapModalMixin from "components/mixins/BootstrapModalMixin";
import actions from "actions";
import CopyButton from "components/common/ui/CopyButton";

export default React.createClass({
mixins: [BootstrapModalMixin],

getInitialState() {
return {
errorMsg: "",
name: "",
hash: "",
successView: false,
isSubmitting: false
};
},

updateName(e) {
this.setState({
name: e.target.value
});
},

onSubmit() {
const {name} = this.state;
this.setState({
isSubmitting: true
});
let promise = actions.APITokenActions.create(name.trim());
promise.then(this.onSuccess);
promise.catch(this.onError);
},

onSuccess(response) {
this.setState({
isSubmitting: false,
successView: true,
hash: response.token
});
},

onError(response) {
this.hide();
},

renderFormView() {
return (
<div>
<p>
Give your Access Token a name to help you remember where you
are using it. After you create your token you will only have
one chance to copy it somewhere safe.
</p>
<div className="form-group">
<label className="control-label">Name</label>
<div>
<input
type="text"
placeholder="My Token Name"
className="form-control"
onChange={this.updateName}
value={this.state.name}
/>
* Name Required
</div>
</div>
</div>
);
},

successStyles() {
return {
infoBlock: {display: "flex", marginBottom: "16px"},
infoBlockIcon: {marginRight: "16px", flex: "1 0 24px"},
figureCaption: {fontWeight: "600"},
figureBody: {
borderRadius: "4px",
border: "solid rgba(0,0,0,.3) 1px",
padding: "8px",
fontSize: "16px",
fontFamily: "monospace"
}
};
},

renderSuccessView() {
const {name, hash} = this.state;
const styles = this.successStyles();
return (
<div>
<h2
style={{marginBottom: "24px"}}
className="t-title">{`Token "${name}" Created Successfully!`}</h2>
<div style={styles.infoBlock}>
<WarningIcon style={styles.infoBlockIcon} />
<p>
Make sure you copy this token now. You will not have
another chance after this modal is closed!
</p>
</div>
<figure>
<figcaption style={styles.figureCaption}>
Your Public Access Token
</figcaption>
<div style={styles.figureBody}>
{hash}
<CopyButton text={hash} />
</div>
</figure>
</div>
);
},

renderSubmitButton() {
if (this.state.successView) {
return (
<RaisedButton
primary
onClick={this.hide}
label="Back to Settings"
/>
);
} else if (this.state.isSubmitting) {
return (
<RaisedButton
primary
disabled
icon={<CircularProgress size={24} color="rgba(0,0,0,.3)" />}
label="creating..."
/>
);
} else {
return (
<RaisedButton
primary
disabled={!this.state.name}
onClick={this.onSubmit}
label="Create Token"
/>
);
}
},

render() {
const {successView, isSubmitting} = this.state;

return (
<div className="modal fade">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
{this.renderCloseButton}
<h1 className="t-title">
Create Personal Access Token
</h1>
</div>
<div
style={{minHeight: "300px"}}
className="modal-body">
{successView
? this.renderSuccessView()
: this.renderFormView()}
</div>
<div className="modal-footer">
{successView || isSubmitting ? null : (
<RaisedButton
style={{marginRight: "16px"}}
onClick={this.hide}
label="Cancel"
/>
)}
{this.renderSubmitButton()}
</div>
</div>
</div>
</div>
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from "react";
import Backbone from "backbone";
import {RaisedButton} from "material-ui";
import WarningIcon from "material-ui/svg-icons/alert/warning";

import actions from "actions";
import BootstrapModalMixin from "components/mixins/BootstrapModalMixin";
import subscribe from "utilities/subscribe";

const APITokenDelete = React.createClass({
propTypes: {
token: React.PropTypes.instanceOf(Backbone.Model)
},

mixins: [BootstrapModalMixin],

onSubmit() {
this.hide();
let token = this.props.token;
actions.APITokenActions.destroy(token);
},

render() {
const {token} = this.props;
const name = token.get("name");
return (
<div className="modal fade">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
{this.renderCloseButton()}
<h1 className="t-title">
Delete Personal Access Token
</h1>
</div>
<div
style={{minHeight: "300px"}}
className="modal-body">
<div style={{display: "flex"}}>
<WarningIcon
style={{
marginRight: "16px",
flex: "1 0 24px"
}}
/>{" "}
<p>
{`Are you sure you want to delete Access Token "${name}"? Any applications using this Token will not be able to connect to your account`}
</p>
</div>
</div>
<div className="modal-footer">
<RaisedButton
style={{marginRight: "16px"}}
onClick={this.hide}
label="Cancel"
/>
<RaisedButton
primary
onClick={this.onSubmit}
label="Yes, Delete Token"
/>
</div>
</div>
</div>
</div>
);
}
});

export default subscribe(APITokenDelete, ["APITokenStore"]);
Loading