FEATURE: better UI to manage 2fa (#19338)

In this PR, we introduced an option, that when all authenticators are disabled, but backup codes still exists, user can authenticate with those backup codes. This was reverted as this is not expected behavior.

https://github.com/discourse/discourse/pull/18982

Instead, when the last authenticator is deleted, backup codes should be deleted as well. Because this disables 2fa, user is asked to confirm that action by typing text.

In addition, UI for 2fa preferences was refreshed.
This commit is contained in:
Krzysztof Kotlarek 2022-12-08 09:41:22 +11:00 committed by GitHub
parent 63119144ff
commit e313190fdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 297 additions and 152 deletions

View File

@ -10,9 +10,14 @@
</div>
{{/if}}
{{#if this.dialog.message}}
{{#if (or this.dialog.message this.dialog.confirmPhrase)}}
<div class="dialog-body">
{{this.dialog.message}}
{{#if this.dialog.message}}
<p>{{this.dialog.message}}</p>
{{/if}}
{{#if this.dialog.confirmPhrase}}
<TextField @value={{this.dialog.confirmPhraseInput}} {{on "input" this.dialog.onConfirmPhraseInput}} @id="confirm-phrase" @autocorrect="off" @autocapitalize="off" />
{{/if}}
</div>
{{/if}}
@ -21,9 +26,9 @@
{{#each this.dialog.buttons as |button|}}
<DButton @icon={{button.icon}} @class={{button.class}} @action={{action "handleButtonAction" button}} @translatedLabel={{button.label}} />
{{else}}
<DButton @class={{this.dialog.confirmButtonClass}} @action={{this.dialog.didConfirmWrapped}} @icon={{this.dialog.confirmButtonIcon}} @label={{this.dialog.confirmButtonLabel}} />
<DButton @class={{this.dialog.confirmButtonClass}} @disabled={{this.dialog.confirmButtonDisabled}} @action={{this.dialog.didConfirmWrapped}} @icon={{this.dialog.confirmButtonIcon}} @label={{this.dialog.confirmButtonLabel}} />
{{#if this.dialog.shouldDisplayCancel}}
<DButton @class="btn-default" @action={{this.dialog.cancel}} @label={{this.dialog.cancelButtonLabel}} />
<DButton @class={{this.dialog.cancelButtonClass}} @action={{this.dialog.cancel}} @label={{this.dialog.cancelButtonLabel}} />
{{/if}}
{{/each}}
</div>

View File

@ -1,6 +1,7 @@
import Service from "@ember/service";
import A11yDialog from "a11y-dialog";
import { bind } from "discourse-common/utils/decorators";
import { isBlank } from "@ember/utils";
export default Service.extend({
message: null,
@ -13,7 +14,10 @@ export default Service.extend({
confirmButtonIcon: null,
confirmButtonLabel: null,
confirmButtonClass: null,
confirmPhrase: null,
confirmPhraseInput: null,
cancelButtonLabel: null,
cancelButtonClass: null,
shouldDisplayCancel: null,
didConfirm: null,
@ -32,6 +36,8 @@ export default Service.extend({
confirmButtonLabel = "ok_value",
confirmButtonClass = "btn-primary",
cancelButtonLabel = "cancel_value",
cancelButtonClass = "btn-default",
confirmPhrase,
shouldDisplayCancel,
didConfirm,
@ -39,6 +45,8 @@ export default Service.extend({
buttons,
} = params;
let confirmButtonDisabled = !isBlank(confirmPhrase);
const element = document.getElementById("dialog-holder");
this.setProperties({
@ -49,10 +57,13 @@ export default Service.extend({
title,
titleElementId: title !== null ? "dialog-title" : null,
confirmButtonDisabled,
confirmButtonClass,
confirmButtonLabel,
confirmButtonIcon,
confirmPhrase,
cancelButtonLabel,
cancelButtonClass,
shouldDisplayCancel,
didConfirm,
@ -131,7 +142,10 @@ export default Service.extend({
confirmButtonLabel: null,
confirmButtonIcon: null,
cancelButtonLabel: null,
cancelButtonClass: null,
shouldDisplayCancel: null,
confirmPhrase: null,
confirmPhraseInput: null,
didConfirm: null,
didCancel: null,
@ -160,4 +174,12 @@ export default Service.extend({
cancel() {
this.dialogInstance.hide();
},
@bind
onConfirmPhraseInput() {
this.set(
"confirmButtonDisabled",
this.confirmPhrase && this.confirmPhraseInput !== this.confirmPhrase
);
},
});

View File

@ -42,12 +42,10 @@ export default Component.extend({
}
},
@discourseComputed("backupEnabled", "totpEnabled", "secondFactorMethod")
showToggleMethodLink(backupEnabled, totpEnabled, secondFactorMethod) {
@discourseComputed("backupEnabled", "secondFactorMethod")
showToggleMethodLink(backupEnabled, secondFactorMethod) {
return (
backupEnabled &&
totpEnabled &&
secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY
backupEnabled && secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY
);
},

View File

@ -223,30 +223,22 @@ export default Controller.extend(ModalFunctionality, {
this.clearFlash();
if (
(result.security_key_enabled ||
result.totp_enabled ||
result.backup_enabled) &&
(result.security_key_enabled || result.totp_enabled) &&
!this.secondFactorRequired
) {
let secondFactorMethod;
if (result.security_key_enabled) {
secondFactorMethod = SECOND_FACTOR_METHODS.SECURITY_KEY;
} else if (result.totp_enabled) {
secondFactorMethod = SECOND_FACTOR_METHODS.TOTP;
} else {
secondFactorMethod = SECOND_FACTOR_METHODS.BACKUP_CODE;
}
this.setProperties({
otherMethodAllowed: result.multiple_second_factor_methods,
secondFactorRequired: true,
showLoginButtons: false,
backupEnabled: result.backup_enabled,
totpEnabled: result.totp_enabled,
showSecondFactor: result.totp_enabled || result.backup_enabled,
showSecondFactor: result.totp_enabled,
showSecurityKey: result.security_key_enabled,
secondFactorMethod: result.security_key_enabled
? SECOND_FACTOR_METHODS.SECURITY_KEY
: SECOND_FACTOR_METHODS.TOTP,
securityKeyChallenge: result.challenge,
securityKeyAllowedCredentialIds: result.allowed_credential_ids,
secondFactorMethod,
});
// only need to focus the 2FA input for TOTP

View File

@ -10,6 +10,7 @@ import { findAll } from "discourse/models/login-method";
import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
import { htmlSafe } from "@ember/template";
export default Controller.extend(CanCheckEmails, {
dialog: service(),
@ -113,6 +114,29 @@ export default Controller.extend(CanCheckEmails, {
.finally(() => this.set("resetPasswordLoading", false));
},
disableAllMessage() {
let templateElements = [I18n.t("user.second_factor.delete_confirm_header")];
templateElements.push("<ul>");
this.totps.forEach((totp) => {
templateElements.push(`<li>${totp.name}</li>`);
});
this.security_keys.forEach((key) => {
templateElements.push(`<li>${key.name}</li>`);
});
if (this.currentUser.second_factor_backup_enabled) {
templateElements.push(
`<li>${I18n.t("user.second_factor_backup.title")}</li>`
);
}
templateElements.push("</ul>");
templateElements.push(
I18n.t("user.second_factor.delete_confirm_instruction", {
confirm: I18n.t("user.second_factor.disable"),
})
);
return htmlSafe(templateElements.join(""));
},
actions: {
confirmPassword() {
if (!this.password) {
@ -130,8 +154,11 @@ export default Controller.extend(CanCheckEmails, {
this.dialog.deleteConfirm({
title: I18n.t("user.second_factor.disable_confirm"),
message: this.disableAllMessage(),
confirmButtonLabel: "user.second_factor.disable",
confirmPhrase: I18n.t("user.second_factor.disable"),
confirmButtonIcon: "ban",
cancelButtonClass: "btn-flat",
didConfirm: () => {
this.model
.disableAllSecondFactors()
@ -144,6 +171,88 @@ export default Controller.extend(CanCheckEmails, {
},
});
},
disableSingleSecondFactor(secondFactorMethod) {
if (this.totps.concat(this.security_keys).length === 1) {
this.send("disableAllSecondFactors");
return;
}
this.dialog.deleteConfirm({
title: I18n.t("user.second_factor.delete_single_confirm_title"),
message: I18n.t("user.second_factor.delete_single_confirm_message", {
name: secondFactorMethod.name,
}),
confirmButtonLabel: "user.second_factor.delete",
confirmButtonIcon: "ban",
cancelButtonClass: "btn-flat",
didConfirm: () => {
this.currentUser
.updateSecondFactor(
secondFactorMethod.id,
secondFactorMethod.name,
true,
secondFactorMethod.method
)
.then((response) => {
if (response.error) {
return;
}
this.markDirty();
})
.catch((e) => this.handleError(e))
.finally(() => {
this.setProperties({
totps: this.totps.filter(
(totp) =>
totp.id !== secondFactorMethod.id ||
totp.method !== secondFactorMethod.method
),
security_keys: this.security_keys.filter(
(key) =>
key.id !== secondFactorMethod.id ||
key.method !== secondFactorMethod.method
),
});
this.set("loading", false);
});
},
});
},
disableSecondFactorBackup() {
this.dialog.deleteConfirm({
title: I18n.t("user.second_factor.delete_backup_codes_confirm_title"),
message: I18n.t(
"user.second_factor.delete_backup_codes_confirm_message"
),
confirmButtonLabel: "user.second_factor.delete",
confirmButtonIcon: "ban",
cancelButtonClass: "btn-flat",
didConfirm: () => {
this.set("backupCodes", []);
this.set("loading", true);
this.model
.updateSecondFactor(0, "", true, SECOND_FACTOR_METHODS.BACKUP_CODE)
.then((response) => {
if (response.error) {
this.set("errorMessage", response.error);
return;
}
this.set("errorMessage", null);
this.model.set("second_factor_backup_enabled", false);
this.markDirty();
this.send("closeModal");
})
.catch((error) => {
this.send("closeModal");
this.onError(error);
})
.finally(() => this.set("loading", false));
},
});
},
createTotp() {
const controller = showModal("second-factor-add-totp", {

View File

@ -40,30 +40,6 @@ export default Controller.extend(ModalFunctionality, {
this._hideCopyMessage();
},
disableSecondFactorBackup() {
this.set("backupCodes", []);
this.set("loading", true);
this.model
.updateSecondFactor(0, "", true, SECOND_FACTOR_METHODS.BACKUP_CODE)
.then((response) => {
if (response.error) {
this.set("errorMessage", response.error);
return;
}
this.set("errorMessage", null);
this.model.set("second_factor_backup_enabled", false);
this.markDirty();
this.send("closeModal");
})
.catch((error) => {
this.send("closeModal");
this.onError(error);
})
.finally(() => this.set("loading", false));
},
generateSecondFactorCodes() {
this.set("loading", true);
this.model

View File

@ -3,30 +3,6 @@ import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
actions: {
disableSecondFactor() {
this.user
.updateSecondFactor(
this.model.id,
this.model.name,
true,
this.model.method
)
.then((response) => {
if (response.error) {
return;
}
this.markDirty();
})
.catch((error) => {
this.send("closeModal");
this.onError(error);
})
.finally(() => {
this.set("loading", false);
this.send("closeModal");
});
},
editSecondFactor() {
this.user
.updateSecondFactor(

View File

@ -26,7 +26,7 @@
<div class="caps-lock-warning {{unless this.capsLockOn "hidden"}}">{{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}}</div>
</div>
</div>
<SecondFactorForm @secondFactorMethod={{this.secondFactorMethod}} @secondFactorToken={{this.secondFactorToken}} @class={{this.secondFactorClass}} @backupEnabled={{this.backupEnabled}} @totpEnabled={{this.totpEnabled}} @isLogin={{true}}>
<SecondFactorForm @secondFactorMethod={{this.secondFactorMethod}} @secondFactorToken={{this.secondFactorToken}} @class={{this.secondFactorClass}} @backupEnabled={{this.backupEnabled}} @isLogin={{true}}>
{{#if this.showSecurityKey}}
<SecurityKeyForm @allowedCredentialIds={{this.securityKeyAllowedCredentialIds}} @challenge={{this.securityKeyChallenge}} @showSecurityKey={{this.showSecurityKey}} @showSecondFactor={{this.showSecondFactor}} @secondFactorMethod={{this.secondFactorMethod}} @otherMethodAllowed={{this.otherMethodAllowed}} @action={{action "authenticateSecurityKey"}}>
</SecurityKeyForm>

View File

@ -20,7 +20,6 @@
<div class="actions">
{{#if this.backupEnabled}}
<DButton @class="btn-primary" @icon="redo" @action={{action "generateSecondFactorCodes"}} @type="submit" @isLoading={{this.loading}} @label="user.second_factor_backup.regenerate" />
<DButton @class="btn-danger" @icon="ban" @action={{action "disableSecondFactorBackup"}} @disabled={{this.loading}} @label="user.second_factor_backup.disable" />
{{else}}
<DButton @class="btn-primary" @action={{action "generateSecondFactorCodes"}} @type="submit" @disabled={{this.loading}} @label="user.second_factor_backup.enable" />
{{/if}}

View File

@ -9,5 +9,4 @@
<div class="modal-footer">
<DButton @action={{action "editSecondFactor"}} @class="btn-primary" @label="user.second_factor.save" />
<DButton @action={{action "disableSecondFactor"}} @class="btn-danger no-text" @icon="trash-alt" @aria-label="user.second_factor.disable" @title="user.second_factor.disable" />
</div>

View File

@ -30,59 +30,79 @@
<div class="control-group totp">
<div class="controls">
<h2>{{i18n "user.second_factor.totp.title"}}</h2>
<DButton @action={{action "createTotp"}} @class="btn-primary new-totp" @icon="plus" @disabled={{this.loading}} @label="user.second_factor.totp.add" />
{{#each this.totps as |totp|}}
<div class="second-factor-item">
{{#if totp.name}}
{{totp.name}}
{{else}}
{{i18n "user.second_factor.totp.default_name"}}
{{/if}}
<div class="second-factor-item row">
<div class="details">
{{#if totp.name}}
{{totp.name}}
{{else}}
{{i18n "user.second_factor.totp.default_name"}}
{{/if}}
</div>
{{#if this.isCurrentUser}}
<DButton @action={{action "editSecondFactor" totp}} @class="btn-default btn-small btn-icon pad-left no-text edit" @disabled={{this.loading}} @icon="pencil-alt" @aria-label="user.second_factor.edit" @title="user.second_factor.edit" />
<div class="actions">
<DButton @action={{action "editSecondFactor" totp}} @class="btn-default btn-flat btn-small btn-icon pad-left no-text edit" @disabled={{this.loading}} @icon="pencil-alt" @aria-label="user.second_factor.edit" @title="user.second_factor.edit" />
<DButton @action={{action "disableSingleSecondFactor" totp}} @class="btn-danger btn-flat no-text" @icon="trash-alt" @aria-label="user.second_factor.disable" @title="user.second_factor.disable" />
</div>
{{/if}}
</div>
{{/each}}
<DButton @action={{action "createTotp"}} @class="btn-primary new-totp" @icon="plus" @disabled={{this.loading}} @label="user.second_factor.totp.add" />
</div>
</div>
<div class="control-group security-key">
<div class="controls">
<h2>{{i18n "user.second_factor.security_key.title"}}</h2>
<DButton @action={{action "createSecurityKey"}} @class="btn-primary new-security-key" @icon="plus" @disabled={{this.loading}} @label="user.second_factor.security_key.add" />
{{#each this.security_keys as |security_key|}}
<div class="second-factor-item">
{{#if security_key.name}}
{{security_key.name}}
{{else}}
{{i18n "user.second_factor.security_key.default_name"}}
{{/if}}
<div class="second-factor-item row">
<div class="details">
{{#if security_key.name}}
{{security_key.name}}
{{else}}
{{i18n "user.second_factor.security_key.default_name"}}
{{/if}}
</div>
{{#if this.isCurrentUser}}
<DButton @action={{action "editSecurityKey" security_key}} @class="btn-default btn-small btn-icon pad-left no-text edit" @disabled={{this.loading}} @icon="pencil-alt" @aria-label="user.second_factor.edit" @title="user.second_factor.edit" />
<div class="actions">
<DButton @action={{action "editSecurityKey" security_key}} @class="btn-default btn-flat btn-small btn-icon pad-left no-text edit" @disabled={{this.loading}} @icon="pencil-alt" @aria-label="user.second_factor.edit" @title="user.second_factor.edit" />
<DButton @action={{action "disableSingleSecondFactor" security_key}} @class="btn-danger btn-flat no-text" @icon="trash-alt" @aria-label="user.second_factor.disable" @title="user.second_factor.disable" />
</div>
{{/if}}
</div>
{{/each}}
<DButton @action={{action "createSecurityKey"}} @class="btn-primary new-security-key" @icon="plus" @disabled={{this.loading}} @label="user.second_factor.security_key.add" />
</div>
</div>
<div class="control-group pref-second-factor-backup">
<div class="controls pref-second-factor-backup">
<h2>{{i18n "user.second_factor_backup.title"}}</h2>
{{#if this.model.second_factor_enabled}}
{{#if this.model.second_factor_backup_enabled}}
{{html-safe (i18n "user.second_factor_backup.manage" count=this.model.second_factor_remaining_backup_codes)}}
{{else}}
{{i18n "user.second_factor_backup.enable_long"}}
{{/if}}
<div class="second-factor-item row">
{{#if this.model.second_factor_enabled}}
<div class="details">
{{#if this.model.second_factor_backup_enabled}}
{{html-safe (i18n "user.second_factor_backup.manage" count=this.model.second_factor_remaining_backup_codes)}}
{{else}}
{{i18n "user.second_factor_backup.enable_long"}}
{{/if}}
</div>
{{#if this.isCurrentUser}}
<DButton @action={{action "editSecondFactorBackup"}} @class="btn-default btn-small btn-icon pad-left no-text edit edit-2fa-backup" @disabled={{this.loading}} @icon="pencil-alt" @aria-label="user.second_factor.edit" @title="user.second_factor.edit" />
{{#if this.isCurrentUser}}
<div class="actions">
<DButton @action={{action "editSecondFactorBackup"}} @class="btn-default btn-flat btn-small btn-icon pad-left no-text edit edit-2fa-backup" @disabled={{this.loading}} @icon="pencil-alt" @aria-label="user.second_factor.edit" @title="user.second_factor.edit" />
{{#if this.model.second_factor_backup_enabled}}
<DButton @action={{action "disableSecondFactorBackup"}} @class="btn-danger btn-flat no-text" @icon="trash-alt" @aria-label="user.second_factor.disable" @title="user.second_factor.disable" />
{{/if}}
</div>
{{/if}}
{{else}}
{{i18n "user.second_factor_backup.enable_prerequisites"}}
{{/if}}
{{else}}
{{i18n "user.second_factor_backup.enable_prerequisites"}}
{{/if}}
</div>
</div>
</div>

View File

@ -44,12 +44,6 @@ const RESPONSES = {
security_keys_enabled: true,
allowed_methods: [BACKUP_CODE],
},
ok010010: {
totp_enabled: false,
backup_enabled: true,
security_keys_enabled: false,
allowed_methods: [BACKUP_CODE],
},
};
Object.keys(RESPONSES).forEach((k) => {
@ -184,14 +178,6 @@ acceptance("Second Factor Auth Page", function (needs) {
!exists(".toggle-second-factor-method"),
"no alternative methods are shown if only 1 method is allowed"
);
// only backup codes
await visit("/session/2fa?nonce=ok010010");
assert.ok(exists("form.backup-code-token"), "backup code form is shown");
assert.ok(
!exists(".toggle-second-factor-method"),
"no alternative methods are shown if only 1 method is allowed"
);
});
test("switching 2FA methods", async function (assert) {

View File

@ -3,6 +3,7 @@ import { click, visit } from "@ember/test-helpers";
import {
acceptance,
exists,
query,
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
@ -42,4 +43,17 @@ acceptance("User Preferences - Second Factor Backup", function (needs) {
assert.ok(exists(".backup-codes-area"), "shows backup codes");
});
test("delete backup codes", async function (assert) {
updateCurrentUser({ second_factor_enabled: true });
await visit("/u/eviltrout/preferences/second-factor");
await click(".edit-2fa-backup");
await click(".second-factor-backup-preferences .btn-primary");
await click(".modal-close");
await click(".pref-second-factor-backup .btn-danger");
assert.strictEqual(
query("#dialog-title").innerText.trim(),
"Deleting backup codes"
);
});
});

View File

@ -4,6 +4,7 @@ import {
acceptance,
exists,
query,
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
acceptance("User Preferences - Second Factor", function (needs) {
@ -14,6 +15,8 @@ acceptance("User Preferences - Second Factor", function (needs) {
return helper.response({
success: "OK",
password_required: "true",
totps: [{ id: 1, name: "one of them" }],
security_keys: [{ id: 2, name: "key" }],
});
});
@ -90,4 +93,33 @@ acceptance("User Preferences - Second Factor", function (needs) {
);
}
});
test("delete second factor security method", async function (assert) {
updateCurrentUser({ moderator: false, admin: false, trust_level: 1 });
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");
await click(".totp .btn-danger");
assert.strictEqual(
query("#dialog-title").innerText.trim(),
"Deleting an authenticator"
);
await click(".dialog-close");
await click(".security-key .btn-danger");
assert.strictEqual(
query("#dialog-title").innerText.trim(),
"Deleting an authenticator"
);
await click(".dialog-close");
await click(".btn-danger.btn-icon-text");
assert.strictEqual(
query("#dialog-title").innerText.trim(),
"Are you sure you want to disable two-factor authentication?"
);
});
});

View File

@ -1,7 +1,13 @@
import I18n from "I18n";
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { click, render, settled, triggerKeyEvent } from "@ember/test-helpers";
import {
click,
fillIn,
render,
settled,
triggerKeyEvent,
} from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
import { query } from "discourse/tests/helpers/qunit-helpers";
@ -388,4 +394,17 @@ module("Integration | Component | dialog-holder", function (hooks) {
".btn-primary element is not present in the dialog"
);
});
test("delete confirm with confirmation phase", async function (assert) {
await render(hbs`<DialogHolder />`);
this.dialog.deleteConfirm({
message: "A delete confirm message",
confirmPhrase: "test",
});
await settled();
assert.strictEqual(query(".btn-danger").disabled, true);
await fillIn("#confirm-phrase", "test");
assert.strictEqual(query(".btn-danger").disabled, false);
});
});

View File

@ -758,6 +758,19 @@
}
.second-factor-item {
margin-top: 0.75em;
width: 500px;
display: flex;
justify-content: space-between;
border-bottom: 1px solid #ddd;
margin: 5px 0px;
align-items: center;
&:last-child {
border-bottom: 0;
}
.btn-danger .d-icon {
color: var(--danger);
}
}
.btn.edit {
min-height: auto;

View File

@ -433,3 +433,13 @@
margin-top: 1em;
}
}
.second-factor {
.second-factor-item {
width: auto;
display: flex;
justify-content: space-between;
}
.details {
width: 75%;
}
}

View File

@ -79,7 +79,7 @@ module SecondFactorManager
end
def has_any_second_factor_methods_enabled?
totp_enabled? || security_keys_enabled? || backup_codes_enabled?
totp_enabled? || security_keys_enabled?
end
def has_multiple_second_factor_methods?

View File

@ -45,7 +45,7 @@ class AdminDetailedUserSerializer < AdminUserSerializer
has_many :groups, embed: :object, serializer: BasicGroupSerializer
def second_factor_enabled
object.totp_enabled? || object.security_keys_enabled? || object.backup_codes_enabled?
object.totp_enabled? || object.security_keys_enabled?
end
def can_disable_second_factor

View File

@ -238,7 +238,7 @@ class CurrentUserSerializer < BasicUserSerializer
end
def second_factor_enabled
object.totp_enabled? || object.security_keys_enabled? || object.backup_codes_enabled?
object.totp_enabled? || object.security_keys_enabled?
end
def featured_topic

View File

@ -105,7 +105,7 @@ class UserSerializer < UserCardSerializer
end
def second_factor_enabled
object.totp_enabled? || object.security_keys_enabled? || object.backup_codes_enabled?
object.totp_enabled? || object.security_keys_enabled?
end
def include_second_factor_backup_enabled?

View File

@ -1392,7 +1392,14 @@ en:
use: "Use Authenticator app"
enforced_notice: "You are required to enable two-factor authentication before accessing this site."
disable: "Disable"
disable_confirm: "Are you sure you want to disable all two-factor methods?"
disable_confirm: "Are you sure you want to disable two-factor authentication?"
delete: "Delete"
delete_confirm_header: "These Token-Based Authenticators and Physical Security Keys will be deleted:"
delete_confirm_instruction: 'To confirm, type <strong>%{confirm}</strong> in the box below.'
delete_single_confirm_title: "Deleting an authenticator"
delete_single_confirm_message: "You are deleting %{name}. You can't undo this action. If you change your mind, you have to register this authenticator again."
delete_backup_codes_confirm_title: "Deleting backup codes"
delete_backup_codes_confirm_message: "You are deleting backup codes. You can't undo this action. If you change your mind, you have to regenerate backup codes."
save: "Save"
edit: "Edit"
edit_title: "Edit Authenticator"

View File

@ -31,18 +31,6 @@ RSpec.describe AdminUserListSerializer do
end
end
context "when backup codes enabled" do
before do
Fabricate(:user_second_factor_backup, user: user)
end
it "is true" do
json = serializer.as_json
expect(json[:second_factor_enabled]).to eq(true)
end
end
describe "emails" do
fab!(:admin) { Fabricate(:user, admin: true, email: "admin@email.com") }
fab!(:moderator) { Fabricate(:user, moderator: true, email: "moderator@email.com") }

View File

@ -102,16 +102,6 @@ RSpec.describe CurrentUserSerializer do
expect(json[:second_factor_enabled]).to eq(true)
end
end
context "when backup codes enabled" do
before do
User.any_instance.stubs(:backup_codes_enabled?).returns(true)
end
it "is true" do
expect(json[:second_factor_enabled]).to eq(true)
end
end
end
describe "#groups" do

View File

@ -250,16 +250,6 @@ RSpec.describe UserSerializer do
expect(json[:second_factor_enabled]).to eq(true)
end
end
context "when backup codes enabled" do
before do
User.any_instance.stubs(:backup_codes_enabled?).returns(true)
end
it "is true" do
expect(json[:second_factor_enabled]).to eq(true)
end
end
end
describe "ignored and muted" do