Skip to content

settings/tokens/new: Add "Expiration" options #6648

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 3 commits into from
Jun 22, 2023
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
44 changes: 43 additions & 1 deletion app/controllers/settings/tokens/new.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export default class NewTokenController extends Controller {

@tracked name;
@tracked nameInvalid;
@tracked expirySelection;
@tracked expiryDateInput;
@tracked expiryDateInvalid;
@tracked scopes;
@tracked scopesInvalid;
@tracked crateScopes;
Expand All @@ -29,6 +32,31 @@ export default class NewTokenController extends Controller {
this.reset();
}

get today() {
return new Date().toISOString().slice(0, 10);
}

get expiryDate() {
if (this.expirySelection === 'none') return null;
if (this.expirySelection === 'custom') {
if (!this.expiryDateInput) return null;

let now = new Date();
let timeSuffix = now.toISOString().slice(10);
return new Date(this.expiryDateInput + timeSuffix);
}

let date = new Date();
date.setDate(date.getDate() + Number(this.expirySelection));
return date;
}

get expiryDescription() {
return this.expirySelection === 'none'
? 'The token will never expire'
: `The token will expire on ${this.expiryDate.toLocaleDateString(undefined, { dateStyle: 'long' })}`;
}

@action isScopeSelected(id) {
return this.scopes.includes(id);
}
Expand All @@ -46,6 +74,7 @@ export default class NewTokenController extends Controller {
name,
endpoint_scopes: scopes,
crate_scopes: crateScopes,
expired_at: this.expiryDate,
});

