Skip to content

Commit 086249c

Browse files
Merge pull request #697 from Nemo157/multi-token
Allow users to create multiple API tokens
2 parents 9f9be0c + 66e36f9 commit 086249c

File tree

27 files changed

+1083
-193
lines changed

27 files changed

+1083
-193
lines changed

app/adapters/api-token.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import DS from 'ember-data';
2+
3+
export default DS.RESTAdapter.extend({
4+
namespace: 'me',
5+
pathForType() {
6+
return 'tokens';
7+
}
8+
});

app/components/api-token-row.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Ember from 'ember';
2+
3+
export default Ember.Component.extend({
4+
emptyName: Ember.computed.empty('api_token.name'),
5+
disableCreate: Ember.computed.or('api_token.isSaving', 'emptyName'),
6+
serverError: null,
7+
8+
didInsertElement() {
9+
if (this.get('api_token.isNew')) {
10+
this.$('input').focus();
11+
}
12+
},
13+
14+
actions: {
15+
saveToken() {
16+
this.get('api_token')
17+
.save()
18+
.then(() => this.set('serverError', null))
19+
.catch(err => {
20+
let msg;
21+
if (err.errors && err.errors[0] && err.errors[0].detail) {
22+
msg = `An error occurred while saving this token, ${err.errors[0].detail}`;
23+
} else {
24+
msg = 'An unknown error occurred while saving this token';
25+
}
26+
this.set('serverError', msg);
27+
});
28+
},
29+
revokeToken() {
30+
this.get('api_token')
31+
.destroyRecord()
32+
.catch(err => {
33+
let msg;
34+
if (err.errors && err.errors[0] && err.errors[0].detail) {
35+
msg = `An error occurred while revoking this token, ${err.errors[0].detail}`;
36+
} else {
37+
msg = 'An unknown error occurred while revoking this token';
38+
}
39+
this.set('serverError', msg);
40+
});
41+
},
42+
}
43+
});

app/controllers/me/index.js

+10-19
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,24 @@ import Ember from 'ember';
33
const { inject: { service } } = Ember;
44

