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/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; } 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 @@