Skip to content

Allow users to create multiple API tokens #697

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jul 1, 2017
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 8 additions & 0 deletions app/adapters/api-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
namespace: 'me',
pathForType() {
return 'tokens';
}
});
43 changes: 43 additions & 0 deletions app/components/api-token-row.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Ember from 'ember';

export default Ember.Component.extend({
emptyName: Ember.computed.empty('api_token.name'),
disableCreate: Ember.computed.or('api_token.isSaving', 'emptyName'),
serverError: null,

didInsertElement() {
if (this.get('api_token.isNew')) {
this.$('input').focus();
}
},

actions: {
saveToken() {
this.get('api_token')
.save()
.then(() => this.set('serverError', null))
.catch(err => {
let msg;
if (err.errors && err.errors[0] && err.errors[0].detail) {
msg = `An error occurred while saving this token, ${err.errors[0].detail}`;
} else {
msg = 'An unknown error occurred while saving this token';
}
this.set('serverError', msg);
});
},
revokeToken() {
this.get('api_token')
.destroyRecord()
.catch(err => {
let msg;
if (err.errors && err.errors[0] && err.errors[0].detail) {
msg = `An error occurred while revoking this token, ${err.errors[0].detail}`;
} else {
msg = 'An unknown error occurred while revoking this token';
}
this.set('serverError', msg);
});
},
}
});
34 changes: 9 additions & 25 deletions app/controllers/me/index.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,17 @@
import Ember from 'ember';
import ajax from 'ic-ajax';

