From e40ff2f46459df9096f67e829a7bb6855c63d5b4 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 4 May 2023 14:33:19 +0200 Subject: [PATCH 1/3] tests/settings/tokens/new: Add token attributes assertions --- tests/routes/settings/tokens/new-test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/routes/settings/tokens/new-test.js b/tests/routes/settings/tokens/new-test.js index 0e7ffa7f296..326c26bb543 100644 --- a/tests/routes/settings/tokens/new-test.js +++ b/tests/routes/settings/tokens/new-test.js @@ -58,6 +58,9 @@ 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.crateScopes, null); + assert.strictEqual(token.endpointScopes, null); assert.strictEqual(currentURL(), '/settings/tokens'); assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name'); From f9d94686cee30303a37eca4b584cc7e7b93d6094 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 4 May 2023 14:36:57 +0200 Subject: [PATCH 2/3] models/api-token: Add `crate_scopes` and `endpoint_scopes` attributes --- app/models/api-token.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/api-token.js b/app/models/api-token.js index f4f2d81cdd2..59403fb1a92 100644 --- a/app/models/api-token.js +++ b/app/models/api-token.js @@ -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; } From 34770e8e13f01cc8e9c7ec4ed194bdb8665c5c6a Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 4 May 2023 14:45:18 +0200 Subject: [PATCH 3/3] settings/tokens/new: Add "Scopes" section This only includes the endpoint scopes for now. The crate scopes will be implemented in a dedicated pull request. --- app/controllers/settings/tokens/new.js | 36 +++++++++++--- app/styles/settings/tokens/new.module.css | 58 +++++++++++++++++++---- app/templates/settings/tokens/new.hbs | 39 ++++++++++++++- tests/routes/settings/tokens/new-test.js | 20 +++++++- 4 files changed, 136 insertions(+), 17 deletions(-) diff --git a/app/controllers/settings/tokens/new.js b/app/controllers/settings/tokens/new.js index e7c829b0250..97d6ccab15d 100644 --- a/app/controllers/settings/tokens/new.js +++ b/app/controllers/settings/tokens/new.js @@ -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 @@ -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; + } } diff --git a/app/styles/settings/tokens/new.module.css b/app/styles/settings/tokens/new.module.css index 052d9ec9ae9..2deb88a7c9b 100644 --- a/app/styles/settings/tokens/new.module.css +++ b/app/styles/settings/tokens/new.module.css @@ -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 { @@ -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"] { @@ -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; diff --git a/app/templates/settings/tokens/new.hbs b/app/templates/settings/tokens/new.hbs index cbf892460c3..b4d3d145f09 100644 --- a/app/templates/settings/tokens/new.hbs +++ b/app/templates/settings/tokens/new.hbs @@ -1,9 +1,10 @@

New API Token

-
+
{{#let (unique-id) as |id|}} - + + + + {{#if this.nameInvalid}} +
+ Please enter a name for this token. +
+ {{/if}} {{/let}}
+
+
Scopes
+ +
    + {{#each this.ENDPOINT_SCOPES as |scope|}} +
  • + +
  • + {{/each}} +
+ + {{#if this.scopesInvalid}} +
+ Please select at least one token scope. +
+ {{/if}} +
+