From d4fc76b335d012275b06b7ce80d0c1b98f9db33d Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 5 Mar 2020 16:29:49 -0500 Subject: [PATCH 001/633] Revert "FIX: Don't allow people to clear the upload bucket while it's enabled" This reverts commit 4bb8db024c247f20af9245723ebea4c474eeb16a. --- lib/site_settings/validations.rb | 2 -- spec/lib/site_settings/validations_spec.rb | 9 --------- 2 files changed, 11 deletions(-) diff --git a/lib/site_settings/validations.rb b/lib/site_settings/validations.rb index 0bf13f7606..56987b0dfe 100644 --- a/lib/site_settings/validations.rb +++ b/lib/site_settings/validations.rb @@ -143,8 +143,6 @@ module SiteSettings::Validations def validate_s3_upload_bucket(new_val) validate_bucket_setting("s3_upload_bucket", new_val, SiteSetting.s3_backup_bucket) - - validate_error(:s3_upload_bucket_is_required, setting_name: 's3_upload_bucket') if new_val.blank? && SiteSetting.enable_s3_uploads? end def validate_s3_backup_bucket(new_val) diff --git a/spec/lib/site_settings/validations_spec.rb b/spec/lib/site_settings/validations_spec.rb index 69ab73920b..7f346d4d5c 100644 --- a/spec/lib/site_settings/validations_spec.rb +++ b/spec/lib/site_settings/validations_spec.rb @@ -103,15 +103,6 @@ describe SiteSettings::Validations do SiteSetting.s3_backup_bucket = "my-awesome-bucket/foo" expect { validate("my-awesome-bucket/foo/uploads") }.to raise_error(Discourse::InvalidParameters, error_message) end - - it "cannot be made blank unless the setting is false" do - SiteSetting.s3_backup_bucket = "really-real-cool-bucket" - SiteSetting.enable_s3_uploads = true - - expect { validate("") }.to raise_error(Discourse::InvalidParameters) - SiteSetting.enable_s3_uploads = false - validate("") - end end end From 54f67661acf04e8c501634823a1c7e183f48c95c Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Thu, 5 Mar 2020 19:21:38 -0300 Subject: [PATCH 002/633] FEATURE: Option to connect to Redis using SSL --- app/models/global_setting.rb | 2 ++ config/discourse_defaults.conf | 3 +++ 2 files changed, 5 insertions(+) diff --git a/app/models/global_setting.rb b/app/models/global_setting.rb index eb3592d1e4..57d22d6618 100644 --- a/app/models/global_setting.rb +++ b/app/models/global_setting.rb @@ -172,6 +172,7 @@ class GlobalSetting c[:db] = redis_db if redis_db != 0 c[:db] = 1 if Rails.env == "test" c[:id] = nil if redis_skip_client_commands + c[:ssl] = true if redis_use_ssl c.freeze end @@ -195,6 +196,7 @@ class GlobalSetting c[:db] = message_bus_redis_db if message_bus_redis_db != 0 c[:db] = 1 if Rails.env == "test" c[:id] = nil if message_bus_redis_skip_client_commands + c[:ssl] = true if redis_use_ssl c.freeze end diff --git a/config/discourse_defaults.conf b/config/discourse_defaults.conf index aee0f88ab2..0a4eb66dd7 100644 --- a/config/discourse_defaults.conf +++ b/config/discourse_defaults.conf @@ -120,6 +120,9 @@ redis_password = # skip configuring client id for cloud providers who support no client commands redis_skip_client_commands = false +# uses SSL for all Redis connections if true +redis_use_ssl = false + # message bus redis server switch message_bus_redis_enabled = false From 243284f998e213034f9f0c6b228f97f7c9834fdd Mon Sep 17 00:00:00 2001 From: Jeff Wong Date: Thu, 5 Mar 2020 13:55:35 -1000 Subject: [PATCH 003/633] FEATURE: prevent accidental canceling when drafting penalties (#9105) Pop up a confirmation box when there is input. This prevents accidental closing of the dialog boxes due to clicking outside. This adds a development hook on modals in the form of a `beforeClose` function. Modal windows can abort the close if the funtion returns false. Additionally fixing a few issues with loop and state on the modal popups: Escape key with bootbox is keyup. Updating modal to close on keyup as well so escape key is working. Fixes an issue where pressing esc will loop immediately back to the modal by: keydown -> bootbox -> keyup -> acts as "cancel", restores modal Needs a next call to reopenModal otherwise, keyup is handled again by the modal. Fixes an issue where pressing esc will loop immediately back to the confirm: esc keyup will be handled and bubble immediately back to the modal. Additionally, only handle key events when the #discourse-modal is visible. This resolves issues where escape or enter events were being handled by a hidden modal window. --- .../admin/mixins/penalty-controller.js.es6 | 24 +++++++++++++++++- .../discourse/components/d-modal.js.es6 | 17 +++++++------ .../discourse/routes/application.js.es6 | 15 +++++++++-- config/locales/client.en.yml | 1 + .../acceptance/admin-suspend-user-test.js.es6 | 25 +++++++++++++++++++ test/javascripts/acceptance/modal-test.js.es6 | 10 ++++---- 6 files changed, 77 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 b/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 index cb07e9d2ca..f923710649 100644 --- a/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 +++ b/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 @@ -1,6 +1,7 @@ import ModalFunctionality from "discourse/mixins/modal-functionality"; import { popupAjaxError } from "discourse/lib/ajax-error"; import Mixin from "@ember/object/mixin"; +import { next } from "@ember/runloop"; import { Promise } from "rsvp"; export default Mixin.create(ModalFunctionality, { @@ -11,6 +12,7 @@ export default Mixin.create(ModalFunctionality, { user: null, postId: null, successCallback: null, + confirmClose: false, resetModal() { this.setProperties({ @@ -21,10 +23,30 @@ export default Mixin.create(ModalFunctionality, { postEdit: null, postAction: "delete", before: null, - successCallback: null + successCallback: null, + confirmClose: false }); }, + beforeClose() { + // prompt a confirmation if we have unsaved content + if ( + (this.reason && this.reason.length > 1) || + (this.message && this.message.length > 1 && !this.confirmClose) + ) { + this.send("hideModal"); + bootbox.confirm(I18n.t("admin.user.confirm_cancel_penalty"), result => { + if (result) { + this.set("confirmClose", true); + this.send("closeModal"); + } else { + next(() => this.send("reopenModal")); + } + }); + return false; + } + }, + penalize(cb) { let before = this.before; let promise = before ? before() : Promise.resolve(); diff --git a/app/assets/javascripts/discourse/components/d-modal.js.es6 b/app/assets/javascripts/discourse/components/d-modal.js.es6 index 300a3e585c..eeae7edc3f 100644 --- a/app/assets/javascripts/discourse/components/d-modal.js.es6 +++ b/app/assets/javascripts/discourse/components/d-modal.js.es6 @@ -31,13 +31,16 @@ export default Component.extend({ @on("didInsertElement") setUp() { - $("html").on("keydown.discourse-modal", e => { - if (e.which === 27 && this.dismissable) { - next(() => $(".modal-header button.modal-close").click()); - } + $("html").on("keyup.discourse-modal", e => { + //only respond to events when the modal is visible + if ($("#discourse-modal:visible").length > 0) { + if (e.which === 27 && this.dismissable) { + next(() => $(".modal-header button.modal-close").click()); + } - if (e.which === 13 && this.triggerClickOnEnter(e)) { - next(() => $(".modal-footer .btn-primary").click()); + if (e.which === 13 && this.triggerClickOnEnter(e)) { + next(() => $(".modal-footer .btn-primary").click()); + } } }); @@ -46,7 +49,7 @@ export default Component.extend({ @on("willDestroyElement") cleanUp() { - $("html").off("keydown.discourse-modal"); + $("html").off("keyup.discourse-modal"); this.appEvents.off("modal:body-shown", this, "_modalBodyShown"); }, diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index e877d8ceff..8dca333360 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -157,12 +157,23 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, { // Close the current modal, and destroy its state. closeModal() { - this.render("hide-modal", { into: "modal", outlet: "modalBody" }); - const route = getOwner(this).lookup("route:application"); let modalController = route.controllerFor("modal"); const controllerName = modalController.get("name"); + if (controllerName) { + const controller = getOwner(this).lookup( + `controller:${controllerName}` + ); + if (controller && controller.beforeClose) { + if (false === controller.beforeClose()) { + return; + } + } + } + + this.render("hide-modal", { into: "modal", outlet: "modalBody" }); + if (controllerName) { const controller = getOwner(this).lookup( `controller:${controllerName}` diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 127c964d04..c7705ab948 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4291,6 +4291,7 @@ en: threshold_reached: "Received too many bounces from that email." trust_level_change_failed: "There was a problem changing the user's trust level." suspend_modal_title: "Suspend User" + confirm_cancel_penalty: "Are you sure you want to discard the penalty?" trust_level_2_users: "Trust Level 2 Users" trust_level_3_requirements: "Trust Level 3 Requirements" trust_level_locked_tip: "trust level is locked, system will not promote or demote user" diff --git a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 index d9983848ca..936f7080bb 100644 --- a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 +++ b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 @@ -34,6 +34,31 @@ QUnit.test("suspend a user - cancel", async assert => { assert.equal(find(".suspend-user-modal:visible").length, 0); }); +QUnit.test("suspend a user - cancel with input", async assert => { + await visit("/admin/users/1234/regular"); + await click(".suspend-user"); + + assert.equal(find(".suspend-user-modal:visible").length, 1); + + await fillIn(".suspend-reason", "for breaking the rules"); + await fillIn(".suspend-message", "this is an email reason why"); + + await click(".d-modal-cancel"); + + assert.equal(find(".bootbox.modal:visible").length, 1); + + await click(".modal-footer .btn-default"); + assert.equal(find(".suspend-user-modal:visible").length, 1); + assert.equal( + find(".suspend-message")[0].value, + "this is an email reason why" + ); + + await click(".d-modal-cancel"); + await click(".modal-footer .btn-primary"); + assert.equal(find(".suspend-user-modal:visible").length, 0); +}); + QUnit.test("suspend, then unsuspend a user", async assert => { const suspendUntilCombobox = selectKit(".suspend-until .combobox"); diff --git a/test/javascripts/acceptance/modal-test.js.es6 b/test/javascripts/acceptance/modal-test.js.es6 index 13725f4d6c..4e43be89bb 100644 --- a/test/javascripts/acceptance/modal-test.js.es6 +++ b/test/javascripts/acceptance/modal-test.js.es6 @@ -28,7 +28,7 @@ QUnit.test("modal", async function(assert) { await click(".login-button"); assert.ok(find(".d-modal:visible").length === 1, "modal should reappear"); - await keyEvent("#main-outlet", "keydown", 27); + await keyEvent("#main-outlet", "keyup", 27); assert.ok( find(".d-modal:visible").length === 0, "ESC should close the modal" @@ -47,7 +47,7 @@ QUnit.test("modal", async function(assert) { find(".d-modal:visible").length === 1, "modal should not disappear when you click outside" ); - await keyEvent("#main-outlet", "keydown", 27); + await keyEvent("#main-outlet", "keyup", 27); assert.ok( find(".d-modal:visible").length === 1, "ESC should not close the modal" @@ -61,7 +61,7 @@ QUnit.test("modal-keyboard-events", async function(assert) { await click(".toggle-admin-menu"); await click(".topic-admin-status-update button"); - await keyEvent(".d-modal", "keydown", 13); + await keyEvent(".d-modal", "keyup", 13); assert.ok( find("#modal-alert:visible").length === 1, @@ -72,7 +72,7 @@ QUnit.test("modal-keyboard-events", async function(assert) { "hitting Enter does not dismiss modal due to alert error" ); - await keyEvent("#main-outlet", "keydown", 27); + await keyEvent("#main-outlet", "keyup", 27); assert.ok( find(".d-modal:visible").length === 0, "ESC should close the modal" @@ -82,7 +82,7 @@ QUnit.test("modal-keyboard-events", async function(assert) { await click(".d-editor-button-bar .btn.link"); - await keyEvent(".d-modal", "keydown", 13); + await keyEvent(".d-modal", "keyup", 13); assert.ok( find(".d-modal:visible").length === 0, "modal should disappear on hitting Enter" From 6fe91bbbbb9a9bd076db6371c1f814f71b0ee711 Mon Sep 17 00:00:00 2001 From: Jeff Wong Date: Thu, 5 Mar 2020 15:29:51 -1000 Subject: [PATCH 004/633] Revert "FEATURE: prevent accidental canceling when drafting penalties (#9105)" (#9122) This reverts commit 243284f998e213034f9f0c6b228f97f7c9834fdd. There are some issues in how the JS tests interact that I will need to figure out here before this can be merged. --- .../admin/mixins/penalty-controller.js.es6 | 24 +----------------- .../discourse/components/d-modal.js.es6 | 17 ++++++------- .../discourse/routes/application.js.es6 | 15 ++--------- config/locales/client.en.yml | 1 - .../acceptance/admin-suspend-user-test.js.es6 | 25 ------------------- test/javascripts/acceptance/modal-test.js.es6 | 10 ++++---- 6 files changed, 15 insertions(+), 77 deletions(-) diff --git a/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 b/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 index f923710649..cb07e9d2ca 100644 --- a/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 +++ b/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 @@ -1,7 +1,6 @@ import ModalFunctionality from "discourse/mixins/modal-functionality"; import { popupAjaxError } from "discourse/lib/ajax-error"; import Mixin from "@ember/object/mixin"; -import { next } from "@ember/runloop"; import { Promise } from "rsvp"; export default Mixin.create(ModalFunctionality, { @@ -12,7 +11,6 @@ export default Mixin.create(ModalFunctionality, { user: null, postId: null, successCallback: null, - confirmClose: false, resetModal() { this.setProperties({ @@ -23,30 +21,10 @@ export default Mixin.create(ModalFunctionality, { postEdit: null, postAction: "delete", before: null, - successCallback: null, - confirmClose: false + successCallback: null }); }, - beforeClose() { - // prompt a confirmation if we have unsaved content - if ( - (this.reason && this.reason.length > 1) || - (this.message && this.message.length > 1 && !this.confirmClose) - ) { - this.send("hideModal"); - bootbox.confirm(I18n.t("admin.user.confirm_cancel_penalty"), result => { - if (result) { - this.set("confirmClose", true); - this.send("closeModal"); - } else { - next(() => this.send("reopenModal")); - } - }); - return false; - } - }, - penalize(cb) { let before = this.before; let promise = before ? before() : Promise.resolve(); diff --git a/app/assets/javascripts/discourse/components/d-modal.js.es6 b/app/assets/javascripts/discourse/components/d-modal.js.es6 index eeae7edc3f..300a3e585c 100644 --- a/app/assets/javascripts/discourse/components/d-modal.js.es6 +++ b/app/assets/javascripts/discourse/components/d-modal.js.es6 @@ -31,16 +31,13 @@ export default Component.extend({ @on("didInsertElement") setUp() { - $("html").on("keyup.discourse-modal", e => { - //only respond to events when the modal is visible - if ($("#discourse-modal:visible").length > 0) { - if (e.which === 27 && this.dismissable) { - next(() => $(".modal-header button.modal-close").click()); - } + $("html").on("keydown.discourse-modal", e => { + if (e.which === 27 && this.dismissable) { + next(() => $(".modal-header button.modal-close").click()); + } - if (e.which === 13 && this.triggerClickOnEnter(e)) { - next(() => $(".modal-footer .btn-primary").click()); - } + if (e.which === 13 && this.triggerClickOnEnter(e)) { + next(() => $(".modal-footer .btn-primary").click()); } }); @@ -49,7 +46,7 @@ export default Component.extend({ @on("willDestroyElement") cleanUp() { - $("html").off("keyup.discourse-modal"); + $("html").off("keydown.discourse-modal"); this.appEvents.off("modal:body-shown", this, "_modalBodyShown"); }, diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index 8dca333360..e877d8ceff 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -157,23 +157,12 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, { // Close the current modal, and destroy its state. closeModal() { + this.render("hide-modal", { into: "modal", outlet: "modalBody" }); + const route = getOwner(this).lookup("route:application"); let modalController = route.controllerFor("modal"); const controllerName = modalController.get("name"); - if (controllerName) { - const controller = getOwner(this).lookup( - `controller:${controllerName}` - ); - if (controller && controller.beforeClose) { - if (false === controller.beforeClose()) { - return; - } - } - } - - this.render("hide-modal", { into: "modal", outlet: "modalBody" }); - if (controllerName) { const controller = getOwner(this).lookup( `controller:${controllerName}` diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index c7705ab948..127c964d04 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4291,7 +4291,6 @@ en: threshold_reached: "Received too many bounces from that email." trust_level_change_failed: "There was a problem changing the user's trust level." suspend_modal_title: "Suspend User" - confirm_cancel_penalty: "Are you sure you want to discard the penalty?" trust_level_2_users: "Trust Level 2 Users" trust_level_3_requirements: "Trust Level 3 Requirements" trust_level_locked_tip: "trust level is locked, system will not promote or demote user" diff --git a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 index 936f7080bb..d9983848ca 100644 --- a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 +++ b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 @@ -34,31 +34,6 @@ QUnit.test("suspend a user - cancel", async assert => { assert.equal(find(".suspend-user-modal:visible").length, 0); }); -QUnit.test("suspend a user - cancel with input", async assert => { - await visit("/admin/users/1234/regular"); - await click(".suspend-user"); - - assert.equal(find(".suspend-user-modal:visible").length, 1); - - await fillIn(".suspend-reason", "for breaking the rules"); - await fillIn(".suspend-message", "this is an email reason why"); - - await click(".d-modal-cancel"); - - assert.equal(find(".bootbox.modal:visible").length, 1); - - await click(".modal-footer .btn-default"); - assert.equal(find(".suspend-user-modal:visible").length, 1); - assert.equal( - find(".suspend-message")[0].value, - "this is an email reason why" - ); - - await click(".d-modal-cancel"); - await click(".modal-footer .btn-primary"); - assert.equal(find(".suspend-user-modal:visible").length, 0); -}); - QUnit.test("suspend, then unsuspend a user", async assert => { const suspendUntilCombobox = selectKit(".suspend-until .combobox"); diff --git a/test/javascripts/acceptance/modal-test.js.es6 b/test/javascripts/acceptance/modal-test.js.es6 index 4e43be89bb..13725f4d6c 100644 --- a/test/javascripts/acceptance/modal-test.js.es6 +++ b/test/javascripts/acceptance/modal-test.js.es6 @@ -28,7 +28,7 @@ QUnit.test("modal", async function(assert) { await click(".login-button"); assert.ok(find(".d-modal:visible").length === 1, "modal should reappear"); - await keyEvent("#main-outlet", "keyup", 27); + await keyEvent("#main-outlet", "keydown", 27); assert.ok( find(".d-modal:visible").length === 0, "ESC should close the modal" @@ -47,7 +47,7 @@ QUnit.test("modal", async function(assert) { find(".d-modal:visible").length === 1, "modal should not disappear when you click outside" ); - await keyEvent("#main-outlet", "keyup", 27); + await keyEvent("#main-outlet", "keydown", 27); assert.ok( find(".d-modal:visible").length === 1, "ESC should not close the modal" @@ -61,7 +61,7 @@ QUnit.test("modal-keyboard-events", async function(assert) { await click(".toggle-admin-menu"); await click(".topic-admin-status-update button"); - await keyEvent(".d-modal", "keyup", 13); + await keyEvent(".d-modal", "keydown", 13); assert.ok( find("#modal-alert:visible").length === 1, @@ -72,7 +72,7 @@ QUnit.test("modal-keyboard-events", async function(assert) { "hitting Enter does not dismiss modal due to alert error" ); - await keyEvent("#main-outlet", "keyup", 27); + await keyEvent("#main-outlet", "keydown", 27); assert.ok( find(".d-modal:visible").length === 0, "ESC should close the modal" @@ -82,7 +82,7 @@ QUnit.test("modal-keyboard-events", async function(assert) { await click(".d-editor-button-bar .btn.link"); - await keyEvent(".d-modal", "keyup", 13); + await keyEvent(".d-modal", "keydown", 13); assert.ok( find(".d-modal:visible").length === 0, "modal should disappear on hitting Enter" From 494379201d8e2927a08f7b3656eb7d1f2949d044 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 6 Mar 2020 12:57:12 +1100 Subject: [PATCH 005/633] DEV: attempt to stabilize flaky spec Spec was checking implementation when it did not need to, temp file was blank so optimized image should be blank --- spec/models/optimized_image_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/models/optimized_image_spec.rb b/spec/models/optimized_image_spec.rb index a7c8e2e352..e90029bec3 100644 --- a/spec/models/optimized_image_spec.rb +++ b/spec/models/optimized_image_spec.rb @@ -312,9 +312,10 @@ describe OptimizedImage do end end - context "when an error happened while generatign the thumbnail" do + context "when we have a bad file returned" do it "returns nil" do - OptimizedImage.expects(:resize).returns(false) + # tempfile is empty + # this can not be resized expect(OptimizedImage.create_for(s3_upload, 100, 200)).to eq(nil) end end From 29ccdf5d3527d61b655c28c8790c94a1bdd6dcba Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Fri, 6 Mar 2020 14:37:40 +1000 Subject: [PATCH 006/633] FIX: Show a nicer error if name/code missing for TOTP/Security Keys (#9124) Meta: https://meta.discourse.org/t/improve-error-message-when-not-including-name-setting-up-totp/143339 * when the user creates a TOTP second factor method we want to show them a nicer error if they forget to add a name or the code from the app, instead of the param missing error * also add a client-side check for this and for security key name, no need to bother the server if we can help it --- .../second-factor-add-security-key.js.es6 | 7 +++ .../controllers/second-factor-add-totp.js.es6 | 8 ++- .../modal/second-factor-add-security-key.hbs | 4 +- app/controllers/users_controller.rb | 8 ++- config/locales/client.en.yml | 2 + config/locales/server.en.yml | 2 + spec/requests/users_controller_spec.rb | 60 ++++++++++++++++++- .../acceptance/preferences-test.js.es6 | 40 +++++++++++-- 8 files changed, 120 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/second-factor-add-security-key.js.es6 b/app/assets/javascripts/discourse/controllers/second-factor-add-security-key.js.es6 index 54f41fc405..2cb03f968f 100644 --- a/app/assets/javascripts/discourse/controllers/second-factor-add-security-key.js.es6 +++ b/app/assets/javascripts/discourse/controllers/second-factor-add-security-key.js.es6 @@ -53,6 +53,13 @@ export default Controller.extend(ModalFunctionality, { actions: { registerSecurityKey() { + if (!this.securityKeyName) { + this.set( + "errorMessage", + I18n.t("user.second_factor.security_key.name_required_error") + ); + return; + } const publicKeyCredentialCreationOptions = { challenge: Uint8Array.from(this.challenge, c => c.charCodeAt(0)), rp: { diff --git a/app/assets/javascripts/discourse/controllers/second-factor-add-totp.js.es6 b/app/assets/javascripts/discourse/controllers/second-factor-add-totp.js.es6 index eab4f2943f..10752e41c7 100644 --- a/app/assets/javascripts/discourse/controllers/second-factor-add-totp.js.es6 +++ b/app/assets/javascripts/discourse/controllers/second-factor-add-totp.js.es6 @@ -45,7 +45,13 @@ export default Controller.extend(ModalFunctionality, { }, enableSecondFactor() { - if (!this.secondFactorToken) return; + if (!this.secondFactorToken || !this.secondFactorName) { + this.set( + "errorMessage", + I18n.t("user.second_factor.totp.name_and_code_required_error") + ); + return; + } this.set("loading", true); this.model diff --git a/app/assets/javascripts/discourse/templates/modal/second-factor-add-security-key.hbs b/app/assets/javascripts/discourse/templates/modal/second-factor-add-security-key.hbs index d63b5fe8eb..36ede3f634 100644 --- a/app/assets/javascripts/discourse/templates/modal/second-factor-add-security-key.hbs +++ b/app/assets/javascripts/discourse/templates/modal/second-factor-add-security-key.hbs @@ -16,7 +16,7 @@
- {{input value=securityKeyName id='test' placeholder='security key name'}} + {{input value=securityKeyName id='security-key-name' placeholder='security key name'}}
@@ -24,7 +24,7 @@
{{#unless webauthnUnsupported}} {{d-button - class="btn-primary add-totp" + class="btn-primary add-security-key" action=(action "registerSecurityKey") label="user.second_factor.security_key.register"}} {{/unless}} diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index e96a79c953..33e7bdce6c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1211,8 +1211,12 @@ class UsersController < ApplicationController end def enable_second_factor_totp - params.require(:second_factor_token) - params.require(:name) + if params[:second_factor_token].blank? + return render json: failed_json.merge(error: I18n.t("login.missing_second_factor_code")) + end + if params[:name].blank? + return render json: failed_json.merge(error: I18n.t("login.missing_second_factor_name")) + end auth_token = params[:second_factor_token] totp_data = secure_session["staged-totp-#{current_user.id}"] diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 127c964d04..a18720a293 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1023,6 +1023,7 @@ en: title: "Token-Based Authenticators" add: "New Authenticator" default_name: "My Authenticator" + name_and_code_required_error: "You must provide a name and the code from your authenticator app." security_key: register: "Register" title: "Security Keys" @@ -1033,6 +1034,7 @@ en: edit: "Edit Security Key" edit_description: "Security Key Name" delete: "Delete" + name_required_error: "You must provide a name for your security key." change_about: title: "Change About Me" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index c4db42fb1c..3705e98f01 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2381,6 +2381,8 @@ en: second_factor_backup_title: "Two Factor Backup Code" invalid_second_factor_code: "Invalid authentication code. Each code can only be used once." invalid_security_key: "Invalid security key." + missing_second_factor_name: "Please provide a name." + missing_second_factor_code: "Please provide a code." second_factor_toggle: totp: "Use an authenticator app or security key instead" backup_code: "Use a backup code instead" diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index ed28cab650..13aebdfd2f 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -3522,6 +3522,58 @@ describe UsersController do end end + describe "#enable_second_factor_totp" do + before do + sign_in(user) + end + + def create_totp + stub_secure_session_confirmed + post "/users/create_second_factor_totp.json" + end + + it "creates a totp for the user successfully" do + create_totp + staged_totp_key = read_secure_session["staged-totp-#{user.id}"] + token = ROTP::TOTP.new(staged_totp_key).now + + post "/users/enable_second_factor_totp.json", params: { name: "test", second_factor_token: token } + + expect(response.status).to eq(200) + expect(user.user_second_factors.count).to eq(1) + end + + context "when an incorrect token is provided" do + before do + create_totp + post "/users/enable_second_factor_totp.json", params: { name: "test", second_factor_token: "123456" } + end + it "shows a helpful error message to the user" do + expect(JSON.parse(response.body)['error']).to eq(I18n.t("login.invalid_second_factor_code")) + end + end + + context "when a name is not provided" do + before do + create_totp + post "/users/enable_second_factor_totp.json", params: { second_factor_token: "123456" } + end + it "shows a helpful error message to the user" do + expect(JSON.parse(response.body)['error']).to eq(I18n.t("login.missing_second_factor_name")) + end + end + + context "when a token is not provided" do + before do + create_totp + post "/users/enable_second_factor_totp.json", params: { name: "test" } + end + it "shows a helpful error message to the user" do + expect(JSON.parse(response.body)['error']).to eq(I18n.t("login.missing_second_factor_code")) + end + end + end + describe '#update_second_factor' do let(:user_second_factor) { Fabricate(:user_second_factor_totp, user: user) } @@ -3554,7 +3606,7 @@ describe UsersController do context 'when token is valid' do before do - ApplicationController.any_instance.stubs(:secure_session).returns("confirmed-password-#{user.id}" => "true") + stub_secure_session_confirmed end it 'should allow second factor for the user to be renamed' do put "/users/second_factor.json", params: { @@ -4062,7 +4114,11 @@ describe UsersController do def create_second_factor_security_key sign_in(user) - UsersController.any_instance.stubs(:secure_session_confirmed?).returns(true) + stub_secure_session_confirmed post "/u/create_second_factor_security_key.json" end + + def stub_secure_session_confirmed + UsersController.any_instance.stubs(:secure_session_confirmed?).returns(true) + end end diff --git a/test/javascripts/acceptance/preferences-test.js.es6 b/test/javascripts/acceptance/preferences-test.js.es6 index ce5d8f68b1..c651c67802 100644 --- a/test/javascripts/acceptance/preferences-test.js.es6 +++ b/test/javascripts/acceptance/preferences-test.js.es6 @@ -20,6 +20,16 @@ acceptance("User Preferences", { }); }); + server.post("/u/create_second_factor_security_key.json", () => { + return helper.response({ + challenge: + "a6d393d12654c130b2273e68ca25ca232d1d7f4c2464c2610fb8710a89d4", + rp_id: "localhost", + rp_name: "Discourse", + supported_algoriths: [-7, -257] + }); + }); + server.post("/u/enable_second_factor_totp.json", () => { return helper.response({ error: "invalid token" }); }); @@ -211,7 +221,7 @@ QUnit.test("connected accounts", async assert => { .indexOf("Connect") > -1; }); -QUnit.test("second factor", async assert => { +QUnit.test("second factor totp", async assert => { await visit("/u/eviltrout/preferences/second-factor"); assert.ok(exists("#password"), "it has a password input"); @@ -223,14 +233,36 @@ QUnit.test("second factor", async assert => { await click(".new-totp"); assert.ok(exists("#test-qr"), "shows qr code"); - await fillIn("#second-factor-token", "111111"); await click(".add-totp"); assert.ok( find(".alert-error") .html() - .indexOf("invalid token") > -1, - "shows server validation error message" + .indexOf("provide a name and the code") > -1, + "shows name/token missing error message" + ); +}); + +QUnit.test("second factor security keys", async assert => { + await visit("/u/eviltrout/preferences/second-factor"); + + assert.ok(exists("#password"), "it has a password input"); + + await fillIn("#password", "secrets"); + await click(".user-preferences .btn-primary"); + assert.notOk(exists("#password"), "it hides the password input"); + + await click(".new-security-key"); + assert.ok(exists("#security-key-name"), "shows security key name input"); + + fillIn("#security-key-name", ""); + await click(".add-security-key"); + + assert.ok( + find(".alert-error") + .html() + .indexOf("provide a name") > -1, + "shows name missing error message" ); }); From ff62911a89d721ebec1716990c5e407e572d8960 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 6 Mar 2020 12:23:22 +0000 Subject: [PATCH 007/633] FEATURE: New route for loading multiple user cards simultaneously (#9078) Introduces `/user-cards.json` Also allows the client-side user model to be passed an existing promise when loading, so that multiple models can share the same AJAX request --- .../javascripts/discourse/models/user.js.es6 | 6 +++- app/controllers/users_controller.rb | 24 ++++++++++++++++ config/routes.rb | 2 ++ spec/requests/users_controller_spec.rb | 28 +++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 6927676805..84c2765f6f 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -536,8 +536,12 @@ const User = RestModel.extend({ const user = this; return PreloadStore.getAndRemove(`user_${user.get("username")}`, () => { - const useCardRoute = options && options.forCard; + if (options && options.existingRequest) { + // Existing ajax request has been passed, use it + return options.existingRequest; + } + const useCardRoute = options && options.forCard; if (options) delete options.forCard; const path = useCardRoute diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 33e7bdce6c..5b7bbc7022 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -99,6 +99,30 @@ class UsersController < ApplicationController show(for_card: true) end + def cards + return redirect_to path('/login') if SiteSetting.hide_user_profiles_from_public && !current_user + + user_ids = params.require(:user_ids).split(",").map(&:to_i) + raise Discourse::InvalidParameters.new(:user_ids) if user_ids.length > 50 + + users = User.where(id: user_ids).includes(:user_option, + :user_stat, + :default_featured_user_badges, + :user_profile, + :card_background_upload, + :primary_group, + :primary_email + ) + + users = users.filter { |u| guardian.can_see_profile?(u) } + + preload_fields = User.whitelisted_user_custom_fields(guardian) + UserField.all.pluck(:id).map { |fid| "#{User::USER_FIELD_PREFIX}#{fid}" } + User.preload_custom_fields(users, preload_fields) + User.preload_recent_time_read(users) + + render json: users, each_serializer: UserCardSerializer + end + def badges raise Discourse::NotFound unless SiteSetting.enable_badges? show diff --git a/config/routes.rb b/config/routes.rb index 8fd2d19727..38047b91eb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -367,6 +367,8 @@ Discourse::Application.routes.draw do get "user_preferences" => "users#user_preferences_redirect" get ".well-known/change-password", to: redirect(relative_url_root + 'my/preferences/account', status: 302) + get "user-cards" => "users#cards", format: :json + %w{users u}.each_with_index do |root_path, index| get "#{root_path}" => "users#index", constraints: { format: 'html' } diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 13aebdfd2f..7a15170e38 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -3052,6 +3052,34 @@ describe UsersController do end end + describe "#cards" do + fab!(:user) { Discourse.system_user } + fab!(:user2) { Fabricate(:user) } + + it "returns success" do + get "/user-cards.json?user_ids=#{user.id},#{user2.id}" + expect(response.status).to eq(200) + parsed = JSON.parse(response.body)["users"] + + expect(parsed.map { |u| u["username"] }).to contain_exactly(user.username, user2.username) + end + + it "should redirect to login page for anonymous user when profiles are hidden" do + SiteSetting.hide_user_profiles_from_public = true + get "/user-cards.json?user_ids=#{user.id},#{user2.id}" + expect(response).to redirect_to '/login' + end + + it "does not include hidden profiles" do + user2.user_option.update(hide_profile_and_presence: true) + get "/user-cards.json?user_ids=#{user.id},#{user2.id}" + expect(response.status).to eq(200) + parsed = JSON.parse(response.body)["users"] + + expect(parsed.map { |u| u["username"] }).to contain_exactly(user.username) + end + end + describe '#badges' do it "renders fine by default" do get "/u/#{user.username}/badges" From 665d8564d80902a3f9febe837466988ceb3fa33b Mon Sep 17 00:00:00 2001 From: Roman Rizzi Date: Fri, 6 Mar 2020 11:19:34 -0300 Subject: [PATCH 008/633] DEV: Use the updated version of our mousetrap fork (#9111) --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2ff91e0b49..005db309fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1915,8 +1915,8 @@ moment@^2.10.2: integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= "mousetrap@https://github.com/discourse/mousetrap#firefox-alt-key": - version "1.6.3" - resolved "https://github.com/discourse/mousetrap#0d41aa7b173abfb83a6bec3ad39a6a708bc49108" + version "1.6.5" + resolved "https://github.com/discourse/mousetrap#cc8e2c0b9229e1a01ce68de4f339b6fd35503041" ms@2.0.0: version "2.0.0" From a5f61729e0593804051047df86f1a8e1d271794a Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 6 Mar 2020 08:49:28 -0500 Subject: [PATCH 009/633] Revert "Revert "FIX: Don't allow people to clear the upload bucket while it's enabled"" This reverts commit d4fc76b335d012275b06b7ce80d0c1b98f9db33d. --- lib/site_settings/validations.rb | 2 ++ spec/lib/site_settings/validations_spec.rb | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/lib/site_settings/validations.rb b/lib/site_settings/validations.rb index 56987b0dfe..0bf13f7606 100644 --- a/lib/site_settings/validations.rb +++ b/lib/site_settings/validations.rb @@ -143,6 +143,8 @@ module SiteSettings::Validations def validate_s3_upload_bucket(new_val) validate_bucket_setting("s3_upload_bucket", new_val, SiteSetting.s3_backup_bucket) + + validate_error(:s3_upload_bucket_is_required, setting_name: 's3_upload_bucket') if new_val.blank? && SiteSetting.enable_s3_uploads? end def validate_s3_backup_bucket(new_val) diff --git a/spec/lib/site_settings/validations_spec.rb b/spec/lib/site_settings/validations_spec.rb index 7f346d4d5c..69ab73920b 100644 --- a/spec/lib/site_settings/validations_spec.rb +++ b/spec/lib/site_settings/validations_spec.rb @@ -103,6 +103,15 @@ describe SiteSettings::Validations do SiteSetting.s3_backup_bucket = "my-awesome-bucket/foo" expect { validate("my-awesome-bucket/foo/uploads") }.to raise_error(Discourse::InvalidParameters, error_message) end + + it "cannot be made blank unless the setting is false" do + SiteSetting.s3_backup_bucket = "really-real-cool-bucket" + SiteSetting.enable_s3_uploads = true + + expect { validate("") }.to raise_error(Discourse::InvalidParameters) + SiteSetting.enable_s3_uploads = false + validate("") + end end end From 91682408e0fc47e0c2d72a993988797eb2d095ac Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 6 Mar 2020 09:35:38 -0500 Subject: [PATCH 010/633] Remove very bad tests --- spec/models/admin_dashboard_problem_spec.rb | 104 -------------------- 1 file changed, 104 deletions(-) diff --git a/spec/models/admin_dashboard_problem_spec.rb b/spec/models/admin_dashboard_problem_spec.rb index c61b346224..84b596e17b 100644 --- a/spec/models/admin_dashboard_problem_spec.rb +++ b/spec/models/admin_dashboard_problem_spec.rb @@ -194,110 +194,6 @@ describe AdminDashboardData do end end - describe 's3_config_check' do - shared_examples 'problem detection for s3-dependent setting' do - subject { described_class.new.s3_config_check } - let(:access_keys) { [:s3_access_key_id, :s3_secret_access_key] } - let(:all_cred_keys) { access_keys + [:s3_use_iam_profile] } - let(:all_setting_keys) { all_cred_keys + [bucket_key] } - - def all_setting_permutations(keys) - ['a', ''].repeated_permutation(keys.size) do |*values| - hash = Hash[keys.zip(values)] - hash.each do |key, value| - SiteSetting.set(key, value) - end - yield hash - end - end - - context 'when setting is enabled' do - before do - all_setting_keys.each { |key| SiteSetting.set(key, 'foo') } - SiteSetting.set(setting[:key], setting[:enabled_value]) - SiteSetting.set(bucket_key, bucket_value) - end - - context 'when bucket is blank' do - let(:bucket_value) { '' } - - it "always returns a string" do - all_setting_permutations(all_cred_keys) do - expect(subject).to_not be_nil - end - end - end - - context 'when bucket is filled in' do - let(:bucket_value) { 'a' } - before do - SiteSetting.s3_use_iam_profile = use_iam_profile - end - - context 'when using iam profile' do - let(:use_iam_profile) { true } - - it 'always returns nil' do - all_setting_permutations(access_keys) do - expect(subject).to be_nil - end - end - end - - context 'when not using iam profile' do - let(:use_iam_profile) { false } - - it 'returns nil only if both access key fields are filled in' do - all_setting_permutations(access_keys) do |settings| - if settings.values.all? - expect(subject).to be_nil - else - expect(subject).to_not be_nil - end - end - end - end - end - end - - context 'when setting is not enabled' do - before do - SiteSetting.set(setting[:key], setting[:disabled_value]) - end - - it "always returns nil" do - all_setting_permutations(all_setting_keys) do - expect(subject).to be_nil - end - end - end - end - - describe 'uploads' do - let(:setting) do - { - key: :enable_s3_uploads, - enabled_value: true, - disabled_value: false - } - end - let(:bucket_key) { :s3_upload_bucket } - include_examples 'problem detection for s3-dependent setting' - end - - describe 'backups' do - let(:setting) do - { - key: :backup_location, - enabled_value: BackupLocationSiteSetting::S3, - disabled_value: BackupLocationSiteSetting::LOCAL - } - end - let(:bucket_key) { :s3_backup_bucket } - include_examples 'problem detection for s3-dependent setting' - end - end - describe 'force_https_check' do subject { described_class.new(check_force_https: true).force_https_check } From a24f51278a954ebd2d0da9580580caf57c36b2ee Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 6 Mar 2020 17:35:18 +0100 Subject: [PATCH 011/633] DEV: enforces link-rel-noopener linting rule (#8936) * DEV: enforces link-rel-noopener linting rule * oops * better syntax --- .template-lintrc.js | 3 ++- .../javascripts/admin/templates/badges-index.hbs | 2 +- .../admin/templates/customize-themes-edit.hbs | 12 +++++++----- .../admin/templates/customize-themes-index.hbs | 2 +- .../admin/templates/customize-themes-show.hbs | 6 +++--- .../admin/templates/modal/admin-install-theme.hbs | 4 ++-- .../admin/templates/modal/admin-theme-item.hbs | 2 +- .../javascripts/admin/templates/plugins-index.hbs | 2 +- .../javascripts/admin/templates/version-checks.hbs | 2 +- .../discourse/templates/components/backup-codes.hbs | 1 + .../discourse/templates/components/ip-lookup.hbs | 2 +- .../templates/components/user-card-contents.hbs | 3 ++- .../discourse/templates/review-topics.hbs | 2 +- .../discourse/templates/user-invited-show.hbs | 2 +- app/assets/javascripts/discourse/templates/user.hbs | 3 ++- .../javascripts/discourse/templates/user/summary.hbs | 3 ++- .../wizard/templates/components/popular-themes.hbs | 2 +- 17 files changed, 30 insertions(+), 23 deletions(-) diff --git a/.template-lintrc.js b/.template-lintrc.js index 5731778fc0..3a85b721b0 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.js @@ -6,6 +6,7 @@ module.exports = { "self-closing-void-elements": true, "table-groups": true, "style-concatenation": true, - "no-invalid-interactive": true + "no-invalid-interactive": true, + "link-rel-noopener": true } }; diff --git a/app/assets/javascripts/admin/templates/badges-index.hbs b/app/assets/javascripts/admin/templates/badges-index.hbs index a3296fc23e..fd08e67279 100644 --- a/app/assets/javascripts/admin/templates/badges-index.hbs +++ b/app/assets/javascripts/admin/templates/badges-index.hbs @@ -5,7 +5,7 @@

{{i18n 'admin.badges.badge_intro.title'}}

{{#each badgeIntroLinks as |link|}} - + {{d-icon link.icon}} {{I18n link.text}} diff --git a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs index d17ab8d2f3..5fdaa76885 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs @@ -2,20 +2,22 @@

{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to showRouteName model.id replace=true}}{{model.name}}{{/link-to}}

- {{admin-theme-editor - theme=model - editRouteName=editRouteName + {{admin-theme-editor + theme=model + editRouteName=editRouteName currentTargetName=currentTargetName fieldName=fieldName fieldAdded=(action 'fieldAdded') maximized=maximized onlyOverriddenChanged=(action 'onlyOverriddenChanged') }} - +
{{#each externalResources as |resource|}} - + {{d-icon resource.icon}} {{I18n resource.key}} diff --git a/app/assets/javascripts/admin/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/templates/customize-themes-show.hbs index de6b46da74..2687c6b692 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-show.hbs @@ -206,7 +206,7 @@
    {{#each model.uploads as |upload|}}
  • - ${{upload.name}}: {{upload.filename}} + ${{upload.name}}: {{upload.filename}} {{d-button action=(action "removeUpload") actionParam=upload class="second btn-default btn-default cancel-edit" icon="times"}} @@ -241,8 +241,8 @@
{{/if}} - {{d-icon 'desktop'}}{{i18n 'admin.customize.theme.preview'}} - {{d-icon "download"}} {{i18n 'admin.export_json.button_text'}} + {{d-icon 'desktop'}}{{i18n 'admin.customize.theme.preview'}} + {{d-icon "download"}} {{i18n 'admin.export_json.button_text'}} {{d-button action=(action "switchType") label="admin.customize.theme.convert" icon=convertIcon class="btn-default btn-normal" title=convertTooltip}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs b/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs index 7307abb411..eec9151f90 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-install-theme.hbs @@ -11,7 +11,7 @@ {{#each themes as |theme|}}
{{number model.todayCount}}
diff --git a/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs b/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs index 4b4a406c59..a1e64441f9 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs @@ -2,7 +2,7 @@ {{#if report.icon}} {{d-icon report.icon}} {{/if}} - {{report.title}} + {{report.title}} {{number report.todayCount}} diff --git a/app/assets/javascripts/admin/templates/components/admin-report-inline-table.hbs b/app/assets/javascripts/admin/templates/components/admin-report-inline-table.hbs index 70138f829d..76c8eacb0b 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-inline-table.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-inline-table.hbs @@ -1,6 +1,6 @@
{{#each model.data as |data|}} - + {{#if data.icon}} {{d-icon data.icon}} diff --git a/app/assets/javascripts/admin/templates/components/admin-report-storage-stats.hbs b/app/assets/javascripts/admin/templates/components/admin-report-storage-stats.hbs index 054b91d707..fbb9f80a64 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-storage-stats.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-storage-stats.hbs @@ -1,7 +1,7 @@ {{#if showBackupStats}}

- {{d-icon "archive"}} {{i18n "admin.dashboard.backups"}} + {{d-icon "archive"}} {{i18n "admin.dashboard.backups"}}

{{#if backupStats.free_bytes}} diff --git a/app/assets/javascripts/admin/templates/components/admin-report-table.hbs b/app/assets/javascripts/admin/templates/components/admin-report-table.hbs index 6c7838e872..f42189eb9a 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-table.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-table.hbs @@ -24,7 +24,7 @@ {{#if showTotalForSample}} - + {{i18n 'admin.dashboard.reports.totals_for_sample'}} diff --git a/app/assets/javascripts/admin/templates/components/admin-report.hbs b/app/assets/javascripts/admin/templates/components/admin-report.hbs index cb0f333bb0..0c7009cc82 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report.hbs @@ -18,17 +18,17 @@ {{#unless showNotFoundError}}

  • - + {{model.title}} {{#if model.description}} {{#if model.description_link}} - + {{d-icon "question-circle"}} {{else}} - + {{d-icon "question-circle"}} {{/if}} @@ -40,7 +40,7 @@ {{#if shouldDisplayTrend}}
    - + {{#if model.average}} {{number model.currentAverage}}{{#if model.percent}}%{{/if}} {{else}} @@ -77,7 +77,7 @@
    {{d-icon "chart-pie"}} {{#if model.reportUrl}} - + {{#if model.title}} {{model.title}} — diff --git a/app/assets/javascripts/admin/templates/components/admin-theme-editor.hbs b/app/assets/javascripts/admin/templates/components/admin-theme-editor.hbs index bf9d2fd3cb..ad669f4d3d 100644 --- a/app/assets/javascripts/admin/templates/components/admin-theme-editor.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-theme-editor.hbs @@ -25,7 +25,7 @@ {{d-icon (if showAdvanced "angle-double-left" "angle-double-right")}} diff --git a/app/assets/javascripts/admin/templates/components/admin-web-hook-event.hbs b/app/assets/javascripts/admin/templates/components/admin-web-hook-event.hbs index ce952f3ec7..11977f1398 100644 --- a/app/assets/javascripts/admin/templates/components/admin-web-hook-event.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-web-hook-event.hbs @@ -1,5 +1,5 @@
    - {{model.status}} + {{model.status}}
    {{model.id}}
    {{createdAt}}
    diff --git a/app/assets/javascripts/admin/templates/components/flag-user.hbs b/app/assets/javascripts/admin/templates/components/flag-user.hbs index 26087789e5..a6a4ce0f95 100644 --- a/app/assets/javascripts/admin/templates/components/flag-user.hbs +++ b/app/assets/javascripts/admin/templates/components/flag-user.hbs @@ -7,7 +7,7 @@ {{#link-to 'adminUser' user.id user.username class="flag-user-username"}} {{user.username}} {{/link-to}} -
    +
    {{format-age date}}
    diff --git a/app/assets/javascripts/admin/templates/components/install-theme-item.hbs b/app/assets/javascripts/admin/templates/components/install-theme-item.hbs index ed4ddd242f..2697ffe8b9 100644 --- a/app/assets/javascripts/admin/templates/components/install-theme-item.hbs +++ b/app/assets/javascripts/admin/templates/components/install-theme-item.hbs @@ -1,5 +1,5 @@ {{radio-button name="install-items" id=value value=value selection=selection}} -