diff --git a/Dockerfile b/Dockerfile index ff083ed8f6..f3351eb3de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,14 +41,18 @@ COPY . /app RUN --mount=type=secret,id=GITHUB_PERSONAL_TOKEN export GITHUB_PERSONAL_TOKEN=$(cat /run/secrets/GITHUB_PERSONAL_TOKEN) && git config --global url."https://$GITHUB_PERSONAL_TOKEN@github.com/".insteadOf ssh://git@github.com -RUN (\ - npm ci; \ +RUN npm ci + +RUN \ + --mount=type=secret,id=GOOGLE_RECAPTCHA_SITE_KEY \ + sh -c ' \ if test $AIDA_URL; then \ curl -o /app/node_modules/asktravis/dist/aida.js $AIDA_URL; \ curl -o /app/node_modules/asktravis/dist/aida.js.map $AIDA_URL.map || true; \ fi; \ - ember build --environment=production; \ -) + export GOOGLE_RECAPTCHA_SITE_KEY=$(cat /run/secrets/GOOGLE_RECAPTCHA_SITE_KEY) && \ + ember build --environment=production' + RUN cp -a public/* dist/ diff --git a/app/components/account-env-var.js b/app/components/account-env-var.js new file mode 100644 index 0000000000..aa98464b60 --- /dev/null +++ b/app/components/account-env-var.js @@ -0,0 +1,38 @@ +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; +import { computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import { task } from 'ember-concurrency'; + +export default Component.extend({ + flashes: service(), + api: service(), + + tagName: 'li', + classNames: ['settings-envvar'], + classNameBindings: ['envVar.public:is-public', 'envVar.newlyCreated:newly-created'], + validates: { name: ['presence'] }, + actionType: 'Save', + showValueField: alias('public'), + + envVarDeleted(key) {}, + + value: computed('envVar.{value,public}', function () { + let value = this.get('envVar.value'); + let isPublic = this.get('envVar.public'); + + if (isPublic) { + return value; + } + return '••••••••••••••••'; + }), + + + delete: task(function* () { + try { + yield this.api.delete(`/account_env_var/${this.envVar.id}`); + } catch (e) {} + + this.envVarDeleted(this.envVar); + }).drop() +}); diff --git a/app/components/add-account-env-var.js b/app/components/add-account-env-var.js new file mode 100644 index 0000000000..9ae8c65527 --- /dev/null +++ b/app/components/add-account-env-var.js @@ -0,0 +1,55 @@ +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; +import { task } from 'ember-concurrency'; +import BranchSearching from 'travis/mixins/branch-searching'; + +export default Component.extend(BranchSearching, { + classNames: ['form--envvar'], + classNameBindings: ['valueError:form-error'], + + api: service(), + store: service(), + raven: service(), + flashes: service(), + + init() { + this.reset(); + this._super(...arguments); + }, + + envVarAdded(envVar) {}, + + reset() { + return this.setProperties({ + name: null, + value: null, + 'public': null + }); + }, + + save: task(function* () { + this.set('valueError', false); + + try { + yield this.api.post( + '/account_env_vars', + { + data: { + owner_id: this.owner.id, + owner_type: this.ownerType, + name: this.name.trim(), + value: this.value.trim(), + 'public': !!this.public + } + } + ).then((data) => { + this.envVarAdded(data); + this.reset(); + }); + } catch (errors) { + errors.clone().json().then((error) => { + this.set('valueError', error.error_message); + }); + } + }).drop() +}); diff --git a/app/controllers/account/settings.js b/app/controllers/account/settings.js index 15c0466bf3..74fde54ac0 100644 --- a/app/controllers/account/settings.js +++ b/app/controllers/account/settings.js @@ -58,6 +58,13 @@ export default Controller.extend({ return this.customKeysLoaded; }), + envVarsLoaded: computed('auth.currentUser.accountEnvVars', function () { + return this.auth.currentUser.accountEnvVars; + }), + envVars: computed('envVarsLoaded.[]', function () { + return (this.envVarsLoaded || []).sortBy('name'); + }), + isShowingAddKeyModal: false, userHasNoEmails: computed('auth.currentUser.emails', function () { @@ -110,6 +117,14 @@ export default Controller.extend({ }, customKeyAdded(key) { this.get('customKeysLoaded').pushObject(key); + }, + envVarDeleted(envVar) { + const envVars = this.auth.currentUser.accountEnvVars; + envVars.removeObject(envVar); + }, + envVarAdded(envVar) { + const envVars = this.auth.currentUser.accountEnvVars; + envVars.pushObject(envVar); } }, diff --git a/app/controllers/organization/settings.js b/app/controllers/organization/settings.js index cd595128a7..ca4a4adb43 100644 --- a/app/controllers/organization/settings.js +++ b/app/controllers/organization/settings.js @@ -39,6 +39,13 @@ export default Controller.extend({ return this.model.organization.customKeys; }), + envVarsLoaded: computed('organization.accountEnvVars', function () { + return this.organization.accountEnvVars; + }), + envVars: computed('envVarsLoaded.[]', function () { + return (this.envVarsLoaded || []).sortBy('name'); + }), + preferences: computed('model.preferences.@each.{name,value}', function () { const list = this.model.preferences || []; return list.reduce((hash, record) => { @@ -87,6 +94,16 @@ export default Controller.extend({ customKeyAdded(key) { this.get('model.organization.customKeys').pushObject(key); + }, + + envVarDeleted(envVar) { + const envVars = this.organization.accountEnvVars; + envVars.removeObject(envVar); + }, + + envVarAdded(envVar) { + const envVars = this.organization.accountEnvVars; + envVars.pushObject(envVar); } }, }); diff --git a/app/models/owner.js b/app/models/owner.js index 1a34dcc21b..59f39fc7c0 100644 --- a/app/models/owner.js +++ b/app/models/owner.js @@ -44,6 +44,7 @@ export default VcsEntity.extend({ isOrganization: equal('type', 'organization'), isAssembla: match('vcsType', /Assembla\S+$/), trialAllowed: attr('boolean', { defaultValue: false}), + accountEnvVars: attr(), allowance: belongsTo('allowance', { async: true, inverse: 'owner', polymorphic: true, as: 'owner' }), diff --git a/app/styles/app/layouts/settings.scss b/app/styles/app/layouts/settings.scss index 7c5b002d26..025fe9ca43 100644 --- a/app/styles/app/layouts/settings.scss +++ b/app/styles/app/layouts/settings.scss @@ -159,6 +159,12 @@ } } +.account-env-vars-section { + .env-var-name, .env-var-value { + width: 40% !important; + } +} + %settings-value-section { display: inline-block; vertical-align: middle; diff --git a/app/styles/app/modules/forms.scss b/app/styles/app/modules/forms.scss index 150b47a519..e06a172d25 100644 --- a/app/styles/app/modules/forms.scss +++ b/app/styles/app/modules/forms.scss @@ -166,9 +166,9 @@ textarea { display: flex; justify-content: left; width: auto; - height: 80px; align-items: center; margin-left: 1px; + height: 80px; button { order: 1; @@ -212,6 +212,19 @@ textarea { } } +.add-account-env-var-form-elem { + width: 40% !important; + + .display-value-switch { + height: auto !important; + } +} + +.add-var-btn { + text-align: left !important; + margin-bottom: 28px !important; +} + .form--envvar { .switch { .label { diff --git a/app/templates/account/settings.hbs b/app/templates/account/settings.hbs index f5e1d2bf51..29b5373211 100644 --- a/app/templates/account/settings.hbs +++ b/app/templates/account/settings.hbs @@ -146,4 +146,31 @@ +
+

