Skip to content

Commit

Permalink
Merge pull request #6428 from Turbo87/endpoint-scopes
Browse files Browse the repository at this point in the history
settings/tokens/new: Add "Scopes" section
  • Loading branch information
Turbo87 authored May 5, 2023
2 parents 2ead493 + 34770e8 commit 5408fec
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 16 deletions.
36 changes: 30 additions & 6 deletions app/controllers/settings/tokens/new.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,30 @@ export default class NewTokenController extends Controller {

@tracked name;
@tracked nameInvalid;
@tracked scopes;
@tracked scopesInvalid;

ENDPOINT_SCOPES = [
{ id: 'change-owners', description: 'Invite new crate owners or remove existing ones' },
{ id: 'publish-new', description: 'Publish new crates' },
{ id: 'publish-update', description: 'Publish new versions of existing crates' },
{ id: 'yank', description: 'Yank and unyank crate versions' },
];

constructor() {
super(...arguments);
this.reset();
}

@action isScopeSelected(id) {
return this.scopes.includes(id);
}

saveTokenTask = task(async () => {
let { name } = this;
if (!name) {
this.nameInvalid = true;
return;
}
if (!this.validate()) return;
let { name, scopes } = this;

let token = this.store.createRecord('api-token', { name });
let token = this.store.createRecord('api-token', { name, endpoint_scopes: scopes });

try {
// Save the new API token on the backend
Expand All @@ -48,9 +58,23 @@ export default class NewTokenController extends Controller {
reset() {
this.name = '';
this.nameInvalid = false;
this.scopes = [];
this.scopesInvalid = false;
}

validate() {
this.nameInvalid = !this.name;
this.scopesInvalid = this.scopes.length === 0;

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

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

@action toggleScope(id) {
this.scopes = this.scopes.includes(id) ? this.scopes.filter(it => it !== id) : [...this.scopes, id];
this.scopesInvalid = false;
}
}
4 changes: 4 additions & 0 deletions app/models/api-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ export default class ApiToken extends Model {
@attr token;
@attr('date') created_at;
@attr('date') last_used_at;
/** @type string[] | null */
@attr crate_scopes;
/** @type string[] | null */
@attr endpoint_scopes;
}
58 changes: 50 additions & 8 deletions app/styles/settings/tokens/new.module.css
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
.form-group, .buttons {
position: relative;
margin: var(--space-s) 0;
margin: var(--space-m) 0;
}

.form-group {
label {
display: block;
margin-bottom: var(--space-3xs);
font-weight: 600;
}
.form-group-name {
display: block;
margin-bottom: var(--space-2xs);
font-weight: 600;
}

.form-group-error {
display: block;
color: red;
font-size: 0.9em;
margin-top: var(--space-2xs);
}

.buttons {
Expand All @@ -21,7 +26,7 @@
max-width: 440px;
width: 100%;
padding: var(--space-2xs);
border: 1px solid #ada796;
border: 1px solid var(--gray-border);
border-radius: var(--space-3xs);

&[aria-invalid="true"] {
Expand All @@ -30,6 +35,43 @@
}
}

.scopes-list {
list-style: none;
padding: 0;
margin: 0;
background-color: white;
border: 1px solid var(--gray-border);
border-radius: var(--space-3xs);

&.invalid {
background: #fff2f2;
border-color: red;
}

> * + * {
border-top: inherit;
}

label {
padding: var(--space-xs) var(--space-s);
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
font-size: 0.9em;
}
}

.scope-id {
display: inline-block;
max-width: 170px;
flex-grow: 1;
font-weight: bold;
}

.scope-description {
display: inline-block;
}

.generate-button {
composes: yellow-button small from '../../../styles/shared/buttons.module.css';
border-radius: 4px;
Expand Down
39 changes: 37 additions & 2 deletions app/templates/settings/tokens/new.hbs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<h2>New API Token</h2>

<form local-class="form" {{on "submit" (prevent-default (perform this.saveTokenTask))}}>
<div local-class="form-group">
<div local-class="form-group" data-test-name-group>
{{#let (unique-id) as |id|}}
<label for={{id}}>Name</label>
<label for={{id}} local-class="form-group-name">Name</label>

<Input
id={{id}}
@type="text"
Expand All @@ -16,9 +17,43 @@
{{auto-focus}}
{{on "input" this.resetNameValidation}}
/>

{{#if this.nameInvalid}}
<div local-class="form-group-error" data-test-error>
Please enter a name for this token.
</div>
{{/if}}
{{/let}}
</div>

<div local-class="form-group" data-test-scopes-group>
<div local-class="form-group-name">Scopes</div>

<ul role="list" local-class="scopes-list {{if this.scopesInvalid "invalid"}}">
{{#each this.ENDPOINT_SCOPES as |scope|}}
<li>
<label data-test-scope={{scope.id}}>
<Input
@type="checkbox"
@checked={{this.isScopeSelected scope.id}}
disabled={{this.saveTokenTask.isRunning}}
{{on "change" (fn this.toggleScope scope.id)}}
/>

<span local-class="scope-id">{{scope.id}}</span>
<span local-class="scope-description">{{scope.description}}</span>
</label>
</li>
{{/each}}
</ul>

{{#if this.scopesInvalid}}
<div local-class="form-group-error" data-test-error>
Please select at least one token scope.
</div>
{{/if}}
</div>

<div local-class="buttons">
<button
type="submit"
Expand Down
21 changes: 21 additions & 0 deletions tests/routes/settings/tokens/new-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ module('/settings/tokens/new', function (hooks) {
assert.strictEqual(currentURL(), '/settings/tokens/new');

await fillIn('[data-test-name]', 'token-name');
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.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');
Expand All @@ -74,6 +78,7 @@ module('/settings/tokens/new', function (hooks) {
assert.strictEqual(currentURL(), '/settings/tokens/new');

await fillIn('[data-test-name]', 'token-name');
await click('[data-test-scope="publish-update"]');
let clickPromise = click('[data-test-generate]');
await waitFor('[data-test-generate] [data-test-spinner]');
assert.dom('[data-test-name]').isDisabled();
Expand Down Expand Up @@ -104,8 +109,24 @@ module('/settings/tokens/new', function (hooks) {
await visit('/settings/tokens/new');
assert.strictEqual(currentURL(), '/settings/tokens/new');

await click('[data-test-scope="publish-update"]');
await click('[data-test-generate]');
assert.strictEqual(currentURL(), '/settings/tokens/new');
assert.dom('[data-test-name]').hasAria('invalid', 'true');
assert.dom('[data-test-name-group] [data-test-error]').exists();
assert.dom('[data-test-scopes-group] [data-test-error]').doesNotExist();
});

test('no scopes selected shows an error', async function (assert) {
prepare(this);

await visit('/settings/tokens/new');
assert.strictEqual(currentURL(), '/settings/tokens/new');

await fillIn('[data-test-name]', 'token-name');
await click('[data-test-generate]');
assert.strictEqual(currentURL(), '/settings/tokens/new');
assert.dom('[data-test-name-group] [data-test-error]').doesNotExist();
assert.dom('[data-test-scopes-group] [data-test-error]').exists();
});
});

0 comments on commit 5408fec

Please sign in to comment.