This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/controllers/create-account.js
Sam Saffron a1d660d951
FEATURE: optional global invite_code for account registration
On some sites when bootstrapping communities it is helpful to bootstrap
with a "light weight" invite code.

Use the site setting `invite_code` to set a global invite code.

In this case the administrator can share the code with
a community which is very easy to remember and then anyone who has
that code can easily register accounts.

People without the invite code are not allowed account registration.

Global invite codes are less secure than indevidual codes, in that they
tend to leak in the community however in some cases when starting a brand
new community the security guarantees of invites are not needed.
2020-03-15 21:17:28 +11:00

325 lines
9.6 KiB
JavaScript

import { A } from "@ember/array";
import { isEmpty } from "@ember/utils";
import { notEmpty, or, not } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { setting } from "discourse/lib/computed";
import discourseComputed, {
observes,
on
} from "discourse-common/utils/decorators";
import { emailValid } from "discourse/lib/utilities";
import PasswordValidation from "discourse/mixins/password-validation";
import UsernameValidation from "discourse/mixins/username-validation";
import NameValidation from "discourse/mixins/name-validation";
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
import { userPath } from "discourse/lib/url";
import { findAll } from "discourse/models/login-method";
import EmberObject from "@ember/object";
import User from "discourse/models/user";
export default Controller.extend(
ModalFunctionality,
PasswordValidation,
UsernameValidation,
NameValidation,
UserFieldsValidation,
{
login: controller(),
complete: false,
accountChallenge: 0,
accountHoneypot: 0,
formSubmitted: false,
rejectedEmails: A(),
prefilledUsername: null,
userFields: null,
isDeveloper: false,
hasAuthOptions: notEmpty("authOptions"),
canCreateLocal: setting("enable_local_logins"),
showCreateForm: or("hasAuthOptions", "canCreateLocal"),
requireInviteCode: setting("require_invite_code"),
resetForm() {
// We wrap the fields in a structure so we can assign a value
this.setProperties({
accountName: "",
accountEmail: "",
accountUsername: "",
accountPassword: "",
authOptions: null,
complete: false,
formSubmitted: false,
rejectedEmails: [],
rejectedPasswords: [],
prefilledUsername: null,
isDeveloper: false
});
this._createUserFields();
},
@discourseComputed(
"passwordRequired",
"nameValidation.failed",
"emailValidation.failed",
"usernameValidation.failed",
"passwordValidation.failed",
"userFieldsValidation.failed",
"formSubmitted",
"inviteCode"
)
submitDisabled() {
if (this.formSubmitted) return true;
if (this.get("nameValidation.failed")) return true;
if (this.get("emailValidation.failed")) return true;
if (this.get("usernameValidation.failed") && this.usernameRequired)
return true;
if (this.get("passwordValidation.failed") && this.passwordRequired)
return true;
if (this.get("userFieldsValidation.failed")) return true;
if (this.requireInviteCode && !this.inviteCode) return true;
return false;
},
usernameRequired: not("authOptions.omit_username"),
@discourseComputed
fullnameRequired() {
return (
this.get("siteSettings.full_name_required") ||
this.get("siteSettings.enable_names")
);
},
@discourseComputed("authOptions.auth_provider")
passwordRequired(authProvider) {
return isEmpty(authProvider);
},
@discourseComputed
disclaimerHtml() {
return I18n.t("create_account.disclaimer", {
tos_link: this.get("siteSettings.tos_url") || Discourse.getURL("/tos"),
privacy_link:
this.get("siteSettings.privacy_policy_url") ||
Discourse.getURL("/privacy")
});
},
// Check the email address
@discourseComputed("accountEmail", "rejectedEmails.[]")
emailValidation(email, rejectedEmails) {
// If blank, fail without a reason
if (isEmpty(email)) {
return EmberObject.create({
failed: true
});
}
if (rejectedEmails.includes(email)) {
return EmberObject.create({
failed: true,
reason: I18n.t("user.email.invalid")
});
}
if (
this.get("authOptions.email") === email &&
this.get("authOptions.email_valid")
) {
return EmberObject.create({
ok: true,
reason: I18n.t("user.email.authenticated", {
provider: this.authProviderDisplayName(
this.get("authOptions.auth_provider")
)
})
});
}
if (emailValid(email)) {
return EmberObject.create({
ok: true,
reason: I18n.t("user.email.ok")
});
}
return EmberObject.create({
failed: true,
reason: I18n.t("user.email.invalid")
});
},
@discourseComputed(
"accountEmail",
"authOptions.email",
"authOptions.email_valid"
)
emailValidated() {
return (
this.get("authOptions.email") === this.accountEmail &&
this.get("authOptions.email_valid")
);
},
authProviderDisplayName(providerName) {
const matchingProvider = findAll().find(provider => {
return provider.name === providerName;
});
return matchingProvider
? matchingProvider.get("prettyName")
: providerName;
},
@observes("emailValidation", "accountEmail")
prefillUsername: function() {
if (this.prefilledUsername) {
// If username field has been filled automatically, and email field just changed,
// then remove the username.
if (this.accountUsername === this.prefilledUsername) {
this.set("accountUsername", "");
}
this.set("prefilledUsername", null);
}
if (
this.get("emailValidation.ok") &&
(isEmpty(this.accountUsername) || this.get("authOptions.email"))
) {
// 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();
}
},
// Determines whether at least one login button is enabled
@discourseComputed
hasAtLeastOneLoginButton() {
return findAll().length > 0;
},
@on("init")
fetchConfirmationValue() {
return ajax(userPath("hp.json")).then(json => {
this._challengeDate = new Date();
// remove 30 seconds for jitter, make sure this works for at least
// 30 seconds so we don't have hard loops
this._challengeExpiry = parseInt(json.expires_in, 10) - 30;
if (this._challengeExpiry < 30) {
this._challengeExpiry = 30;
}
this.setProperties({
accountHoneypot: json.value,
accountChallenge: json.challenge
.split("")
.reverse()
.join("")
});
});
},
performAccountCreation() {
const attrs = this.getProperties(
"accountName",
"accountEmail",
"accountPassword",
"accountUsername",
"accountChallenge",
"inviteCode"
);
attrs["accountPasswordConfirm"] = this.accountHoneypot;
const userFields = this.userFields;
const destinationUrl = this.get("authOptions.destination_url");
if (!isEmpty(destinationUrl)) {
$.cookie("destination_url", destinationUrl, { path: "/" });
}
// Add the userfields to the data
if (!isEmpty(userFields)) {
attrs.userFields = {};
userFields.forEach(
f => (attrs.userFields[f.get("field.id")] = f.get("value"))
);
}
this.set("formSubmitted", true);
return User.createAccount(attrs).then(
result => {
this.set("isDeveloper", false);
if (result.success) {
// invalidate honeypot
this._challengeExpiry = 1;
// Trigger the browser's password manager using the hidden static login form:
const $hidden_login_form = $("#hidden-login-form");
$hidden_login_form
.find("input[name=username]")
.val(attrs.accountUsername);
$hidden_login_form
.find("input[name=password]")
.val(attrs.accountPassword);
$hidden_login_form
.find("input[name=redirect]")
.val(userPath("account-created"));
$hidden_login_form.submit();
} else {
this.flash(
result.message || I18n.t("create_account.failed"),
"error"
);
if (result.is_developer) {
this.set("isDeveloper", true);
}
if (
result.errors &&
result.errors.email &&
result.errors.email.length > 0 &&
result.values
) {
this.rejectedEmails.pushObject(result.values.email);
}
if (
result.errors &&
result.errors.password &&
result.errors.password.length > 0
) {
this.rejectedPasswords.pushObject(attrs.accountPassword);
}
this.set("formSubmitted", false);
$.removeCookie("destination_url");
}
},
() => {
this.set("formSubmitted", false);
$.removeCookie("destination_url");
return this.flash(I18n.t("create_account.failed"), "error");
}
);
},
actions: {
externalLogin(provider) {
this.login.send("externalLogin", provider);
},
createAccount() {
if (new Date() - this._challengeDate > 1000 * this._challengeExpiry) {
this.fetchConfirmationValue().then(() =>
this.performAccountCreation()
);
} else {
this.performAccountCreation();
}
}
}
}
);