+ Account Environment Variables +

+

+ Customize your build using environment variables. For secure tips on generating private keys + + read our documentation + +

+
+

+ + If your secret variable has special characters like &, + escape them by adding \ in front of each special character. + For example, ma&w!doc would be entered as ma\&w\!doc. + +

+
+ + +
    + {{#each this.envVars as |envVar|}} + + {{/each}} +
+
diff --git a/app/templates/components/account-env-var.hbs b/app/templates/components/account-env-var.hbs new file mode 100644 index 0000000000..8d30f37b28 --- /dev/null +++ b/app/templates/components/account-env-var.hbs @@ -0,0 +1,24 @@ + +
+ {{this.envVar.name}} +
+
+ {{#unless this.envVar.public}} + + {{/unless}} + +
+
+ {{#if this.delete.isRunning}} + + {{else}} + + {{/if}} +
diff --git a/app/templates/components/add-account-env-var.hbs b/app/templates/components/add-account-env-var.hbs new file mode 100644 index 0000000000..73f05c38c8 --- /dev/null +++ b/app/templates/components/add-account-env-var.hbs @@ -0,0 +1,68 @@ + +
+ +
+ + +
+
+
+ + + + +
+ + +
diff --git a/app/templates/organization/settings.hbs b/app/templates/organization/settings.hbs index f5a003eb28..0609381f0c 100644 --- a/app/templates/organization/settings.hbs +++ b/app/templates/organization/settings.hbs @@ -57,4 +57,31 @@ {{/unless}} + diff --git a/mirage/models/organization.js b/mirage/models/organization.js index 78b6b6232a..3791663359 100644 --- a/mirage/models/organization.js +++ b/mirage/models/organization.js @@ -1,7 +1,8 @@ -import { Model, belongsTo } from 'miragejs'; +import { Model, belongsTo, hasMany } from 'miragejs'; export default Model.extend({ allowance: belongsTo(), installation: belongsTo(), subscription: belongsTo(), + envVars: hasMany(), }); diff --git a/mirage/models/user.js b/mirage/models/user.js index e633a2f1f2..f7e7ad0ba5 100644 --- a/mirage/models/user.js +++ b/mirage/models/user.js @@ -1,6 +1,8 @@ -import { Model, belongsTo } from 'miragejs'; +import { Model, belongsTo, hasMany } from 'miragejs'; export default Model.extend({ allowance: belongsTo(), installation: belongsTo('installation', { embed: true, inverse: 'owner' }), + envVars: hasMany(), + settings: hasMany(), }); diff --git a/tests/integration/components/account-env-var-test.js b/tests/integration/components/account-env-var-test.js new file mode 100644 index 0000000000..fda1059101 --- /dev/null +++ b/tests/integration/components/account-env-var-test.js @@ -0,0 +1,43 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import EmberObject from '@ember/object'; + +module('Integration | Component | account-env-var', function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + test('it renders an account-env-var with public value', async function (assert) { + assert.expect(2); + + const envVar = EmberObject.create({ + name: 'TEST_VAR', + value: 'TEST', + public: true + }); + this.set('envVar', envVar); + + await render(hbs`{{account-env-var envVar=this.envVar}}`); + + assert.dom('.env-var-name').hasText(envVar['name'], 'name should be displayed'); + assert.dom('.env-var-value input').hasValue(envVar['value'], 'value should be visible'); + }); + + test('it renders an account-env-var with private value', async function (assert) { + assert.expect(2); + + const envVar = EmberObject.create({ + name: 'TEST_VAR', + value: 'TEST', + public: false + }); + this.set('envVar', envVar); + + await render(hbs`{{account-env-var envVar=this.envVar}}`); + + assert.dom('.env-var-name').hasText(envVar['name'], 'name should be displayed'); + assert.dom('.env-var-value input').hasValue('••••••••••••••••', 'value should be hidden'); + }); +}); diff --git a/tests/integration/components/add-account-env-var-test.js b/tests/integration/components/add-account-env-var-test.js new file mode 100644 index 0000000000..9e5a089425 --- /dev/null +++ b/tests/integration/components/add-account-env-var-test.js @@ -0,0 +1,62 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, fillIn, click } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import signInUser from 'travis/tests/helpers/sign-in-user'; + +module('Integration | Component | add-account-env-var', function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + test('it renders an account-env-var with public value', async function (assert) { + assert.expect(2); + + const currentUser = this.server.create('user'); + signInUser(currentUser); + + await render(hbs`{{add-account-env-var}}`); + + assert.dom('.env-name').exists('name should be displayed'); + assert.dom('.env-value').exists('value should be visible'); + }); + + test('it updates the public flag when toggled', async function (assert) { + assert.expect(2); + + const currentUser = this.server.create('user'); + signInUser(currentUser); + + await render(hbs`{{add-account-env-var}}`); + assert.dom('.display-value-switch button').doesNotHaveClass('active', 'public flag should be off by default'); + + await click('.display-value-switch button'); + assert.dom('.display-value-switch button').hasClass('active', 'public flag should be on after clicking'); + }); + + test('it displays an error if name is blank', async function (assert) { + assert.expect(1); + + const currentUser = this.server.create('user'); + signInUser(currentUser); + + await render(hbs`{{add-account-env-var}}`); + await fillIn('.env-value', 'test value'); + await click('.form-submit'); + + assert.dom('[data-test-name-error]').containsText('This field is required', 'error message should be for missing name'); + }); + + test('it displays an error if value is blank', async function (assert) { + assert.expect(1); + + const currentUser = this.server.create('user'); + signInUser(currentUser); + + await render(hbs`{{add-account-env-var}}`); + await fillIn('.env-name', 'TEST_VAR'); + await click('.add-account-env-form-submit'); + + assert.dom('[data-test-value-error]').containsText('This field is required', 'error message should be for missing name'); + }); +});