Skip to content

Commit 61f6dd8

Browse files
committed
settings/tokens/new: Add "Expiration" options
1 parent e2695c6 commit 61f6dd8

File tree

4 files changed

+175
-2
lines changed

4 files changed

+175
-2
lines changed

app/controllers/settings/tokens/new.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export default class NewTokenController extends Controller {
1616

1717
@tracked name;
1818
@tracked nameInvalid;
19+
@tracked expirySelection;
20+
@tracked expiryDateInput;
21+
@tracked expiryDateInvalid;
1922
@tracked scopes;
2023
@tracked scopesInvalid;
2124
@tracked crateScopes;
@@ -29,6 +32,31 @@ export default class NewTokenController extends Controller {
2932
this.reset();
3033
}
3134

35+
get today() {
36+
return new Date().toISOString().slice(0, 10);
37+
}
38+
39+
get expiryDate() {
40+
if (this.expirySelection === 'none') return null;
41+
if (this.expirySelection === 'custom') {
42+
if (!this.expiryDateInput) return null;
43+
44+
let now = new Date();
45+
let timeSuffix = now.toISOString().slice(10);
46+
return new Date(this.expiryDateInput + timeSuffix);
47+
}
48+
49+
let date = new Date();
50+
date.setDate(date.getDate() + Number(this.expirySelection));
51+
return date;
52+
}
53+
54+
get expiryDescription() {
55+
return this.expirySelection === 'none'
56+
? 'The token will never expire'
57+
: `The token will expire on ${this.expiryDate.toLocaleDateString(undefined, { dateStyle: 'long' })}`;
58+
}
59+
3260
@action isScopeSelected(id) {
3361
return this.scopes.includes(id);
3462
}
@@ -46,6 +74,7 @@ export default class NewTokenController extends Controller {
4674
name,
4775
endpoint_scopes: scopes,
4876
crate_scopes: crateScopes,
77+
expired_at: this.expiryDate,
4978
});
5079

