mirror of
https://github.com/discourse/discourse.git
synced 2024-11-27 03:10:46 -06:00
DEV: Standardize session confirmation prompt (#24212)
Switches to using a dialog to confirm a session (i.e. sudo mode for account changes where we want to be extra sure the current user is who they say they are) to match what we do with passkeys.
This commit is contained in:
parent
dcaa719363
commit
a1c1f7ce75
@ -21,17 +21,12 @@ export default Controller.extend(CanCheckEmails, {
|
||||
modal: service(),
|
||||
loading: false,
|
||||
dirty: false,
|
||||
resetPasswordLoading: false,
|
||||
resetPasswordProgress: "",
|
||||
password: null,
|
||||
errorMessage: null,
|
||||
newUsername: null,
|
||||
backupEnabled: alias("model.second_factor_backup_enabled"),
|
||||
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
|
||||
totps: null,
|
||||
|
||||
loaded: false,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set("totps", []);
|
||||
@ -47,6 +42,11 @@ export default Controller.extend(CanCheckEmails, {
|
||||
return user && user.enforcedSecondFactor;
|
||||
},
|
||||
|
||||
@discourseComputed("totps", "security_keys")
|
||||
hasSecondFactors(totps, security_keys) {
|
||||
return totps.length > 0 || security_keys.length > 0;
|
||||
},
|
||||
|
||||
@action
|
||||
handleError(error) {
|
||||
if (error.jqXHR) {
|
||||
@ -81,7 +81,7 @@ export default Controller.extend(CanCheckEmails, {
|
||||
this.set("loading", true);
|
||||
|
||||
this.model
|
||||
.loadSecondFactorCodes(this.password)
|
||||
.loadSecondFactorCodes()
|
||||
.then((response) => {
|
||||
if (response.error) {
|
||||
this.set("errorMessage", response.error);
|
||||
@ -90,17 +90,10 @@ export default Controller.extend(CanCheckEmails, {
|
||||
|
||||
this.setProperties({
|
||||
errorMessage: null,
|
||||
loaded: true,
|
||||
totps: response.totps,
|
||||
security_keys: response.security_keys,
|
||||
password: null,
|
||||
dirty: false,
|
||||
});
|
||||
this.set(
|
||||
"model.second_factor_enabled",
|
||||
(response.totps && response.totps.length > 0) ||
|
||||
(response.security_keys && response.security_keys.length > 0)
|
||||
);
|
||||
})
|
||||
.catch((e) => this.handleError(e))
|
||||
.finally(() => this.set("loading", false));
|
||||
@ -111,37 +104,7 @@ export default Controller.extend(CanCheckEmails, {
|
||||
this.set("dirty", true);
|
||||
},
|
||||
|
||||
@action
|
||||
resetPassword(event) {
|
||||
event?.preventDefault();
|
||||
|
||||
this.setProperties({
|
||||
resetPasswordLoading: true,
|
||||
resetPasswordProgress: "",
|
||||
});
|
||||
|
||||
return this.model
|
||||
.changePassword()
|
||||
.then(() => {
|
||||
this.set(
|
||||
"resetPasswordProgress",
|
||||
I18n.t("user.change_password.success")
|
||||
);
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => this.set("resetPasswordLoading", false));
|
||||
},
|
||||
|
||||
actions: {
|
||||
confirmPassword() {
|
||||
if (!this.password) {
|
||||
return;
|
||||
}
|
||||
this.markDirty();
|
||||
this.loadSecondFactors();
|
||||
this.set("password", null);
|
||||
},
|
||||
|
||||
disableAllSecondFactors() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
|
@ -2,6 +2,7 @@ import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { gt } from "@ember/object/computed";
|
||||
import { inject as service } from "@ember/service";
|
||||
import ConfirmSession from "discourse/components/dialog-messages/confirm-session";
|
||||
import AuthTokenModal from "discourse/components/modal/auth-token";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
@ -17,6 +18,8 @@ const DEFAULT_AUTH_TOKENS_COUNT = 2;
|
||||
|
||||
export default Controller.extend(CanCheckEmails, {
|
||||
modal: service(),
|
||||
dialog: service(),
|
||||
router: service(),
|
||||
passwordProgress: null,
|
||||
subpageTitle: I18n.t("user.preferences_nav.security"),
|
||||
showAllAuthTokens: false,
|
||||
@ -114,6 +117,27 @@ export default Controller.extend(CanCheckEmails, {
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
@action
|
||||
async manage2FA() {
|
||||
try {
|
||||
const trustedSession = await this.model.trustedSession();
|
||||
|
||||
if (!trustedSession.success) {
|
||||
this.dialog.dialog({
|
||||
title: I18n.t("user.confirm_access.title"),
|
||||
type: "notice",
|
||||
bodyComponent: ConfirmSession,
|
||||
didConfirm: () =>
|
||||
this.router.transitionTo("preferences.second-factor"),
|
||||
});
|
||||
} else {
|
||||
await this.router.transitionTo("preferences.second-factor");
|
||||
}
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.set("saved", false);
|
||||
|
@ -540,9 +540,8 @@ const User = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
loadSecondFactorCodes(password) {
|
||||
loadSecondFactorCodes() {
|
||||
return ajax("/u/second_factors.json", {
|
||||
data: { password },
|
||||
type: "POST",
|
||||
});
|
||||
},
|
||||
|
@ -5,6 +5,7 @@ import RestrictedUserRoute from "discourse/routes/restricted-user";
|
||||
export default RestrictedUserRoute.extend({
|
||||
currentUser: service(),
|
||||
siteSettings: service(),
|
||||
router: service(),
|
||||
|
||||
model() {
|
||||
return this.modelFor("user");
|
||||
@ -15,15 +16,15 @@ export default RestrictedUserRoute.extend({
|
||||
controller.set("loading", true);
|
||||
|
||||
model
|
||||
.loadSecondFactorCodes("")
|
||||
.loadSecondFactorCodes()
|
||||
.then((response) => {
|
||||
if (response.error) {
|
||||
controller.set("errorMessage", response.error);
|
||||
} else if (response.unconfirmed_session) {
|
||||
this.router.transitionTo("preferences.security");
|
||||
} else {
|
||||
controller.setProperties({
|
||||
errorMessage: null,
|
||||
loaded: !response.password_required,
|
||||
dirty: !!response.password_required,
|
||||
totps: response.totps,
|
||||
security_keys: response.security_keys,
|
||||
});
|
||||
|
@ -19,194 +19,140 @@
|
||||
<div class="alert alert-error">{{this.errorMessage}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.loaded}}
|
||||
<div class="control-group totp">
|
||||
<div class="controls">
|
||||
<h2>{{i18n "user.second_factor.totp.title"}}</h2>
|
||||
{{#each this.totps as |totp|}}
|
||||
<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}}
|
||||
<div class="actions">
|
||||
<TokenBasedAuthDropdown
|
||||
@totp={{totp}}
|
||||
@editSecondFactor={{action "editSecondFactor"}}
|
||||
@disableSingleSecondFactor={{action
|
||||
"disableSingleSecondFactor"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
<DButton
|
||||
@action={{action "createTotp"}}
|
||||
@icon="plus"
|
||||
@disabled={{this.loading}}
|
||||
@label="user.second_factor.totp.add"
|
||||
class="btn-default new-totp"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group security-key">
|
||||
<div class="controls">
|
||||
<h2>{{i18n "user.second_factor.security_key.title"}}</h2>
|
||||
{{#each this.security_keys as |security_key|}}
|
||||
<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}}
|
||||
<div class="actions">
|
||||
<SecurityKeyDropdown
|
||||
@securityKey={{security_key}}
|
||||
@editSecurityKey={{action "editSecurityKey"}}
|
||||
@disableSingleSecondFactor={{action
|
||||
"disableSingleSecondFactor"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
<DButton
|
||||
@action={{action "createSecurityKey"}}
|
||||
@icon="plus"
|
||||
@disabled={{this.loading}}
|
||||
@label="user.second_factor.security_key.add"
|
||||
class="btn-default new-security-key"
|
||||
/>
|
||||
</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>
|
||||
<div class="control-group totp">
|
||||
<div class="controls">
|
||||
<h2>{{i18n "user.second_factor.totp.title"}}</h2>
|
||||
{{#each this.totps as |totp|}}
|
||||
<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}}
|
||||
<DButton
|
||||
@action={{action "editSecondFactorBackup"}}
|
||||
@icon="plus"
|
||||
@disabled={{this.loading}}
|
||||
@label="user.second_factor_backup.enable_long"
|
||||
class="btn-default new-second-factor-backup"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if
|
||||
(and
|
||||
this.model.second_factor_backup_enabled this.isCurrentUser
|
||||
)
|
||||
}}
|
||||
<div class="actions">
|
||||
<TwoFactorBackupDropdown
|
||||
@secondFactorBackupEnabled={{this.model.second_factor_backup_enabled}}
|
||||
@editSecondFactorBackup={{action
|
||||
"editSecondFactorBackup"
|
||||
}}
|
||||
@disableSecondFactorBackup={{action
|
||||
"disableSecondFactorBackup"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="details">
|
||||
{{#if totp.name}}
|
||||
{{totp.name}}
|
||||
{{else}}
|
||||
{{i18n "user.second_factor.totp.default_name"}}
|
||||
{{/if}}
|
||||
|
||||
{{else}}
|
||||
{{i18n "user.second_factor_backup.enable_prerequisites"}}
|
||||
</div>
|
||||
{{#if this.isCurrentUser}}
|
||||
<div class="actions">
|
||||
<TokenBasedAuthDropdown
|
||||
@totp={{totp}}
|
||||
@editSecondFactor={{action "editSecondFactor"}}
|
||||
@disableSingleSecondFactor={{action
|
||||
"disableSingleSecondFactor"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
<DButton
|
||||
@action={{action "createTotp"}}
|
||||
@icon="plus"
|
||||
@disabled={{this.loading}}
|
||||
@label="user.second_factor.totp.add"
|
||||
class="btn-default new-totp"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.model.second_factor_enabled}}
|
||||
{{#unless this.showEnforcedNotice}}
|
||||
<div class="control-group pref-second-factor-disable-all">
|
||||
<div class="controls -actions">
|
||||
<DButton
|
||||
@icon="ban"
|
||||
@action={{action "disableAllSecondFactors"}}
|
||||
@disabled={{this.loading}}
|
||||
@label="user.second_factor.disable_all"
|
||||
class="btn-danger"
|
||||
/>
|
||||
<CancelLink
|
||||
@route="preferences.security"
|
||||
@args={{this.model.username}}
|
||||
/>
|
||||
<div class="control-group security-key">
|
||||
<div class="controls">
|
||||
<h2>{{i18n "user.second_factor.security_key.title"}}</h2>
|
||||
{{#each this.security_keys as |security_key|}}
|
||||
<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>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n "user.password.title"}}</label>
|
||||
|
||||
<div class="controls">
|
||||
<div>
|
||||
<TextField
|
||||
@value={{this.password}}
|
||||
@id="password"
|
||||
@type="password"
|
||||
@classNames="input-large"
|
||||
@autofocus="autofocus"
|
||||
/>
|
||||
</div>
|
||||
<div class="instructions">
|
||||
{{i18n "user.second_factor.confirm_password_description"}}
|
||||
{{#if this.isCurrentUser}}
|
||||
<div class="actions">
|
||||
<SecurityKeyDropdown
|
||||
@securityKey={{security_key}}
|
||||
@editSecurityKey={{action "editSecurityKey"}}
|
||||
@disableSingleSecondFactor={{action
|
||||
"disableSingleSecondFactor"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
<DButton
|
||||
@action={{action "createSecurityKey"}}
|
||||
@icon="plus"
|
||||
@disabled={{this.loading}}
|
||||
@label="user.second_factor.security_key.add"
|
||||
class="btn-default new-security-key"
|
||||
/>
|
||||
</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>
|
||||
<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}}
|
||||
<DButton
|
||||
@action={{action "editSecondFactorBackup"}}
|
||||
@icon="plus"
|
||||
@disabled={{this.loading}}
|
||||
@label="user.second_factor_backup.enable_long"
|
||||
class="btn-default new-second-factor-backup"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if
|
||||
(and this.model.second_factor_backup_enabled this.isCurrentUser)
|
||||
}}
|
||||
<div class="actions">
|
||||
<TwoFactorBackupDropdown
|
||||
@secondFactorBackupEnabled={{this.model.second_factor_backup_enabled}}
|
||||
@editSecondFactorBackup={{action "editSecondFactorBackup"}}
|
||||
@disableSecondFactorBackup={{action
|
||||
"disableSecondFactorBackup"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{else}}
|
||||
{{i18n "user.second_factor_backup.enable_prerequisites"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls -actions">
|
||||
<DButton
|
||||
@action={{action "confirmPassword"}}
|
||||
@disabled={{this.loading}}
|
||||
@label="continue"
|
||||
type="submit"
|
||||
class="btn-primary"
|
||||
/>
|
||||
|
||||
{{#unless this.showEnforcedNotice}}
|
||||
{{#if this.hasSecondFactors}}
|
||||
{{#unless this.showEnforcedNotice}}
|
||||
<div class="control-group pref-second-factor-disable-all">
|
||||
<div class="controls -actions">
|
||||
<DButton
|
||||
@icon="ban"
|
||||
@action={{action "disableAllSecondFactors"}}
|
||||
@disabled={{this.loading}}
|
||||
@label="user.second_factor.disable_all"
|
||||
class="btn-danger"
|
||||
/>
|
||||
<CancelLink
|
||||
@route="preferences.security"
|
||||
@args={{this.model.username}}
|
||||
/>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls" style="margin-top: 5px">
|
||||
{{this.resetPasswordProgress}}
|
||||
{{#unless this.resetPasswordLoading}}
|
||||
<a
|
||||
href
|
||||
class="instructions"
|
||||
{{on "click" this.resetPassword}}
|
||||
>{{i18n "user.second_factor.forgot_password"}}</a>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</form>
|
||||
</ConditionalLoadingSpinner>
|
||||
|
@ -19,28 +19,26 @@
|
||||
<UserPreferences::UserPasskeys @model={{@model}} />
|
||||
{{/if}}
|
||||
|
||||
<div
|
||||
class="control-group pref-second-factor"
|
||||
data-setting-name="user-second-factor"
|
||||
>
|
||||
<label class="control-label">{{i18n "user.second_factor.title"}}</label>
|
||||
{{#unless this.model.second_factor_enabled}}
|
||||
{{#if this.isCurrentUser}}
|
||||
<div
|
||||
class="control-group pref-second-factor"
|
||||
data-setting-name="user-second-factor"
|
||||
>
|
||||
<label class="control-label">{{i18n "user.second_factor.title"}}</label>
|
||||
<div class="instructions">
|
||||
{{i18n "user.second_factor.short_description"}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
<div class="controls pref-second-factor">
|
||||
{{#if this.isCurrentUser}}
|
||||
<LinkTo
|
||||
@route="preferences.second-factor"
|
||||
class="btn btn-default btn-second-factor"
|
||||
>
|
||||
{{d-icon "lock"}}
|
||||
<span>{{i18n "user.second_factor.enable"}}</span>
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
||||
<div class="controls pref-second-factor">
|
||||
<DButton
|
||||
@action={{this.manage2FA}}
|
||||
@icon="lock"
|
||||
@label="user.second_factor.enable"
|
||||
class="btn-second-factor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canCheckEmails}}
|
||||
|
@ -15,13 +15,13 @@ async function catchAbortedTransition() {
|
||||
}
|
||||
}
|
||||
|
||||
acceptance("Enforce Second Factor", function (needs) {
|
||||
acceptance("Enforce Second Factor for unconfirmed session", function (needs) {
|
||||
needs.user();
|
||||
needs.pretender((server, helper) => {
|
||||
server.post("/u/second_factors.json", () => {
|
||||
return helper.response({
|
||||
success: "OK",
|
||||
password_required: "true",
|
||||
unconfirmed_session: "true",
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -30,22 +30,10 @@ acceptance("Enforce Second Factor", function (needs) {
|
||||
await visit("/u/eviltrout/preferences/second-factor");
|
||||
this.siteSettings.enforce_second_factor = "staff";
|
||||
|
||||
await catchAbortedTransition();
|
||||
|
||||
assert.strictEqual(
|
||||
currentRouteName(),
|
||||
"preferences.second-factor",
|
||||
"it will not transition from second-factor preferences"
|
||||
);
|
||||
|
||||
await click(
|
||||
".sidebar-section[data-section-name='community'] .sidebar-section-link[data-link-name='admin']"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
currentRouteName(),
|
||||
"preferences.second-factor",
|
||||
"it stays at second-factor preferences"
|
||||
"preferences.security",
|
||||
"it transitions to security preferences"
|
||||
);
|
||||
});
|
||||
|
||||
@ -55,26 +43,10 @@ acceptance("Enforce Second Factor", function (needs) {
|
||||
await visit("/u/eviltrout/preferences/second-factor");
|
||||
this.siteSettings.enforce_second_factor = "all";
|
||||
|
||||
await catchAbortedTransition();
|
||||
|
||||
assert.strictEqual(
|
||||
currentRouteName(),
|
||||
"preferences.second-factor",
|
||||
"it will not transition from second-factor preferences"
|
||||
);
|
||||
|
||||
await click(
|
||||
".sidebar-section[data-section-name='community'] .sidebar-more-section-links-details-summary"
|
||||
);
|
||||
|
||||
await click(
|
||||
".sidebar-section[data-section-name='community'] .sidebar-section-link[data-link-name='about']"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
currentRouteName(),
|
||||
"preferences.second-factor",
|
||||
"it stays at second-factor preferences"
|
||||
"preferences.security",
|
||||
"it will transition to security preferences"
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import { click, currentRouteName, fillIn, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import {
|
||||
acceptance,
|
||||
@ -14,7 +14,6 @@ acceptance("User Preferences - Second Factor", function (needs) {
|
||||
server.post("/u/second_factors.json", () => {
|
||||
return helper.response({
|
||||
success: "OK",
|
||||
password_required: "true",
|
||||
totps: [{ id: 1, name: "one of them" }],
|
||||
security_keys: [{ id: 2, name: "key" }],
|
||||
});
|
||||
@ -57,12 +56,6 @@ acceptance("User Preferences - Second Factor", function (needs) {
|
||||
test("second factor totp", async function (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-totp");
|
||||
assert.ok(exists(".qr-code img"), "shows qr code image");
|
||||
|
||||
@ -82,12 +75,6 @@ acceptance("User Preferences - Second Factor", function (needs) {
|
||||
test("second factor security keys", async function (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");
|
||||
|
||||
@ -109,10 +96,6 @@ acceptance("User Preferences - Second Factor", function (needs) {
|
||||
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(".token-based-auth-dropdown .select-kit-header");
|
||||
await click("li[data-name='Disable']");
|
||||
|
||||
@ -147,3 +130,29 @@ acceptance("User Preferences - Second Factor", function (needs) {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
acceptance(
|
||||
"User Preferences - Second Factor - Unconfirmed Session",
|
||||
function (needs) {
|
||||
needs.user();
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
server.post("/u/second_factors.json", () => {
|
||||
return helper.response({
|
||||
success: "OK",
|
||||
unconfirmed_session: "true",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("redirects to security preferences", async function (assert) {
|
||||
await visit("/u/eviltrout/preferences/second-factor");
|
||||
|
||||
assert.strictEqual(
|
||||
currentRouteName(),
|
||||
"preferences.security",
|
||||
"it transitions to security preferences"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -518,6 +518,23 @@
|
||||
}
|
||||
|
||||
.user-preferences {
|
||||
.form-vertical {
|
||||
width: 500px;
|
||||
max-width: 100%;
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.category-selector,
|
||||
.tag-chooser,
|
||||
textarea,
|
||||
input.user-selector,
|
||||
.user-chooser {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 100px;
|
||||
}
|
||||
@ -735,6 +752,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pref-second-factor {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.paginated-topics-list {
|
||||
|
@ -107,10 +107,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pref-second-factor {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.invite-controls .btn {
|
||||
margin-right: 0px;
|
||||
}
|
||||
@ -252,21 +248,6 @@ table.user-invite-list {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
.user-preferences {
|
||||
.form-vertical {
|
||||
width: 500px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.category-selector,
|
||||
.tag-chooser,
|
||||
textarea,
|
||||
input.user-selector,
|
||||
.user-chooser {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.user-crawler {
|
||||
.username {
|
||||
margin-left: 5px;
|
||||
|
@ -1533,12 +1533,6 @@ class UsersController < ApplicationController
|
||||
raise Discourse::NotFound
|
||||
end
|
||||
|
||||
if params[:password].present?
|
||||
if !confirm_secure_session
|
||||
return render json: failed_json.merge(error: I18n.t("login.incorrect_password"))
|
||||
end
|
||||
end
|
||||
|
||||
if secure_session_confirmed?
|
||||
totp_second_factors =
|
||||
current_user
|
||||
@ -1555,7 +1549,7 @@ class UsersController < ApplicationController
|
||||
|
||||
render json: success_json.merge(totps: totp_second_factors, security_keys: security_keys)
|
||||
else
|
||||
render json: success_json.merge(password_required: true)
|
||||
render json: success_json.merge(unconfirmed_session: true)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1463,8 +1463,6 @@ en:
|
||||
title: "Two-Factor Authentication"
|
||||
enable: "Manage Two-Factor Authentication"
|
||||
disable_all: "Disable All"
|
||||
forgot_password: "Forgot password?"
|
||||
confirm_password_description: "Please confirm your password to continue"
|
||||
name: "Name"
|
||||
label: "Code"
|
||||
rate_limit: "Please wait before trying another authentication code."
|
||||
|
@ -6322,55 +6322,40 @@ RSpec.describe UsersController do
|
||||
SiteSetting.enable_discourse_connect = false
|
||||
end
|
||||
|
||||
context "when the password parameter is not provided" do
|
||||
let(:password) { "" }
|
||||
context "when the session is unconfirmed" do
|
||||
it "returns unconfirmed session response" do
|
||||
post "/u/second_factors.json"
|
||||
|
||||
before { post "/u/second_factors.json", params: { password: password } }
|
||||
|
||||
it "returns password required response" do
|
||||
expect(response.status).to eq(200)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body["password_required"]).to eq(true)
|
||||
expect(response_body["unconfirmed_session"]).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the password is provided" do
|
||||
fab!(:user) { Fabricate(:user, password: "8555039dd212cc66ec68") }
|
||||
context "when the session is confirmed" do
|
||||
fab!(:user) { Fabricate(:user, password: "acoolpassword") }
|
||||
|
||||
context "when the password is correct" do
|
||||
let(:password) { "8555039dd212cc66ec68" }
|
||||
|
||||
it "returns a list of enabled totps and security_key second factors" do
|
||||
totp_second_factor = Fabricate(:user_second_factor_totp, user: user)
|
||||
security_key_second_factor =
|
||||
Fabricate(
|
||||
:user_security_key,
|
||||
user: user,
|
||||
factor_type: UserSecurityKey.factor_types[:second_factor],
|
||||
)
|
||||
|
||||
post "/u/second_factors.json", params: { password: password }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body["totps"].map { |second_factor| second_factor["id"] }).to include(
|
||||
totp_second_factor.id,
|
||||
it "returns a list of enabled totps and security_key second factors" do
|
||||
totp_second_factor = Fabricate(:user_second_factor_totp, user: user)
|
||||
security_key_second_factor =
|
||||
Fabricate(
|
||||
:user_security_key,
|
||||
user: user,
|
||||
factor_type: UserSecurityKey.factor_types[:second_factor],
|
||||
)
|
||||
expect(
|
||||
response_body["security_keys"].map { |second_factor| second_factor["id"] },
|
||||
).to include(security_key_second_factor.id)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the password is not correct" do
|
||||
let(:password) { "wrongpassword" }
|
||||
post "/u/confirm-session.json", params: { password: "acoolpassword" }
|
||||
|
||||
it "returns the incorrect password response" do
|
||||
post "/u/second_factors.json", params: { password: password }
|
||||
post "/u/second_factors.json"
|
||||
|
||||
response_body = response.parsed_body
|
||||
expect(response_body["error"]).to eq(I18n.t("login.incorrect_password"))
|
||||
end
|
||||
expect(response.status).to eq(200)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body["totps"].map { |second_factor| second_factor["id"] }).to include(
|
||||
totp_second_factor.id,
|
||||
)
|
||||
expect(
|
||||
response_body["security_keys"].map { |second_factor| second_factor["id"] },
|
||||
).to include(security_key_second_factor.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -9,10 +9,10 @@ module PageObjects
|
||||
end
|
||||
|
||||
def visit_second_factor(password)
|
||||
click_link(class: "btn-second-factor")
|
||||
click_button "Manage Two-Factor Authentication"
|
||||
|
||||
find(".second-factor input#password").fill_in(with: password)
|
||||
find(".second-factor .btn-primary").click
|
||||
find(".dialog-body input#password").fill_in(with: password)
|
||||
find(".dialog-body .btn-primary").click
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user