FEATURE: Check email availability in signup form (#12328)
* FEATURE: Check email availability on focus out * FIX: Properly debounce username availability
This commit is contained in:
parent
4fb2d397a4
commit
ec7415ff49
|
@ -5,7 +5,7 @@ import discourseComputed, {
|
|||
on,
|
||||
} from "discourse-common/utils/decorators";
|
||||
import { A } from "@ember/array";
|
||||
import EmberObject from "@ember/object";
|
||||
import EmberObject, { action } from "@ember/object";
|
||||
import I18n from "I18n";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import NameValidation from "discourse/mixins/name-validation";
|
||||
|
@ -17,6 +17,7 @@ import UsernameValidation from "discourse/mixins/username-validation";
|
|||
import { ajax } from "discourse/lib/ajax";
|
||||
import { emailValid } from "discourse/lib/utilities";
|
||||
import { findAll } from "discourse/models/login-method";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { notEmpty } from "@ember/object/computed";
|
||||
|
@ -58,6 +59,8 @@ export default Controller.extend(
|
|||
accountEmail: "",
|
||||
accountUsername: "",
|
||||
accountPassword: "",
|
||||
serverAccountEmail: null,
|
||||
serverEmailValidation: null,
|
||||
authOptions: null,
|
||||
complete: false,
|
||||
formSubmitted: false,
|
||||
|
@ -130,13 +133,27 @@ export default Controller.extend(
|
|||
},
|
||||
|
||||
// Check the email address
|
||||
@discourseComputed("accountEmail", "rejectedEmails.[]")
|
||||
emailValidation(email, rejectedEmails) {
|
||||
@discourseComputed(
|
||||
"serverAccountEmail",
|
||||
"serverEmailValidation",
|
||||
"accountEmail",
|
||||
"rejectedEmails.[]"
|
||||
)
|
||||
emailValidation(
|
||||
serverAccountEmail,
|
||||
serverEmailValidation,
|
||||
email,
|
||||
rejectedEmails
|
||||
) {
|
||||
const failedAttrs = {
|
||||
failed: true,
|
||||
element: document.querySelector("#new-account-email"),
|
||||
};
|
||||
|
||||
if (serverAccountEmail === email && serverEmailValidation) {
|
||||
return serverEmailValidation;
|
||||
}
|
||||
|
||||
// If blank, fail without a reason
|
||||
if (isEmpty(email)) {
|
||||
return EmberObject.create(
|
||||
|
@ -146,7 +163,7 @@ export default Controller.extend(
|
|||
);
|
||||
}
|
||||
|
||||
if (rejectedEmails.includes(email)) {
|
||||
if (rejectedEmails.includes(email) || !emailValid(email)) {
|
||||
return EmberObject.create(
|
||||
Object.assign(failedAttrs, {
|
||||
reason: I18n.t("user.email.invalid"),
|
||||
|
@ -168,18 +185,48 @@ export default Controller.extend(
|
|||
});
|
||||
}
|
||||
|
||||
if (emailValid(email)) {
|
||||
return EmberObject.create({
|
||||
ok: true,
|
||||
reason: I18n.t("user.email.ok"),
|
||||
});
|
||||
return EmberObject.create({
|
||||
ok: true,
|
||||
reason: I18n.t("user.email.ok"),
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
checkEmailAvailability() {
|
||||
if (
|
||||
!this.emailValidation.ok ||
|
||||
this.serverAccountEmail === this.accountEmail
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return EmberObject.create(
|
||||
Object.assign(failedAttrs, {
|
||||
reason: I18n.t("user.email.invalid"),
|
||||
return User.checkEmail(this.accountEmail)
|
||||
.then((result) => {
|
||||
if (result.failed) {
|
||||
this.setProperties({
|
||||
serverAccountEmail: this.accountEmail,
|
||||
serverEmailValidation: EmberObject.create({
|
||||
failed: true,
|
||||
element: document.querySelector("#new-account-email"),
|
||||
reason: result.errors[0],
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
this.setProperties({
|
||||
serverAccountEmail: this.accountEmail,
|
||||
serverEmailValidation: EmberObject.create({
|
||||
ok: true,
|
||||
reason: I18n.t("user.email.ok"),
|
||||
}),
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
.catch(() => {
|
||||
this.setProperties({
|
||||
serverAccountEmail: null,
|
||||
serverEmailValidation: null,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
|
@ -220,7 +267,7 @@ export default Controller.extend(
|
|||
// If email is valid and username has not been entered yet,
|
||||
// or email and username were filled automatically by 3rd parth auth,
|
||||
// then look for a registered username that matches the email.
|
||||
this.fetchExistingUsername();
|
||||
discourseDebounce(this, this.fetchExistingUsername, 500);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -29,24 +29,18 @@ export default Mixin.create({
|
|||
minUsernameLength: setting("min_username_length"),
|
||||
|
||||
fetchExistingUsername() {
|
||||
discourseDebounce(
|
||||
this,
|
||||
function () {
|
||||
User.checkUsername(null, this.accountEmail).then((result) => {
|
||||
if (
|
||||
result.suggestion &&
|
||||
(isEmpty(this.accountUsername) ||
|
||||
this.accountUsername === this.get("authOptions.username"))
|
||||
) {
|
||||
this.setProperties({
|
||||
accountUsername: result.suggestion,
|
||||
prefilledUsername: result.suggestion,
|
||||
});
|
||||
}
|
||||
User.checkUsername(null, this.accountEmail).then((result) => {
|
||||
if (
|
||||
result.suggestion &&
|
||||
(isEmpty(this.accountUsername) ||
|
||||
this.accountUsername === this.get("authOptions.username"))
|
||||
) {
|
||||
this.setProperties({
|
||||
accountUsername: result.suggestion,
|
||||
prefilledUsername: result.suggestion,
|
||||
});
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@observes("accountUsername")
|
||||
|
@ -55,7 +49,7 @@ export default Mixin.create({
|
|||
|
||||
let result = this.basicUsernameValidation(accountUsername);
|
||||
if (result.shouldCheck) {
|
||||
this.checkUsernameAvailability();
|
||||
discourseDebounce(this, this.checkUsernameAvailability, 500);
|
||||
}
|
||||
this.set("usernameValidation", result);
|
||||
},
|
||||
|
@ -84,43 +78,37 @@ export default Mixin.create({
|
|||
},
|
||||
|
||||
checkUsernameAvailability() {
|
||||
discourseDebounce(
|
||||
this,
|
||||
function () {
|
||||
return User.checkUsername(this.accountUsername, this.accountEmail).then(
|
||||
(result) => {
|
||||
this.set("isDeveloper", false);
|
||||
if (result.available) {
|
||||
if (result.is_developer) {
|
||||
this.set("isDeveloper", true);
|
||||
}
|
||||
return this.set(
|
||||
"usernameValidation",
|
||||
validResult({ reason: I18n.t("user.username.available") })
|
||||
);
|
||||
} else {
|
||||
if (result.suggestion) {
|
||||
return this.set(
|
||||
"usernameValidation",
|
||||
failedResult({
|
||||
reason: I18n.t("user.username.not_available", result),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return this.set(
|
||||
"usernameValidation",
|
||||
failedResult({
|
||||
reason: result.errors
|
||||
? result.errors.join(" ")
|
||||
: I18n.t("user.username.not_available_no_suggestion"),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
return User.checkUsername(this.accountUsername, this.accountEmail).then(
|
||||
(result) => {
|
||||
this.set("isDeveloper", false);
|
||||
if (result.available) {
|
||||
if (result.is_developer) {
|
||||
this.set("isDeveloper", true);
|
||||
}
|
||||
);
|
||||
},
|
||||
500
|
||||
return this.set(
|
||||
"usernameValidation",
|
||||
validResult({ reason: I18n.t("user.username.available") })
|
||||
);
|
||||
} else {
|
||||
if (result.suggestion) {
|
||||
return this.set(
|
||||
"usernameValidation",
|
||||
failedResult({
|
||||
reason: I18n.t("user.username.not_available", result),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return this.set(
|
||||
"usernameValidation",
|
||||
failedResult({
|
||||
reason: result.errors
|
||||
? result.errors.join(" ")
|
||||
: I18n.t("user.username.not_available_no_suggestion"),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1044,6 +1044,10 @@ User.reopenClass(Singleton, {
|
|||
});
|
||||
},
|
||||
|
||||
checkEmail(email) {
|
||||
return ajax(userPath("check_email"), { data: { email } });
|
||||
},
|
||||
|
||||
groupStats(stats) {
|
||||
const responses = UserActionStat.create({
|
||||
count: 0,
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
{{#if emailValidated}}
|
||||
<span class="value">{{accountEmail}}</span>
|
||||
{{else}}
|
||||
{{input type="email" value=accountEmail id="new-account-email" name="email" class=(value-entered accountEmail) autofocus="autofocus"}}
|
||||
{{input type="email" value=accountEmail id="new-account-email" name="email" class=(value-entered accountEmail) autofocus="autofocus" focusOut=(action "checkEmailAvailability")}}
|
||||
<label class="alt-placeholder" for="new-account-email">
|
||||
{{i18n "user.email.title"}}
|
||||
{{~#if userFields~}}
|
||||
|
|
|
@ -449,6 +449,10 @@ export function applyDefaultHandlers(pretender) {
|
|||
return response({ available: true });
|
||||
});
|
||||
|
||||
pretender.get("/u/check_email", function () {
|
||||
return response({ success: "OK" });
|
||||
});
|
||||
|
||||
pretender.post("/u", () => response({ success: true }));
|
||||
|
||||
pretender.get("/login.html", () => [200, {}, "LOGIN PAGE"]);
|
||||
|
|
|
@ -34,6 +34,7 @@ class UsersController < ApplicationController
|
|||
# once that happens you can't log in with social
|
||||
skip_before_action :verify_authenticity_token, only: [:create]
|
||||
skip_before_action :redirect_to_login_if_required, only: [:check_username,
|
||||
:check_email,
|
||||
:create,
|
||||
:account_created,
|
||||
:activate_account,
|
||||
|
@ -522,6 +523,24 @@ class UsersController < ApplicationController
|
|||
render json: checker.check_username(username, email)
|
||||
end
|
||||
|
||||
def check_email
|
||||
RateLimiter.new(nil, "check-email-#{request.remote_ip}", 10, 1.minute).performed!
|
||||
|
||||
if SiteSetting.hide_email_address_taken?
|
||||
return render json: success_json
|
||||
end
|
||||
|
||||
user_email = UserEmail.new(email: params[:email])
|
||||
|
||||
if user_email.valid?
|
||||
render json: success_json
|
||||
else
|
||||
render json: failed_json.merge(errors: user_email.errors.full_messages)
|
||||
end
|
||||
rescue RateLimiter::LimitExceeded
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
def user_from_params_or_current_user
|
||||
params[:for_user_id] ? User.find(params[:for_user_id]) : current_user
|
||||
end
|
||||
|
|
|
@ -388,6 +388,7 @@ Discourse::Application.routes.draw do
|
|||
resources :users, except: [:index, :new, :show, :update, :destroy], path: root_path do
|
||||
collection do
|
||||
get "check_username"
|
||||
get "check_email"
|
||||
get "is_local_username"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1584,6 +1584,30 @@ describe UsersController do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#check_email' do
|
||||
it 'returns success if hide_email_address_taken is true' do
|
||||
SiteSetting.hide_email_address_taken = true
|
||||
|
||||
get "/u/check_email.json", params: { email: user.email }
|
||||
expect(response.parsed_body["success"]).to be_present
|
||||
end
|
||||
|
||||
it 'returns failure if email is not valid' do
|
||||
get "/u/check_email.json", params: { email: "invalid" }
|
||||
expect(response.parsed_body["failed"]).to be_present
|
||||
end
|
||||
|
||||
it 'returns failure if email exists' do
|
||||
get "/u/check_email.json", params: { email: user.email }
|
||||
expect(response.parsed_body["failed"]).to be_present
|
||||
end
|
||||
|
||||
it 'returns success if email does not exists' do
|
||||
get "/u/check_email.json", params: { email: "available@example.com" }
|
||||
expect(response.parsed_body["success"]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe '#invited' do
|
||||
it 'fails for anonymous users' do
|
||||
user = Fabricate(:user)
|
||||
|
|
Loading…
Reference in New Issue