5180
try {
@@ -68,23 +97,36 @@ export default class NewTokenController extends Controller {
6897
reset() {
6998
this.name = '';
7099
this.nameInvalid = false;
100+
this.expirySelection = 'none';
101+
this.expiryDateInput = null;
102+
this.expiryDateInvalid = false;
71103
this.scopes = [];
72104
this.scopesInvalid = false;
73105
this.crateScopes = TrackedArray.of();
74106
}
75107

76108
validate() {
77109
this.nameInvalid = !this.name;
110+
this.expiryDateInvalid = this.expirySelection === 'custom' && !this.expiryDateInput;
78111
this.scopesInvalid = this.scopes.length === 0;
79112
let crateScopesValid = this.crateScopes.map(pattern => pattern.validate(false)).every(Boolean);
80113

81-
return !this.nameInvalid && !this.scopesInvalid && crateScopesValid;
114+
return !this.nameInvalid && !this.expiryDateInvalid && !this.scopesInvalid && crateScopesValid;
82115
}
83116

84117
@action resetNameValidation() {
85118
this.nameInvalid = false;
86119
}
87120

121+
@action updateExpirySelection(event) {
122+
this.expiryDateInput = this.expiryDate?.toISOString().slice(0, 10);
123+
this.expirySelection = event.target.value;
124+
}
125+
126+
@action resetExpiryDateValidation() {
127+
this.expiryDateInvalid = false;
128+
}
129+
88130
@action toggleScope(id) {
89131
this.scopes = this.scopes.includes(id) ? this.scopes.filter(it => it !== id) : [...this.scopes, id];
90132
this.scopesInvalid = false;

app/styles/settings/tokens/new.module.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,26 @@
6262
width: 100%;
6363
}
6464

65+
.expiry-select {
66+
composes: base-input;
67+
68+
padding-right: var(--space-m);
69+
background-image: url("data:image/svg+xml,<svg viewBox='0 0 16 16' fill='currentColor' xmlns='http://www.w3.org/2000/svg'><path d='M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z'/></svg>");
70+
background-repeat: no-repeat;
71+
background-position: calc(100% - var(--space-2xs)) center;
72+
background-size: 10px;
73+
appearance: none;
74+
}
75+
76+
.expiry-date-input {
77+
composes: base-input;
78+
}
79+
80+
.expiry-description {
81+
margin-left: var(--space-2xs);
82+
font-size: 0.9em;
83+
}
84+
6585
.scopes-list {
6686
list-style: none;
6787
padding: 0;

app/templates/settings/tokens/new.hbs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,46 @@
2626
{{/let}}
2727
</div>
2828

29+
<div local-class="form-group" data-test-expiry-group>
30+
{{#let (unique-id) as |id|}}
31+
<label for={{id}} local-class="form-group-name">Expiration</label>
32+
33+
<select
34+
id={{id}}
35+
disabled={{this.saveTokenTask.isRunning}}
36+
local-class="expiry-select"
37+
data-test-expiry
38+
{{on "change" this.updateExpirySelection}}
39+
>
40+
<option value="none" selected>No expiration</option>
41+
<option value="7">7 days</option>
42+
<option value="30">30 days</option>
43+
<option value="60">60 days</option>
44+
<option value="90">90 days</option>
45+
<option value="365">365 days</option>
46+
<option value="custom">Custom...</option>
47+
</select>
48+
{{/let}}
49+
50+
{{#if (eq this.expirySelection "custom")}}
51+
<Input
52+
@type="date"
53+
@value={{this.expiryDateInput}}
54+
min={{this.today}}
55+
disabled={{this.saveTokenTask.isRunning}}
56+
aria-invalid={{if this.expiryDateInvalid "true" "false"}}
57+
aria-label="Custom expiration date"
58+
local-class="expiry-date-input"
59+
data-test-expiry-date
60+
{{on "input" this.resetExpiryDateValidation}}
61+
/>
62+
{{else}}
63+
<span local-class="expiry-description" data-test-expiry-description>
64+
{{this.expiryDescription}}
65+
</span>
66+
{{/if}}
67+
</div>
68+
2969
<div local-class="form-group" data-test-scopes-group>
3070
<div local-class="form-group-name">
3171
Scopes

tests/routes/settings/tokens/new-test.js

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { click, currentURL, fillIn, waitFor } from '@ember/test-helpers';
1+
import { click, currentURL, fillIn, select, waitFor } from '@ember/test-helpers';
22
import { module, test } from 'qunit';
33

44
import { defer } from 'rsvp';
@@ -60,6 +60,7 @@ module('/settings/tokens/new', function (hooks) {
6060
let token = this.server.schema.apiTokens.findBy({ name: 'token-name' });
6161
assert.ok(Boolean(token), 'API token has been created in the backend database');
6262
assert.strictEqual(token.name, 'token-name');
63+
assert.strictEqual(token.expiredAt, null);
6364
assert.strictEqual(token.crateScopes, null);
6465
assert.deepEqual(token.endpointScopes, ['publish-update']);
6566

@@ -68,6 +69,7 @@ module('/settings/tokens/new', function (hooks) {
6869
assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token);
6970
assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update');
7071
assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').doesNotExist();
72+
assert.dom('[data-test-api-token="1"] [data-test-expired-at]').doesNotExist();
7173
});
7274

7375
test('crate scopes', async function (assert) {
@@ -138,6 +140,75 @@ module('/settings/tokens/new', function (hooks) {
138140
assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token);
139141
assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update and yank');
140142
assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').hasText('Crates: serde-* and serde');
143+
assert.dom('[data-test-api-token="1"] [data-test-expired-at]').doesNotExist();
144+
});
145+
146+
test('token expiry', async function (assert) {
147+
prepare(this);
148+
149+
await visit('/settings/tokens/new');
150+
assert.strictEqual(currentURL(), '/settings/tokens/new');
151+
assert.dom('[data-test-expiry-description]').hasText('The token will never expire');
152+
153+
await fillIn('[data-test-name]', 'token-name');
154+
await select('[data-test-expiry]', '30');
155+
156+
let expiryDate = new Date('2017-12-20');
157+
let expectedDate = expiryDate.toLocaleDateString(undefined, { dateStyle: 'long' });
158+
let expectedDescription = `The token will expire on ${expectedDate}`;
159+
assert.dom('[data-test-expiry-description]').hasText(expectedDescription);
160+
161+
await click('[data-test-scope="publish-update"]');
162+
await click('[data-test-generate]');
163+
164+
let token = this.server.schema.apiTokens.findBy({ name: 'token-name' });
165+
assert.ok(Boolean(token), 'API token has been created in the backend database');
166+
assert.strictEqual(token.name, 'token-name');
167+
assert.strictEqual(token.expiredAt.slice(0, 10), '2017-12-20');
168+
assert.strictEqual(token.crateScopes, null);
169+
assert.deepEqual(token.endpointScopes, ['publish-update']);
170+
171+
assert.strictEqual(currentURL(), '/settings/tokens');
172+
assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name');
173+
assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token);
174+
assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update');
175+
assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').doesNotExist();
176+
assert.dom('[data-test-api-token="1"] [data-test-expired-at]').hasText('Expires in about 1 month');
177+
});
178+
179+
test('token expiry with custom date', async function (assert) {
180+
prepare(this);
181+
182+
await visit('/settings/tokens/new');
183+
assert.strictEqual(currentURL(), '/settings/tokens/new');
184+
assert.dom('[data-test-expiry-description]').hasText('The token will never expire');
185+
186+
await fillIn('[data-test-name]', 'token-name');
187+
await select('[data-test-expiry]', 'custom');
188+
assert.dom('[data-test-expiry-description]').doesNotExist();
189+
190+
await click('[data-test-scope="publish-update"]');
191+
await click('[data-test-generate]');
192+
assert.dom('[data-test-expiry-date]').hasAria('invalid', 'true');
193+
194+
await fillIn('[data-test-expiry-date]', '2024-05-04');
195+
assert.dom('[data-test-expiry-description]').doesNotExist();
196+
197+
await click('[data-test-generate]');
198+
199+
let token = this.server.schema.apiTokens.findBy({ name: 'token-name' });
200+
assert.ok(Boolean(token), 'API token has been created in the backend database');
201+
assert.strictEqual(token.name, 'token-name');
202+
assert.strictEqual(token.expiredAt.slice(0, 10), '2024-05-04');
203+
assert.strictEqual(token.crateScopes, null);
204+
assert.deepEqual(token.endpointScopes, ['publish-update']);
205+
206+
assert.strictEqual(currentURL(), '/settings/tokens');
207+
assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name');
208+
assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token);
209+
assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update');
210+
assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').doesNotExist();
211+
assert.dom('[data-test-api-token="1"] [data-test-expired-at]').hasText('Expires in over 6 years');
141212
});
142213

143214
test('loading and error state', async function (assert) {

0 commit comments

Comments
 (0)