From e9513f5e2e72bc78b43420d061044b6a034ca7e8 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 25 Nov 2025 13:05:10 +0100 Subject: [PATCH 1/6] models/crate: Add `trustpubOnly` attribute --- app/models/crate.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/models/crate.js b/app/models/crate.js index f7112d80ea5..e7d7c72273a 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -29,6 +29,12 @@ export default class Crate extends Model { @attr documentation; @attr repository; + /** + * Whether this crate can only be published via Trusted Publishing. + * @type {boolean} + */ + @attr trustpub_only; + /** * This isn't an attribute in the crate response. * It's actually the `meta` attribute that belongs to `versions` From aafe0bc0fa9b04e1d16d14189275cddaf0c6aeb2 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 25 Nov 2025 13:04:03 +0100 Subject: [PATCH 2/6] models/crate: Add `setTrustpubOnlyTask` This adds a `setTrustpubOnlyTask` to enable/disable the trusted publishing restriction via the PATCH `/api/v1/crates/{name}` endpoint. --- app/models/crate.js | 8 ++++++- tests/models/crate-test.js | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/app/models/crate.js b/app/models/crate.js index e7d7c72273a..e764999d15b 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -4,7 +4,7 @@ import { waitForPromise } from '@ember/test-waiters'; import { cached } from '@glimmer/tracking'; import { apiAction } from '@mainmatter/ember-api-actions'; -import { task } from 'ember-concurrency'; +import { keepLatestTask, task } from 'ember-concurrency'; export default class Crate extends Model { @attr name; @@ -129,4 +129,10 @@ export default class Crate extends Model { let fut = reload === true ? versionsRef.reload() : versionsRef.load(); return (await fut) ?? []; }); + + setTrustpubOnlyTask = keepLatestTask(async trustpubOnly => { + let data = { crate: { trustpub_only: trustpubOnly } }; + let payload = await waitForPromise(apiAction(this, { method: 'PATCH', data })); + this.store.pushPayload(payload); + }); } diff --git a/tests/models/crate-test.js b/tests/models/crate-test.js index bb2e0a8dfde..211edf5e490 100644 --- a/tests/models/crate-test.js +++ b/tests/models/crate-test.js @@ -13,6 +13,52 @@ module('Model | Crate', function (hooks) { this.store = this.owner.lookup('service:store'); }); + module('setTrustpubOnlyTask', function () { + test('enables trustpub_only', async function (assert) { + let user = this.db.user.create(); + this.authenticateAs(user); + + let crate = this.db.crate.create({ trustpubOnly: false }); + this.db.version.create({ crate }); + + let crateRecord = await this.store.findRecord('crate', crate.name); + assert.false(crateRecord.trustpub_only); + assert.false(this.db.crate.findFirst({ where: { id: { equals: crate.id } } }).trustpubOnly); + + await crateRecord.setTrustpubOnlyTask.perform(true); + assert.true(crateRecord.trustpub_only); + assert.true(this.db.crate.findFirst({ where: { id: { equals: crate.id } } }).trustpubOnly); + }); + + test('disables trustpub_only', async function (assert) { + let user = this.db.user.create(); + this.authenticateAs(user); + + let crate = this.db.crate.create({ trustpubOnly: true }); + this.db.version.create({ crate }); + + let crateRecord = await this.store.findRecord('crate', crate.name); + assert.true(crateRecord.trustpub_only); + assert.true(this.db.crate.findFirst({ where: { id: { equals: crate.id } } }).trustpubOnly); + + await crateRecord.setTrustpubOnlyTask.perform(false); + assert.false(crateRecord.trustpub_only); + assert.false(this.db.crate.findFirst({ where: { id: { equals: crate.id } } }).trustpubOnly); + }); + + test('requires authentication', async function (assert) { + let crate = this.db.crate.create(); + this.db.version.create({ crate }); + + let crateRecord = await this.store.findRecord('crate', crate.name); + + await assert.rejects(crateRecord.setTrustpubOnlyTask.perform(true), function (error) { + assert.deepEqual(error.errors, [{ detail: 'must be logged in to perform that action' }]); + return true; + }); + }); + }); + module('inviteOwner()', function () { test('happy path', async function (assert) { let user = this.db.user.create(); From 432c2e79cc25ce52d92b51d581d8b3e892d59e3e Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 25 Nov 2025 14:41:44 +0100 Subject: [PATCH 3/6] crate/settings: Add `trustpub_only` checkbox --- app/components/trustpub-only-checkbox.css | 25 +++ app/components/trustpub-only-checkbox.gjs | 41 +++++ app/templates/crate/settings/index.css | 11 +- app/templates/crate/settings/index.gjs | 211 +++++++++++----------- tests/routes/crate/settings-test.js | 63 ++++++- 5 files changed, 246 insertions(+), 105 deletions(-) create mode 100644 app/components/trustpub-only-checkbox.css create mode 100644 app/components/trustpub-only-checkbox.gjs diff --git a/app/components/trustpub-only-checkbox.css b/app/components/trustpub-only-checkbox.css new file mode 100644 index 00000000000..f6fc65159c8 --- /dev/null +++ b/app/components/trustpub-only-checkbox.css @@ -0,0 +1,25 @@ +.trustpub-only-checkbox { + display: grid; + grid-template: + 'checkbox label' auto + 'checkbox note' auto / 16px 1fr; + row-gap: var(--space-3xs); + column-gap: var(--space-xs); + padding: var(--space-s) var(--space-m); + cursor: pointer; +} + +.checkbox { + grid-area: checkbox; +} + +.label { + grid-area: label; + font-weight: bold; +} + +.note { + grid-area: note; + font-size: 85%; + color: var(--main-color-light); +} diff --git a/app/components/trustpub-only-checkbox.gjs b/app/components/trustpub-only-checkbox.gjs new file mode 100644 index 00000000000..57f1288b6bb --- /dev/null +++ b/app/components/trustpub-only-checkbox.gjs @@ -0,0 +1,41 @@ +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +import LoadingSpinner from 'crates-io/components/loading-spinner'; + +export default class TrustpubOnlyCheckbox extends Component { + @service notifications; + + @action async toggle(event) { + let { checked } = event.target; + try { + await this.args.crate.setTrustpubOnlyTask.perform(checked); + } catch (error) { + let detail = error.errors?.[0]?.detail; + if (detail && !detail.startsWith('{')) { + this.notifications.error(detail); + } else { + this.notifications.error('Failed to update trusted publishing setting'); + } + } + } + + +} diff --git a/app/templates/crate/settings/index.css b/app/templates/crate/settings/index.css index 817498307b0..f96db2e2d8c 100644 --- a/app/templates/crate/settings/index.css +++ b/app/templates/crate/settings/index.css @@ -54,6 +54,11 @@ background-color: light-dark(white, #141413); border-radius: var(--space-3xs); box-shadow: 0 1px 3px light-dark(hsla(51, 90%, 42%, .35), #232321); +} + +.trustpub table { + width: 100%; + border-spacing: 0; :global(tbody) > :global(tr) > :global(td) { border-top: 1px solid light-dark(hsla(51, 90%, 42%, .25), #232321); @@ -82,7 +87,7 @@ display: none; } - tbody > tr > td:first-child { + tbody > tr:not(.no-trustpub-config) > td:first-child { padding-bottom: 0; } @@ -109,6 +114,10 @@ } } +.trustpub-only-checkbox { + border-top: 1px solid light-dark(hsla(51, 90%, 42%, 0.25), #232321); +} + .email-column { width: 25%; color: var(--main-color-light); diff --git a/app/templates/crate/settings/index.gjs b/app/templates/crate/settings/index.gjs index 0095058b929..0338118e319 100644 --- a/app/templates/crate/settings/index.gjs +++ b/app/templates/crate/settings/index.gjs @@ -10,6 +10,7 @@ import or from 'ember-truth-helpers/helpers/or'; import CrateHeader from 'crates-io/components/crate-header'; import Tooltip from 'crates-io/components/tooltip'; +import TrustpubOnlyCheckbox from 'crates-io/components/trustpub-only-checkbox'; import UserAvatar from 'crates-io/components/user-avatar';