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,
|
on,
|
||||||
} from "discourse-common/utils/decorators";
|
} from "discourse-common/utils/decorators";
|
||||||
import { A } from "@ember/array";
|
import { A } from "@ember/array";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject, { action } from "@ember/object";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||||
import NameValidation from "discourse/mixins/name-validation";
|
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 { ajax } from "discourse/lib/ajax";
|
||||||
import { emailValid } from "discourse/lib/utilities";
|
import { emailValid } from "discourse/lib/utilities";
|
||||||
import { findAll } from "discourse/models/login-method";
|
import { findAll } from "discourse/models/login-method";
|
||||||
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import { isEmpty } from "@ember/utils";
|
import { isEmpty } from "@ember/utils";
|
||||||
import { notEmpty } from "@ember/object/computed";
|
import { notEmpty } from "@ember/object/computed";
|
||||||
|
@ -58,6 +59,8 @@ export default Controller.extend(
|
||||||
accountEmail: "",
|
accountEmail: "",
|
||||||
accountUsername: "",
|
accountUsername: "",
|
||||||
accountPassword: "",
|
accountPassword: "",
|
||||||
|
serverAccountEmail: null,
|
||||||
|
serverEmailValidation: null,
|
||||||
authOptions: null,
|
authOptions: null,
|
||||||
complete: false,
|
complete: false,
|
||||||
formSubmitted: false,
|
formSubmitted: false,
|
||||||
|
@ -130,13 +133,27 @@ export default Controller.extend(
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check the email address
|
// Check the email address
|
||||||
@discourseComputed("accountEmail", "rejectedEmails.[]")
|
@discourseComputed(
|
||||||
emailValidation(email, rejectedEmails) {
|
"serverAccountEmail",
|
||||||
|
"serverEmailValidation",
|
||||||
|
"accountEmail",
|
||||||
|
"rejectedEmails.[]"
|
||||||
|
)
|
||||||
|
emailValidation(
|
||||||
|
serverAccountEmail,
|
||||||
|
serverEmailValidation,
|
||||||
|
email,
|
||||||
|
rejectedEmails
|
||||||
|
) {
|
||||||
const failedAttrs = {
|
const failedAttrs = {
|
||||||
failed: true,
|
failed: true,
|
||||||
element: document.querySelector("#new-account-email"),
|
element: document.querySelector("#new-account-email"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (serverAccountEmail === email && serverEmailValidation) {
|
||||||
|
return serverEmailValidation;
|
||||||
|
}
|
||||||
|
|
||||||
// If blank, fail without a reason
|
// If blank, fail without a reason
|
||||||
if (isEmpty(email)) {
|
if (isEmpty(email)) {
|
||||||
return EmberObject.create(
|
return EmberObject.create(
|
||||||
|
@ -146,7 +163,7 @@ export default Controller.extend(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rejectedEmails.includes(email)) {
|
if (rejectedEmails.includes(email) || !emailValid(email)) {
|
||||||
return EmberObject.create(
|
return EmberObject.create(
|
||||||
Object.assign(failedAttrs, {
|
Object.assign(failedAttrs, {
|
||||||
reason: I18n.t("user.email.invalid"),
|
reason: I18n.t("user.email.invalid"),
|
||||||
|
@ -168,18 +185,48 @@ export default Controller.extend(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emailValid(email)) {
|
return EmberObject.create({
|
||||||
return EmberObject.create({
|
ok: true,
|
||||||
ok: true,
|
reason: I18n.t("user.email.ok"),
|
||||||
reason: I18n.t("user.email.ok"),
|
});
|
||||||
});
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
checkEmailAvailability() {
|
||||||
|
if (
|
||||||
|
!this.emailValidation.ok ||
|
||||||
|
this.serverAccountEmail === this.accountEmail
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return EmberObject.create(
|
return User.checkEmail(this.accountEmail)
|
||||||
Object.assign(failedAttrs, {
|
.then((result) => {
|
||||||
reason: I18n.t("user.email.invalid"),
|
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(
|
@discourseComputed(
|
||||||
|
@ -220,7 +267,7 @@ export default Controller.extend(
|
||||||
// If email is valid and username has not been entered yet,
|
// If email is valid and username has not been entered yet,
|
||||||
// or email and username were filled automatically by 3rd parth auth,
|
// or email and username were filled automatically by 3rd parth auth,
|
||||||
// then look for a registered username that matches the email.
|
// 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"),
|
minUsernameLength: setting("min_username_length"),
|
||||||
|
|
||||||
fetchExistingUsername() {
|
fetchExistingUsername() {
|
||||||
discourseDebounce(
|
User.checkUsername(null, this.accountEmail).then((result) => {
|
||||||
this,
|
if (
|
||||||
function () {
|
result.suggestion &&
|
||||||
User.checkUsername(null, this.accountEmail).then((result) => {
|
(isEmpty(this.accountUsername) ||
|
||||||
if (
|
this.accountUsername === this.get("authOptions.username"))
|
||||||
result.suggestion &&
|
) {
|
||||||
(isEmpty(this.accountUsername) ||
|
this.setProperties({
|
||||||
this.accountUsername === this.get("authOptions.username"))
|
accountUsername: result.suggestion,
|
||||||
) {
|
prefilledUsername: result.suggestion,
|
||||||
this.setProperties({
|
|
||||||
accountUsername: result.suggestion,
|
|
||||||
prefilledUsername: result.suggestion,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
500
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@observes("accountUsername")
|
@observes("accountUsername")
|
||||||
|
@ -55,7 +49,7 @@ export default Mixin.create({
|
||||||
|
|
||||||
let result = this.basicUsernameValidation(accountUsername);
|
let result = this.basicUsernameValidation(accountUsername);
|
||||||
if (result.shouldCheck) {
|
if (result.shouldCheck) {
|
||||||
this.checkUsernameAvailability();
|
discourseDebounce(this, this.checkUsernameAvailability, 500);
|
||||||
}
|
}
|
||||||
this.set("usernameValidation", result);
|
this.set("usernameValidation", result);
|
||||||
},
|
},
|
||||||
|
@ -84,43 +78,37 @@ export default Mixin.create({
|
||||||
},
|
},
|
||||||
|
|
||||||
checkUsernameAvailability() {
|
checkUsernameAvailability() {
|
||||||
discourseDebounce(
|
return User.checkUsername(this.accountUsername, this.accountEmail).then(
|
||||||
this,
|
(result) => {
|
||||||
function () {
|
this.set("isDeveloper", false);
|
||||||
return User.checkUsername(this.accountUsername, this.accountEmail).then(
|
if (result.available) {
|
||||||
(result) => {
|
if (result.is_developer) {
|
||||||
this.set("isDeveloper", false);
|
this.set("isDeveloper", true);
|
||||||
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 this.set(
|
||||||
},
|
"usernameValidation",
|
||||||
500
|
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) {
|
groupStats(stats) {
|
||||||
const responses = UserActionStat.create({
|
const responses = UserActionStat.create({
|
||||||
count: 0,
|
count: 0,
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
{{#if emailValidated}}
|
{{#if emailValidated}}
|
||||||
<span class="value">{{accountEmail}}</span>
|
<span class="value">{{accountEmail}}</span>
|
||||||
{{else}}
|
{{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">
|
<label class="alt-placeholder" for="new-account-email">
|
||||||
{{i18n "user.email.title"}}
|
{{i18n "user.email.title"}}
|
||||||
{{~#if userFields~}}
|
{{~#if userFields~}}
|
||||||
|
|
|
@ -449,6 +449,10 @@ export function applyDefaultHandlers(pretender) {
|
||||||
return response({ available: true });
|
return response({ available: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pretender.get("/u/check_email", function () {
|
||||||
|
return response({ success: "OK" });
|
||||||
|
});
|
||||||
|
|
||||||
pretender.post("/u", () => response({ success: true }));
|
pretender.post("/u", () => response({ success: true }));
|
||||||
|
|
||||||
pretender.get("/login.html", () => [200, {}, "LOGIN PAGE"]);
|
pretender.get("/login.html", () => [200, {}, "LOGIN PAGE"]);
|
||||||
|
|
|
@ -34,6 +34,7 @@ class UsersController < ApplicationController
|
||||||
# once that happens you can't log in with social
|
# once that happens you can't log in with social
|
||||||
skip_before_action :verify_authenticity_token, only: [:create]
|
skip_before_action :verify_authenticity_token, only: [:create]
|
||||||
skip_before_action :redirect_to_login_if_required, only: [:check_username,
|
skip_before_action :redirect_to_login_if_required, only: [:check_username,
|
||||||
|
:check_email,
|
||||||
:create,
|
:create,
|
||||||
:account_created,
|
:account_created,
|
||||||
:activate_account,
|
:activate_account,
|
||||||
|
@ -522,6 +523,24 @@ class UsersController < ApplicationController
|
||||||
render json: checker.check_username(username, email)
|
render json: checker.check_username(username, email)
|
||||||
end
|
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
|
def user_from_params_or_current_user
|
||||||
params[:for_user_id] ? User.find(params[:for_user_id]) : current_user
|
params[:for_user_id] ? User.find(params[:for_user_id]) : current_user
|
||||||
end
|
end
|
||||||
|
|
|
@ -388,6 +388,7 @@ Discourse::Application.routes.draw do
|
||||||
resources :users, except: [:index, :new, :show, :update, :destroy], path: root_path do
|
resources :users, except: [:index, :new, :show, :update, :destroy], path: root_path do
|
||||||
collection do
|
collection do
|
||||||
get "check_username"
|
get "check_username"
|
||||||
|
get "check_email"
|
||||||
get "is_local_username"
|
get "is_local_username"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1584,6 +1584,30 @@ describe UsersController do
|
||||||
end
|
end
|
||||||
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
|
describe '#invited' do
|
||||||
it 'fails for anonymous users' do
|
it 'fails for anonymous users' do
|
||||||
user = Fabricate(:user)
|
user = Fabricate(:user)
|
||||||
|
|
Loading…
Reference in New Issue