55
export default Ember.Controller.extend({
6+
tokenSort: ['created_at:desc'],
7+
8+
sortedTokens: Ember.computed.sort('model.api_tokens', 'tokenSort'),
69

710
ajax: service(),
811

912
flashMessages: service(),
1013

1114
isResetting: false,
1215

16+
newTokens: Ember.computed.filterBy('model.api_tokens', 'isNew', true),
17+
disableCreate: Ember.computed.notEmpty('newTokens'),
18+
1319
actions: {
14-
resetToken() {
15-
this.set('isResetting', true);
16-
17-
this.get('ajax').put('/me/reset_token').then((d) => {
18-
this.get('model').set('api_token', d.api_token);
19-
}).catch((reason) => {
20-
let msg;
21-
if (reason.status === 403) {
22-
msg = 'A login is required to perform this action';
23-
} else {
24-
msg = 'An unknown error occurred';
25-
}
26-
this.get('flashMessages').queue(msg);
27-
// TODO: this should be an action, the route state machine
28-
// should receive signals not external transitions
29-
this.transitionToRoute('index');
30-
}).finally(() => {
31-
this.set('isResetting', false);
20+
startNewToken() {
21+
this.get('store').createRecord('api-token', {
22+
created_at: new Date(Date.now() + 2000),
3223
});
33-
}
24+
},
3425
}
3526
});

app/models/api-token.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import DS from 'ember-data';
2+
3+
export default DS.Model.extend({
4+
name: DS.attr('string'),
5+
token: DS.attr('string'),
6+
created_at: DS.attr('date'),
7+
last_used_at: DS.attr('date'),
8+
});

app/models/user.js

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export default DS.Model.extend({
44
email: DS.attr('string'),
55
name: DS.attr('string'),
66
login: DS.attr('string'),
7-
api_token: DS.attr('string'),
87
avatar: DS.attr('string'),
98
url: DS.attr('string'),
109
kind: DS.attr('string'),

app/routes/application.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ export default Ember.Route.extend({
1212
if (this.session.get('isLoggedIn') &&
1313
this.session.get('currentUser') === null) {
1414
this.get('ajax').request('/me').then((response) => {
15-
let user = this.store.push(this.store.normalize('user', response.user));
16-
user.set('api_token', response.api_token);
17-
this.session.set('currentUser', user);
15+
this.session.set('currentUser', this.store.push(this.store.normalize('user', response.user)));
1816
}).catch(() => this.session.logoutUser()).finally(() => {
1917
window.currentUserDetected = true;
2018
Ember.$(window).trigger('currentUserDetected');

app/routes/login.js

-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ export default Ember.Route.extend({
6767
}
6868

6969
let user = this.store.push(this.store.normalize('user', data.user));
70-
user.set('api_token', data.api_token);
7170
let transition = this.session.get('savedTransition');
7271
this.session.loginUser(user);
7372
if (transition) {

app/routes/me/index.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import AuthenticatedRoute from '../../mixins/authenticated-route';
33

44
export default Ember.Route.extend(AuthenticatedRoute, {
55
model() {
6-
return this.session.get('currentUser');
6+
return {
7+
user: this.session.get('currentUser'),
8+
api_tokens: this.get('store').findAll('api-token'),
9+
};
710
},
811
});

app/serializers/api-token.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import DS from 'ember-data';
2+
3+
export default DS.RESTSerializer.extend({
4+
payloadKeyFromModelName() {
5+
return 'api_token';
6+
}
7+
});

app/styles/home.scss

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
@mixin button($start, $end) {
2-
$s2: darken($start, 5%);
3-
$e2: darken($end, 5%);
2+
$start_dark: darken($start, 5%);
3+
$end_dark: darken($end, 5%);
4+
$start_light: lighten($start, 5%);
5+
$end_light: lighten($end, 5%);
6+
47
padding: 15px 40px;
58
display: inline-block;
69
color: $main-color;
@@ -17,15 +20,24 @@
1720
margin-right: 10px;
1821
}
1922

20-
&:hover { @include vertical-gradient($s2, $e2); outline: 0; }
21-
&.active { @include vertical-gradient($s2, $e2); outline: 0; }
23+
&:hover { @include vertical-gradient($start_dark, $end_dark); outline: 0; }
24+
&.active { @include vertical-gradient($start_dark, $end_dark); outline: 0; }
25+
&[disabled] {
26+
@include vertical-gradient($start_light, $end_light);
27+
color: $main-color-light;
28+
}
2229
}
2330

2431
.yellow-button {
2532
@include button(#fede9e, #fdc452);
2633
vertical-align: middle;
2734
}
2835

36+
button.small {
37+
padding: 10px 20px;
38+
@include border-radius(30px);
39+
}
40+
2941
.tan-button {
3042
@include button(rgb(232, 227, 199), rgb(214, 205, 153));
3143
}

app/styles/me.scss

+63
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,66 @@
8686
&:hover { background-color: darken($bg, 10%); }
8787
}
8888
}
89+
90+
.me-subheading {
91+
@include display-flex;
92+
.right {
93+
@include flex(2);
94+
@include display-flex;
95+
@include justify-content(flex-end);
96+
@include align-self(center);
97+
}
98+
}
99+
100+
#tokens {
101+
background-color: $main-bg-dark;
102+
@include display-flex;
103+
@include flex-direction(column);
104+
.row {
105+
width: 100%;
106+
border: 1px solid #d5d3cb;
107+
border-bottom-width: 0px;
108+
&:last-child { border-bottom-width: 1px; }
109+
padding: 10px 20px;
110+
@include display-flex;
111+
@include align-items(center);
112+
.name {
113+
@include flex(1);
114+
margin-right: 0.4em;
115+
font-weight: bold;
116+
}
117+
.dates {
118+
@include flex(content);
119+
@include display-flex;
120+
@include flex-direction(column);
121+
@include align-items(flex-end);
122+
margin-right: 0.4em;
123+
}
124+
.actions {
125+
@include display-flex;
126+
@include align-items(center);
127+
img { margin-left: 10px }
128+
}
129+
}
130+
.create-token {
131+
.name {
132+
input {
133+
width: 100%;
134+
}
135+
padding-right: 20px;
136+
margin-right: 0px;
137+
}
138+
background-color: $main-bg-dark;
139+
}
140+
.new-token {
141+
border-top-width: 0px;
142+
@include flex-direction(column);
143+
@include justify-content(stretch);
144+
}
145+
.error {
146+
border-top-width: 0px;
147+
font-weight: bold;
148+
color: rgb(216, 0, 41);
149+
padding: 0px 10px 10px 20px;
150+
}
151+
}
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<div class={{if api_token.isNew "row create-token" "row"}}>
2+
<div class='name'>
3+
{{#if api_token.isNew}}
4+
{{input
5+
type="text"
6+
placeholder="New token name"
7+
disabled=api_token.isSaving
8+
value=api_token.name
9+
autofocus=true
10+
enter="saveToken"}}
11+
{{else}}
12+
{{ api_token.name }}
13+
{{/if}}
14+
</div>
15+
16+
<div class='spacer'></div>
17+
18+
{{#unless api_token.isNew}}
19+
<div class='dates'>
20+
<div class='created-at'>
21+
<span class='small' title={{api_token.created_at}}>
22+
Created {{moment-from-now api_token.created_at}}
23+
</span>
24+
</div>
25+
{{#if api_token.last_used_at}}
26+
<div class='last_used_at'>
27+
<span class='small' title={{api_token.last_used_at}}>
28+
Last used {{moment-from-now api_token.last_used_at}}
29+
</span>
30+
</div>
31+
{{else}}
32+
<div class='last_used_at'>
33+
<span class='small'>Never used</span>
34+
</div>
35+
{{/if}}
36+
</div>
37+
{{/unless}}
38+
39+
<div class='actions'>
40+
{{#if api_token.isNew}}
41+
<button class='small yellow-button'
42+
disabled={{disableCreate}}
43+
title={{if emptyName "You must specify a name" ""}}
44+
{{action "saveToken"}}>Create</button>
45+
{{else}}
46+
<button class='small tan-button'
47+
disabled={{api_token.isSaving}}
48+
{{action "revokeToken"}}>Revoke</button>
49+
{{/if}}
50+
{{#if api_token.isSaving}}
51+
<img class='overlay' src="/assets/ajax-loader.gif" />
52+
{{/if}}
53+
</div>
54+
</div>
55+
56+
{{#if serverError}}
57+
<div class='row error'>
58+
<div>
59+
{{ serverError }}
60+
</div>
61+
</div>
62+
{{/if}}
63+
64+
{{#if api_token.token}}
65+
<div class='row new-token'>
66+
<div>
67+
Please record this token somewhere, you cannot retrieve
68+
its value again. For use on the command line you can save it to <code>~/.cargo/config</code>
69+
with:
70+
71+
<pre>cargo login {{ api_token.token }}</pre>
72+
</div>
73+
</div>
74+
{{/if}}

app/templates/me/index.hbs

+17-14
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,36 @@
99
<h2>Profile Information</h2>
1010

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

1414
<dl>
1515
<dt>Name</dt>
16-
<dd>{{ model.name }}</dd>
16+
<dd>{{ model.user.name }}</dd>
1717
<dt>GitHub Account</dt>
18-
<dd>{{ model.login }}</dd>
18+
<dd>{{ model.user.login }}</dd>
1919
<dt>Email</dt>
20-
<dd>{{ model.email }}</dd>
20+
<dd>{{ model.user.email }}</dd>
2121
</dl>
2222
</div>
2323
</div>
2424

2525
<div id='me-api'>
26-
<h2>API Access</h2>
26+
<div class='me-subheading'>
27+
<h2>API Access</h2>
28+
<div class='right'>
29+
<button class='yellow-button' disabled={{disableCreate}} {{action "startNewToken"}}>New Token</button>
30+
</div>
31+
</div>
2732

28-
<p class='api'>Your API key is <strong>{{ model.api_token }}</strong></p>
2933
<p>
3034
If you want to use package commands from the command line, you'll need a
31-
<code>~/.cargo/config</code> which can be generated with:
35+
<code>~/.cargo/config</code>, the first step to creating this
36+
is to generate a new token using the button above.
3237
</p>
33-
<pre>cargo login {{ model.api_token }}</pre>
34-
35-
<button {{action "resetToken"}} class='yellow-button'>
36-
Reset my API key
37-
</button>
3838

39-
<img src="/assets/ajax-loader.gif"
40-
class={{if isResetting "" "hidden"}} />
39+
<div id='tokens'>
40+
{{#each sortedTokens as |api_token|}}
41+
{{api-token-row api_token=api_token}}
42+
{{/each}}
43+
</div>
4144
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE api_tokens;

0 commit comments

Comments
 (0)