mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 06:04:11 +08:00
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.
This commit is contained in:
parent
a14313e9d0
commit
a1d660d951
|
@ -40,6 +40,7 @@ export default Controller.extend(
|
||||||
hasAuthOptions: notEmpty("authOptions"),
|
hasAuthOptions: notEmpty("authOptions"),
|
||||||
canCreateLocal: setting("enable_local_logins"),
|
canCreateLocal: setting("enable_local_logins"),
|
||||||
showCreateForm: or("hasAuthOptions", "canCreateLocal"),
|
showCreateForm: or("hasAuthOptions", "canCreateLocal"),
|
||||||
|
requireInviteCode: setting("require_invite_code"),
|
||||||
|
|
||||||
resetForm() {
|
resetForm() {
|
||||||
// We wrap the fields in a structure so we can assign a value
|
// We wrap the fields in a structure so we can assign a value
|
||||||
|
@ -66,7 +67,8 @@ export default Controller.extend(
|
||||||
"usernameValidation.failed",
|
"usernameValidation.failed",
|
||||||
"passwordValidation.failed",
|
"passwordValidation.failed",
|
||||||
"userFieldsValidation.failed",
|
"userFieldsValidation.failed",
|
||||||
"formSubmitted"
|
"formSubmitted",
|
||||||
|
"inviteCode"
|
||||||
)
|
)
|
||||||
submitDisabled() {
|
submitDisabled() {
|
||||||
if (this.formSubmitted) return true;
|
if (this.formSubmitted) return true;
|
||||||
|
@ -78,6 +80,8 @@ export default Controller.extend(
|
||||||
return true;
|
return true;
|
||||||
if (this.get("userFieldsValidation.failed")) return true;
|
if (this.get("userFieldsValidation.failed")) return true;
|
||||||
|
|
||||||
|
if (this.requireInviteCode && !this.inviteCode) return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -225,7 +229,8 @@ export default Controller.extend(
|
||||||
"accountEmail",
|
"accountEmail",
|
||||||
"accountPassword",
|
"accountPassword",
|
||||||
"accountUsername",
|
"accountUsername",
|
||||||
"accountChallenge"
|
"accountChallenge",
|
||||||
|
"inviteCode"
|
||||||
);
|
);
|
||||||
|
|
||||||
attrs["accountPasswordConfirm"] = this.accountHoneypot;
|
attrs["accountPasswordConfirm"] = this.accountHoneypot;
|
||||||
|
|
|
@ -909,17 +909,23 @@ User.reopenClass(Singleton, {
|
||||||
},
|
},
|
||||||
|
|
||||||
createAccount(attrs) {
|
createAccount(attrs) {
|
||||||
|
let data = {
|
||||||
|
name: attrs.accountName,
|
||||||
|
email: attrs.accountEmail,
|
||||||
|
password: attrs.accountPassword,
|
||||||
|
username: attrs.accountUsername,
|
||||||
|
password_confirmation: attrs.accountPasswordConfirm,
|
||||||
|
challenge: attrs.accountChallenge,
|
||||||
|
user_fields: attrs.userFields,
|
||||||
|
timezone: moment.tz.guess()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (attrs.inviteCode) {
|
||||||
|
data.invite_code = attrs.inviteCode;
|
||||||
|
}
|
||||||
|
|
||||||
return ajax(userPath(), {
|
return ajax(userPath(), {
|
||||||
data: {
|
data,
|
||||||
name: attrs.accountName,
|
|
||||||
email: attrs.accountEmail,
|
|
||||||
password: attrs.accountPassword,
|
|
||||||
username: attrs.accountUsername,
|
|
||||||
password_confirmation: attrs.accountPasswordConfirm,
|
|
||||||
challenge: attrs.accountChallenge,
|
|
||||||
user_fields: attrs.userFields,
|
|
||||||
timezone: moment.tz.guess()
|
|
||||||
},
|
|
||||||
type: "POST"
|
type: "POST"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,17 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
{{#if requireInviteCode }}
|
||||||
|
<tr class="invite-code">
|
||||||
|
<td><label for='invite-code'>{{i18n 'user.invite_code.title'}}</label></td>
|
||||||
|
<td>
|
||||||
|
{{input value=inviteCode id="inviteCode"}}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td><label>{{i18n 'user.invite_code.instructions'}}</label></td>
|
||||||
|
</tr>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{plugin-outlet name="create-account-after-password"
|
{{plugin-outlet name="create-account-after-password"
|
||||||
noTags=true
|
noTags=true
|
||||||
args=(hash accountName=accountName
|
args=(hash accountName=accountName
|
||||||
|
|
|
@ -410,6 +410,7 @@ class UsersController < ApplicationController
|
||||||
def create
|
def create
|
||||||
params.require(:email)
|
params.require(:email)
|
||||||
params.require(:username)
|
params.require(:username)
|
||||||
|
params.require(:invite_code) if SiteSetting.require_invite_code
|
||||||
params.permit(:user_fields)
|
params.permit(:user_fields)
|
||||||
|
|
||||||
unless SiteSetting.allow_new_registrations
|
unless SiteSetting.allow_new_registrations
|
||||||
|
@ -424,6 +425,10 @@ class UsersController < ApplicationController
|
||||||
return fail_with("login.email_too_long")
|
return fail_with("login.email_too_long")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if SiteSetting.require_invite_code && SiteSetting.invite_code != params[:invite_code]
|
||||||
|
return fail_with("login.wrong_invite_code")
|
||||||
|
end
|
||||||
|
|
||||||
if clashing_with_existing_route?(params[:username]) || User.reserved_username?(params[:username])
|
if clashing_with_existing_route?(params[:username]) || User.reserved_username?(params[:username])
|
||||||
return fail_with("login.reserved_username")
|
return fail_with("login.reserved_username")
|
||||||
end
|
end
|
||||||
|
|
|
@ -173,6 +173,11 @@ class SiteSetting < ActiveRecord::Base
|
||||||
SiteSetting::Upload
|
SiteSetting::Upload
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.require_invite_code
|
||||||
|
invite_code.present?
|
||||||
|
end
|
||||||
|
client_settings << :require_invite_code
|
||||||
|
|
||||||
%i{
|
%i{
|
||||||
site_logo_url
|
site_logo_url
|
||||||
site_logo_small_url
|
site_logo_small_url
|
||||||
|
|
|
@ -1132,6 +1132,10 @@ en:
|
||||||
password_confirmation:
|
password_confirmation:
|
||||||
title: "Password Again"
|
title: "Password Again"
|
||||||
|
|
||||||
|
invite_code:
|
||||||
|
title: "Invite Code"
|
||||||
|
instructions: "Account registration requires an invite code"
|
||||||
|
|
||||||
auth_tokens:
|
auth_tokens:
|
||||||
title: "Recently Used Devices"
|
title: "Recently Used Devices"
|
||||||
ip: "IP"
|
ip: "IP"
|
||||||
|
|
|
@ -1530,6 +1530,7 @@ en:
|
||||||
markdown_typographer_quotation_marks: "List of double and single quotes replacement pairs"
|
markdown_typographer_quotation_marks: "List of double and single quotes replacement pairs"
|
||||||
post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)."
|
post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)."
|
||||||
must_approve_users: "Staff must approve all new user accounts before they are allowed to access the site."
|
must_approve_users: "Staff must approve all new user accounts before they are allowed to access the site."
|
||||||
|
invite_code: "User must type this code to be allowed account registration, ignored when empty"
|
||||||
approve_suspect_users: "Add suspicious users to the review queue. Suspicious users have entered a bio/website but have no reading activity."
|
approve_suspect_users: "Add suspicious users to the review queue. Suspicious users have entered a bio/website but have no reading activity."
|
||||||
pending_users_reminder_delay: "Notify moderators if new users have been waiting for approval for longer than this many hours. Set to -1 to disable notifications."
|
pending_users_reminder_delay: "Notify moderators if new users have been waiting for approval for longer than this many hours. Set to -1 to disable notifications."
|
||||||
maximum_session_age: "User will remain logged in for n hours since last visit"
|
maximum_session_age: "User will remain logged in for n hours since last visit"
|
||||||
|
@ -2382,6 +2383,7 @@ en:
|
||||||
new_registrations_disabled: "New account registrations are not allowed at this time."
|
new_registrations_disabled: "New account registrations are not allowed at this time."
|
||||||
password_too_long: "Passwords are limited to 200 characters."
|
password_too_long: "Passwords are limited to 200 characters."
|
||||||
email_too_long: "The email you provided is too long. Mailbox names must be no more than 254 characters, and domain names must be no more than 253 characters."
|
email_too_long: "The email you provided is too long. Mailbox names must be no more than 254 characters, and domain names must be no more than 253 characters."
|
||||||
|
wrong_invite_code: "The invite code you entered was incorrect."
|
||||||
reserved_username: "That username is not allowed."
|
reserved_username: "That username is not allowed."
|
||||||
missing_user_field: "You have not completed all the user fields"
|
missing_user_field: "You have not completed all the user fields"
|
||||||
auth_complete: "Authentication is complete."
|
auth_complete: "Authentication is complete."
|
||||||
|
|
|
@ -336,6 +336,7 @@ login:
|
||||||
must_approve_users:
|
must_approve_users:
|
||||||
client: true
|
client: true
|
||||||
default: false
|
default: false
|
||||||
|
invite_code: ""
|
||||||
enable_local_logins:
|
enable_local_logins:
|
||||||
client: true
|
client: true
|
||||||
default: true
|
default: true
|
||||||
|
|
|
@ -593,8 +593,8 @@ describe UsersController do
|
||||||
email: @user.email }
|
email: @user.email }
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_user
|
def post_user(extra_params = {})
|
||||||
post "/u.json", params: post_user_params
|
post "/u.json", params: post_user_params.merge(extra_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when email params is missing' do
|
context 'when email params is missing' do
|
||||||
|
@ -616,17 +616,27 @@ describe UsersController do
|
||||||
expect(User.find_by(username: @user.username).locale).to eq('fr')
|
expect(User.find_by(username: @user.username).locale).to eq('fr')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'requires invite code when specified' do
|
||||||
|
expect(SiteSetting.require_invite_code).to eq(false)
|
||||||
|
SiteSetting.invite_code = "abc"
|
||||||
|
expect(SiteSetting.require_invite_code).to eq(true)
|
||||||
|
|
||||||
|
post_user(invite_code: "abcd")
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
expect(json["success"]).to eq(false)
|
||||||
|
|
||||||
|
post_user(invite_code: "abc")
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
expect(json["success"]).to eq(true)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
context "when timezone is provided as a guess on signup" do
|
context "when timezone is provided as a guess on signup" do
|
||||||
let(:post_user_params) do
|
|
||||||
{ name: @user.name,
|
|
||||||
username: @user.username,
|
|
||||||
password: "strongpassword",
|
|
||||||
email: @user.email,
|
|
||||||
timezone: "Australia/Brisbane" }
|
|
||||||
end
|
|
||||||
|
|
||||||
it "sets the timezone" do
|
it "sets the timezone" do
|
||||||
post_user
|
post_user(timezone: "Australia/Brisbane")
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
expect(User.find_by(username: @user.username).user_option.timezone).to eq("Australia/Brisbane")
|
expect(User.find_by(username: @user.username).user_option.timezone).to eq("Australia/Brisbane")
|
||||||
end
|
end
|
||||||
|
@ -1440,7 +1450,7 @@ describe UsersController do
|
||||||
inviter = Fabricate(:user, trust_level: 2)
|
inviter = Fabricate(:user, trust_level: 2)
|
||||||
sign_in(inviter)
|
sign_in(inviter)
|
||||||
invitee = Fabricate(:user)
|
invitee = Fabricate(:user)
|
||||||
invite = Fabricate(:invite, invited_by: inviter, user: invitee)
|
_invite = Fabricate(:invite, invited_by: inviter, user: invitee)
|
||||||
get "/u/#{user.username}/invited_count.json"
|
get "/u/#{user.username}/invited_count.json"
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user