mirror of
https://github.com/discourse/discourse.git
synced 2025-01-19 07:02:46 +08:00
UX: Use regular reset-password flow for expired passwords (#27316)
This makes it more obvious what's happening, and makes it much less likely that users will send repeated reset emails (and thereby hit the rate limit)
Followup to e97ef7e9af
This commit is contained in:
parent
f0539afb02
commit
aa37be3323
|
@ -5,6 +5,7 @@
|
|||
@flash={{this.flash}}
|
||||
@flashType={{this.flashType}}
|
||||
{{did-insert this.preloadLogin}}
|
||||
{{on "click" this.interceptResetLink}}
|
||||
>
|
||||
<:body>
|
||||
<PluginOutlet @name="login-before-modal-body" @connectorTagName="div" />
|
||||
|
|
|
@ -3,10 +3,12 @@ import { tracked } from "@glimmer/tracking";
|
|||
import { action } from "@ember/object";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import cookie, { removeCookie } from "discourse/lib/cookie";
|
||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import { areCookiesEnabled } from "discourse/lib/utilities";
|
||||
import { wavingHandURL } from "discourse/lib/waving-hand-url";
|
||||
import {
|
||||
|
@ -18,6 +20,7 @@ import { SECOND_FACTOR_METHODS } from "discourse/models/user";
|
|||
import escape from "discourse-common/lib/escape";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import I18n from "discourse-i18n";
|
||||
import ForgotPassword from "./forgot-password";
|
||||
|
||||
export default class Login extends Component {
|
||||
@service capabilities;
|
||||
|
@ -25,6 +28,7 @@ export default class Login extends Component {
|
|||
@service siteSettings;
|
||||
@service site;
|
||||
@service login;
|
||||
@service modal;
|
||||
|
||||
@tracked loggingIn = false;
|
||||
@tracked loggedIn = false;
|
||||
|
@ -248,6 +252,13 @@ export default class Login extends Component {
|
|||
} else if (result.reason === "suspended") {
|
||||
this.args.closeModal();
|
||||
this.dialog.alert(result.error);
|
||||
} else if (result.reason === "expired") {
|
||||
this.flash = htmlSafe(
|
||||
I18n.t("login.password_expired", {
|
||||
reset_url: getURL("/password-reset"),
|
||||
})
|
||||
);
|
||||
this.flashType = "error";
|
||||
} else {
|
||||
this.flash = result.error;
|
||||
this.flashType = "error";
|
||||
|
@ -344,4 +355,21 @@ export default class Login extends Component {
|
|||
}
|
||||
this.args.model.showCreateAccount(createAccountProps);
|
||||
}
|
||||
|
||||
@action
|
||||
interceptResetLink(event) {
|
||||
if (
|
||||
!wantsNewWindow(event) &&
|
||||
event.target.href &&
|
||||
new URL(event.target.href).pathname === getURL("/password-reset")
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.modal.show(ForgotPassword, {
|
||||
model: {
|
||||
emailOrUsername: this.loginName,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -353,13 +353,7 @@ class SessionController < ApplicationController
|
|||
|
||||
# User's password has expired so they need to reset it
|
||||
if user.password_expired?(password)
|
||||
begin
|
||||
enqueue_password_reset_for_user(user)
|
||||
rescue RateLimiter::LimitExceeded
|
||||
# Just noop here as user would have already been sent the forgot password email more than once
|
||||
end
|
||||
|
||||
render json: { error: I18n.t("login.password_expired") }
|
||||
render json: { error: "expired", reason: "expired" }
|
||||
return
|
||||
end
|
||||
else
|
||||
|
|
|
@ -2291,6 +2291,7 @@ en:
|
|||
error: "Unknown error"
|
||||
cookies_error: "Your browser seems to have cookies disabled. You might not be able to log in without enabling them first."
|
||||
rate_limit: "Please wait before trying to log in again."
|
||||
password_expired: "Password expired. Please <a href='%{reset_url}'>reset your password</a>."
|
||||
blank_username: "Please enter your email or username."
|
||||
blank_username_or_password: "Please enter your email or username, and password."
|
||||
reset_password: "Reset Password"
|
||||
|
|
|
@ -2899,7 +2899,6 @@ en:
|
|||
not_approved: "Your account hasn't been approved yet. You will be notified by email when you are ready to log in."
|
||||
incorrect_username_email_or_password: "Incorrect username, email or password"
|
||||
incorrect_password: "Incorrect password"
|
||||
password_expired: "Password expired. Reset instructions sent to your email."
|
||||
incorrect_password_or_passkey: "Incorrect password or passkey"
|
||||
wait_approval: "Thanks for signing up. We will notify you when your account has been approved."
|
||||
active: "Your account is activated and ready to use."
|
||||
|
|
|
@ -2045,53 +2045,14 @@ RSpec.describe SessionController do
|
|||
|
||||
use_redis_snapshotting
|
||||
|
||||
it "should return an error response code with the right error message and enqueues the password reset email" do
|
||||
expect_enqueued_with(
|
||||
job: :critical_user_email,
|
||||
args: {
|
||||
type: "forgot_password",
|
||||
user_id: user.id,
|
||||
},
|
||||
) do
|
||||
post "/session.json", params: { login: user.username, password: "myawesomepassword" }
|
||||
end
|
||||
it "should return an error response code with the right error message" do
|
||||
post "/session.json", params: { login: user.username, password: "myawesomepassword" }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["error"]).to eq(I18n.t("login.password_expired"))
|
||||
expect(response.parsed_body["error"]).to eq("expired")
|
||||
expect(response.parsed_body["reason"]).to eq("expired")
|
||||
expect(session[:current_user_id]).to eq(nil)
|
||||
end
|
||||
|
||||
it "should limit the number of forgot password emails sent a day to the user when logging in with an expired password" do
|
||||
SiteSetting.max_logins_per_ip_per_minute =
|
||||
described_class::FORGOT_PASSWORD_EMAIL_LIMIT_PER_DAY + 1
|
||||
|
||||
SiteSetting.max_logins_per_ip_per_hour =
|
||||
described_class::FORGOT_PASSWORD_EMAIL_LIMIT_PER_DAY + 1
|
||||
|
||||
described_class::FORGOT_PASSWORD_EMAIL_LIMIT_PER_DAY.times do
|
||||
expect_enqueued_with(
|
||||
job: :critical_user_email,
|
||||
args: {
|
||||
type: "forgot_password",
|
||||
user_id: user.id,
|
||||
},
|
||||
) do
|
||||
post "/session.json", params: { login: user.username, password: "myawesomepassword" }
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
expect_not_enqueued_with(
|
||||
job: :critical_user_email,
|
||||
args: {
|
||||
type: "forgot_password",
|
||||
user_id: user.id,
|
||||
},
|
||||
) do
|
||||
post "/session.json", params: { login: user.username, password: "myawesomepassword" }
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when a user has security key-only 2FA login" do
|
||||
|
|
|
@ -41,13 +41,23 @@ shared_examples "login scenarios" do
|
|||
it "displays the right message when user's email has been marked as expired" do
|
||||
password = "myawesomepassword"
|
||||
user.update!(password:)
|
||||
expired_user_password = Fabricate(:expired_user_password, user:, password:)
|
||||
Fabricate(:expired_user_password, user:, password:)
|
||||
|
||||
login_modal.open
|
||||
login_modal.fill(username: user.username, password:)
|
||||
login_modal.click_login
|
||||
|
||||
expect(login_modal).to have_content(I18n.t("login.password_expired"))
|
||||
expect(login_modal.find("#modal-alert")).to have_content(
|
||||
I18n.t("js.login.password_expired", reset_url: "/password-reset").gsub(/<.*?>/, ""),
|
||||
)
|
||||
login_modal.find("#modal-alert a").click
|
||||
find("button.forgot-password-reset").click
|
||||
|
||||
wait_for(timeout: 5) { ActionMailer::Base.deliveries.count != 0 }
|
||||
|
||||
mail = ActionMailer::Base.deliveries.last
|
||||
expect(mail.to).to contain_exactly(user.email)
|
||||
expect(mail.body).to match(%r{/u/password-reset/\S+})
|
||||
end
|
||||
|
||||
it "can reset password" do
|
||||
|
|
Loading…
Reference in New Issue
Block a user