try {
Expand All @@ -68,23 +97,36 @@ export default class NewTokenController extends Controller {
reset() {
this.name = '';
this.nameInvalid = false;
this.expirySelection = 'none';
this.expiryDateInput = null;
this.expiryDateInvalid = false;
this.scopes = [];
this.scopesInvalid = false;
this.crateScopes = TrackedArray.of();
}

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

return !this.nameInvalid && !this.scopesInvalid && crateScopesValid;
return !this.nameInvalid && !this.expiryDateInvalid && !this.scopesInvalid && crateScopesValid;
}

@action resetNameValidation() {
this.nameInvalid = false;
}

@action updateExpirySelection(event) {
this.expiryDateInput = this.expiryDate?.toISOString().slice(0, 10);
this.expirySelection = event.target.value;
}

@action resetExpiryDateValidation() {
this.expiryDateInvalid = false;
}

@action toggleScope(id) {
this.scopes = this.scopes.includes(id) ? this.scopes.filter(it => it !== id) : [...this.scopes, id];
this.scopesInvalid = false;
Expand Down
16 changes: 16 additions & 0 deletions app/styles/application.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,22 @@ noscript {
color: white;
}

/* see https://github.com/twbs/bootstrap/pull/30269 */
::-webkit-datetime-edit,
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}

::-webkit-calendar-picker-indicator {
font-size: 0.9em
}

:global {
.c-notification__icon {
display: flex;
Expand Down
32 changes: 29 additions & 3 deletions app/styles/settings/tokens/new.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,9 @@
flex-wrap: wrap;
}

.name-input {
max-width: 440px;
width: 100%;
.base-input {
padding: var(--space-2xs);
background-color: white;
border: 1px solid var(--gray-border);
border-radius: var(--space-3xs);

Expand All @@ -56,6 +55,33 @@
}
}

.name-input {
composes: base-input;

max-width: 440px;
width: 100%;
}

.expiry-select {
composes: base-input;

padding-right: var(--space-m);
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>");
background-repeat: no-repeat;
background-position: calc(100% - var(--space-2xs)) center;
background-size: 10px;
appearance: none;
}

.expiry-date-input {
composes: base-input;
}

.expiry-description {
margin-left: var(--space-2xs);
font-size: 0.9em;
}

.scopes-list {
list-style: none;
padding: 0;
Expand Down
40 changes: 40 additions & 0 deletions app/templates/settings/tokens/new.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,46 @@
{{/let}}
</div>

<div local-class="form-group" data-test-expiry-group>
{{#let (unique-id) as |id|}}
<label for={{id}} local-class="form-group-name">Expiration</label>

<select
id={{id}}
disabled={{this.saveTokenTask.isRunning}}
local-class="expiry-select"
data-test-expiry
{{on "change" this.updateExpirySelection}}
>
<option value="none" selected>No expiration</option>
<option value="7">7 days</option>
<option value="30">30 days</option>
<option value="60">60 days</option>
<option value="90">90 days</option>
<option value="365">365 days</option>
<option value="custom">Custom...</option>
</select>
{{/let}}

{{#if (eq this.expirySelection "custom")}}
<Input
@type="date"
@value={{this.expiryDateInput}}
min={{this.today}}
disabled={{this.saveTokenTask.isRunning}}
aria-invalid={{if this.expiryDateInvalid "true" "false"}}
aria-label="Custom expiration date"
local-class="expiry-date-input"
data-test-expiry-date
{{on "input" this.resetExpiryDateValidation}}
/>
{{else}}
<span local-class="expiry-description" data-test-expiry-description>
{{this.expiryDescription}}
</span>
{{/if}}
</div>

<div local-class="form-group" data-test-scopes-group>
<div local-class="form-group-name">
Scopes
Expand Down
73 changes: 72 additions & 1 deletion tests/routes/settings/tokens/new-test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { click, currentURL, fillIn, waitFor } from '@ember/test-helpers';
import { click, currentURL, fillIn, select, waitFor } from '@ember/test-helpers';
import { module, test } from 'qunit';

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

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

test('crate scopes', async function (assert) {
Expand Down Expand Up @@ -138,6 +140,75 @@ module('/settings/tokens/new', function (hooks) {
assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token);
assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update and yank');
assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').hasText('Crates: serde-* and serde');
assert.dom('[data-test-api-token="1"] [data-test-expired-at]').doesNotExist();
});

test('token expiry', async function (assert) {
prepare(this);

await visit('/settings/tokens/new');
assert.strictEqual(currentURL(), '/settings/tokens/new');
assert.dom('[data-test-expiry-description]').hasText('The token will never expire');

await fillIn('[data-test-name]', 'token-name');
await select('[data-test-expiry]', '30');

let expiryDate = new Date('2017-12-20');
let expectedDate = expiryDate.toLocaleDateString(undefined, { dateStyle: 'long' });
let expectedDescription = `The token will expire on ${expectedDate}`;
assert.dom('[data-test-expiry-description]').hasText(expectedDescription);

await click('[data-test-scope="publish-update"]');
await click('[data-test-generate]');

let token = this.server.schema.apiTokens.findBy({ name: 'token-name' });
assert.ok(Boolean(token), 'API token has been created in the backend database');
assert.strictEqual(token.name, 'token-name');
assert.strictEqual(token.expiredAt.slice(0, 10), '2017-12-20');
assert.strictEqual(token.crateScopes, null);
assert.deepEqual(token.endpointScopes, ['publish-update']);

assert.strictEqual(currentURL(), '/settings/tokens');
assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name');
assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token);
assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update');
assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').doesNotExist();
assert.dom('[data-test-api-token="1"] [data-test-expired-at]').hasText('Expires in about 1 month');
});

test('token expiry with custom date', async function (assert) {
prepare(this);

await visit('/settings/tokens/new');
assert.strictEqual(currentURL(), '/settings/tokens/new');
assert.dom('[data-test-expiry-description]').hasText('The token will never expire');

await fillIn('[data-test-name]', 'token-name');
await select('[data-test-expiry]', 'custom');
assert.dom('[data-test-expiry-description]').doesNotExist();

await click('[data-test-scope="publish-update"]');
await click('[data-test-generate]');
assert.dom('[data-test-expiry-date]').hasAria('invalid', 'true');

await fillIn('[data-test-expiry-date]', '2024-05-04');
assert.dom('[data-test-expiry-description]').doesNotExist();

await click('[data-test-generate]');

let token = this.server.schema.apiTokens.findBy({ name: 'token-name' });
assert.ok(Boolean(token), 'API token has been created in the backend database');
assert.strictEqual(token.name, 'token-name');
assert.strictEqual(token.expiredAt.slice(0, 10), '2024-05-04');
assert.strictEqual(token.crateScopes, null);
assert.deepEqual(token.endpointScopes, ['publish-update']);

assert.strictEqual(currentURL(), '/settings/tokens');
assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name');
assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token);
assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update');
assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').doesNotExist();
assert.dom('[data-test-api-token="1"] [data-test-expired-at]').hasText('Expires in over 6 years');
});

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