export default Ember.Controller.extend({
isResetting: false,
tokenSort: ['created_at:desc'],
sortedTokens: Ember.computed.sort('model.api_tokens', 'tokenSort'),

actions: {
resetToken() {
this.set('isResetting', true);
newTokens: Ember.computed.filterBy('model.api_tokens', 'isNew', true),
disableCreate: Ember.computed.notEmpty('newTokens'),

ajax({
dataType: 'json',
url: '/me/reset_token',
method: 'put',
}).then((d) => {
this.get('model').set('api_token', d.api_token);
}).catch((reason) => {
var msg;
if (reason.status === 403) {
msg = 'A login is required to perform this action';
} else {
msg = 'An unknown error occurred';
}
this.controllerFor('application').set('nextFlashError', msg);
// TODO: this should be an action, the route state machine
// should recieve signals not external transitions
this.transitionToRoute('index');
}).finally(() => {
this.set('isResetting', false);
actions: {
startNewToken() {
this.get('store').createRecord('api-token', {
created_at: new Date(Date.now() + 2000),
});
}
},
}
});
8 changes: 8 additions & 0 deletions app/models/api-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import DS from 'ember-data';

export default DS.Model.extend({
name: DS.attr('string'),
token: DS.attr('string'),
created_at: DS.attr('date'),
last_used_at: DS.attr('date'),
});
1 change: 0 additions & 1 deletion app/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ export default DS.Model.extend({
email: DS.attr('string'),
name: DS.attr('string'),
login: DS.attr('string'),
api_token: DS.attr('string'),
avatar: DS.attr('string'),
url: DS.attr('string'),
});
4 changes: 1 addition & 3 deletions app/routes/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ export default Ember.Route.extend({
if (this.session.get('isLoggedIn') &&
this.session.get('currentUser') === null) {
ajax('/me').then((response) => {
var user = this.store.push(this.store.normalize('user', response.user));
user.set('api_token', response.api_token);
this.session.set('currentUser', user);
this.session.set('currentUser', this.store.push(this.store.normalize('user', response.user)));
}).catch(() => this.session.logoutUser()).finally(() => {
window.currentUserDetected = true;
Ember.$(window).trigger('currentUserDetected');
Expand Down
1 change: 0 additions & 1 deletion app/routes/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export default Ember.Route.extend({
}

var user = this.store.push(this.store.normalize('user', data.user));
user.set('api_token', data.api_token);
var transition = this.session.get('savedTransition');
this.session.loginUser(user);
if (transition) {
Expand Down
5 changes: 4 additions & 1 deletion app/routes/me/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import AuthenticatedRoute from '../../mixins/authenticated-route';

export default Ember.Route.extend(AuthenticatedRoute, {
model() {
return this.session.get('currentUser');
return {
user: this.session.get('currentUser'),
api_tokens: this.get('store').findAll('api-token'),
};
},
});
7 changes: 7 additions & 0 deletions app/serializers/api-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import DS from 'ember-data';

export default DS.RESTSerializer.extend({
payloadKeyFromModelName() {
return 'api_token';
}
});
20 changes: 16 additions & 4 deletions app/styles/home.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
@mixin button($start, $end) {
$s2: darken($start, 5%);
$e2: darken($end, 5%);
$start_dark: darken($start, 5%);
$end_dark: darken($end, 5%);
$start_light: lighten($start, 5%);
$end_light: lighten($end, 5%);

padding: 15px 40px;
display: inline-block;
color: $main-color;
Expand All @@ -17,15 +20,24 @@
margin-right: 10px;
}

&:hover { @include vertical-gradient($s2, $e2); outline: 0; }
&.active { @include vertical-gradient($s2, $e2); outline: 0; }
&:hover { @include vertical-gradient($start_dark, $end_dark); outline: 0; }
&.active { @include vertical-gradient($start_dark, $end_dark); outline: 0; }
&[disabled] {
@include vertical-gradient($start_light, $end_light);
color: $main-color-light;
}
}

.yellow-button {
@include button(#fede9e, #fdc452);
vertical-align: middle;
}

button.small {
padding: 10px 20px;
@include border-radius(30px);
}

.tan-button {
@include button(rgb(232, 227, 199), rgb(214, 205, 153));
}
Expand Down
63 changes: 63 additions & 0 deletions app/styles/me.scss
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,66 @@
&:hover { background-color: darken($bg, 10%); }
}
}

.me-subheading {
@include display-flex;
.right {
@include flex(2);
@include display-flex;
@include justify-content(flex-end);
@include align-self(center);
}
}

#tokens {
background-color: $main-bg-dark;
@include display-flex;
@include flex-direction(column);
.row {
width: 100%;
border: 1px solid #d5d3cb;
border-bottom-width: 0px;
&:last-child { border-bottom-width: 1px; }
padding: 10px 20px;
@include display-flex;
@include align-items(center);
.name {
@include flex(1);
margin-right: 0.4em;
font-weight: bold;
}
.dates {
@include flex(content);
@include display-flex;
@include flex-direction(column);
@include align-items(flex-end);
margin-right: 0.4em;
}
.actions {
@include display-flex;
@include align-items(center);
img { margin-left: 10px }
}
}
.create-token {
.name {
input {
width: 100%;
}
padding-right: 20px;
margin-right: 0px;
}
background-color: $main-bg-dark;
}
.new-token {
border-top-width: 0px;
@include flex-direction(column);
@include justify-content(stretch);
}
.error {
border-top-width: 0px;
font-weight: bold;
color: rgb(216, 0, 41);
padding: 0px 10px 10px 20px;
}
}
74 changes: 74 additions & 0 deletions app/templates/components/api-token-row.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<div class={{if api_token.isNew "row create-token" "row"}}>
<div class='name'>
{{#if api_token.isNew}}
{{input
type="text"
placeholder="New token name"
disabled=api_token.isSaving
value=api_token.name
autofocus=true
enter="saveToken"}}
{{else}}
{{ api_token.name }}
{{/if}}
</div>

<div class='spacer'></div>

{{#unless api_token.isNew}}
<div class='dates'>
<div class='created-at'>
<span class='small' title={{api_token.created_at}}>
Created {{moment-from-now api_token.created_at}}
</span>
</div>
{{#if api_token.last_used_at}}
<div class='last_used_at'>
<span class='small' title={{api_token.last_used_at}}>
Last used {{moment-from-now api_token.last_used_at}}
</span>
</div>
{{else}}
<div class='last_used_at'>
<span class='small'>Never used</span>
</div>
{{/if}}
</div>
{{/unless}}

<div class='actions'>
{{#if api_token.isNew}}
<button class='small yellow-button'
disabled={{disableCreate}}
title={{if emptyName "You must specify a name" ""}}
{{action "saveToken"}}>Create</button>
{{else}}
<button class='small tan-button'
disabled={{api_token.isSaving}}
{{action "revokeToken"}}>Revoke</button>
{{/if}}
{{#if api_token.isSaving}}
<img class='overlay' src="/assets/ajax-loader.gif" />
{{/if}}
</div>
</div>

{{#if serverError}}
<div class='row error'>
<div>
{{ serverError }}
</div>
</div>
{{/if}}

{{#if api_token.token}}
<div class='row new-token'>
<div>
Please record this token somewhere, you cannot retrieve
its value again. For use on the command line you can save it to <code>~/.cargo/config</code>
with:

<pre>cargo login {{ api_token.token }}</pre>
</div>
</div>
{{/if}}
31 changes: 17 additions & 14 deletions app/templates/me/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,36 @@
<h2>Profile Information</h2>

<div class='info'>
{{#user-link user=model }} {{user-avatar user=model size='medium'}} {{/user-link}}
{{#user-link user=model.user }} {{user-avatar user=model.user size='medium'}} {{/user-link}}

<dl>
<dt>Name</dt>
<dd>{{ model.name }}</dd>
<dd>{{ model.user.name }}</dd>
<dt>GitHub Account</dt>
<dd>{{ model.login }}</dd>
<dd>{{ model.user.login }}</dd>
<dt>Email</dt>
<dd>{{ model.email }}</dd>
<dd>{{ model.user.email }}</dd>
</dl>
</div>
</div>

<div id='me-api'>
<h2>API Access</h2>
<div class='me-subheading'>
<h2>API Access</h2>
<div class='right'>
<button class='yellow-button' disabled={{disableCreate}} {{action "startNewToken"}}>New Token</button>
</div>
</div>

<p class='api'>Your API key is <strong>{{ model.api_token }}</strong></p>
<p>
If you want to use package commands from the command line, you'll need a
<code>~/.cargo/config</code> which can be generated with:
<code>~/.cargo/config</code>, the first step to creating this
is to generate a new token using the button above.
</p>
<pre>cargo login {{ model.api_token }}</pre>

<button {{action "resetToken"}} class='yellow-button'>
Reset my API key
</button>

<img src="/assets/ajax-loader.gif"
class={{if isResetting "" "hidden"}} />
<div id='tokens'>
{{#each sortedTokens as |api_token|}}
{{api-token-row api_token=api_token}}
{{/each}}
</div>
</div>
1 change: 1 addition & 0 deletions migrations/20170428154714_multiple_api_tokens/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE api_tokens;
Loading