mirror of
https://github.com/discourse/discourse.git
synced 2025-03-22 14:47:54 +08:00
FEATURE: External auth when redeeming invites
This feature (when enabled) will allow for invite_only sites to require external authentication before they can redeem an invite. - Created hidden site setting to toggle this - Enables sending invites with local logins disabled - OAuth button added to invite form - Requires OAuth email address to match invite email address - Prevents redeeming invite if OAuth authentication fails
This commit is contained in:
parent
3503758599
commit
87a0a6664e
@ -5,6 +5,7 @@ import { ajax } from "discourse/lib/ajax";
|
|||||||
import PasswordValidation from "discourse/mixins/password-validation";
|
import PasswordValidation from "discourse/mixins/password-validation";
|
||||||
import UsernameValidation from "discourse/mixins/username-validation";
|
import UsernameValidation from "discourse/mixins/username-validation";
|
||||||
import NameValidation from "discourse/mixins/name-validation";
|
import NameValidation from "discourse/mixins/name-validation";
|
||||||
|
import InviteEmailAuthValidation from "discourse/mixins/invite-email-auth-validation";
|
||||||
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
|
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
|
||||||
import { findAll as findLoginMethods } from "discourse/models/login-method";
|
import { findAll as findLoginMethods } from "discourse/models/login-method";
|
||||||
|
|
||||||
@ -12,8 +13,11 @@ export default Ember.Controller.extend(
|
|||||||
PasswordValidation,
|
PasswordValidation,
|
||||||
UsernameValidation,
|
UsernameValidation,
|
||||||
NameValidation,
|
NameValidation,
|
||||||
|
InviteEmailAuthValidation,
|
||||||
UserFieldsValidation,
|
UserFieldsValidation,
|
||||||
{
|
{
|
||||||
|
login: Ember.inject.controller(),
|
||||||
|
|
||||||
invitedBy: Ember.computed.alias("model.invited_by"),
|
invitedBy: Ember.computed.alias("model.invited_by"),
|
||||||
email: Ember.computed.alias("model.email"),
|
email: Ember.computed.alias("model.email"),
|
||||||
accountUsername: Ember.computed.alias("model.username"),
|
accountUsername: Ember.computed.alias("model.username"),
|
||||||
@ -22,6 +26,7 @@ export default Ember.Controller.extend(
|
|||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
userFields: null,
|
userFields: null,
|
||||||
inviteImageUrl: getUrl("/images/envelope.svg"),
|
inviteImageUrl: getUrl("/images/envelope.svg"),
|
||||||
|
hasAuthOptions: Ember.computed.notEmpty("authOptions"),
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
welcomeTitle() {
|
welcomeTitle() {
|
||||||
@ -35,24 +40,40 @@ export default Ember.Controller.extend(
|
|||||||
return I18n.t("invites.your_email", { email: email });
|
return I18n.t("invites.your_email", { email: email });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
authProviderDisplayName(providerName) {
|
||||||
|
const matchingProvider = findLoginMethods().find(provider => {
|
||||||
|
return provider.name === providerName;
|
||||||
|
});
|
||||||
|
return matchingProvider
|
||||||
|
? matchingProvider.get("prettyName")
|
||||||
|
: providerName;
|
||||||
|
},
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
externalAuthsEnabled() {
|
externalAuthsEnabled() {
|
||||||
return findLoginMethods().length > 0;
|
return findLoginMethods().length > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@computed
|
||||||
|
inviteOnlyOauthEnabled() {
|
||||||
|
return this.siteSettings.enable_invite_only_oauth;
|
||||||
|
},
|
||||||
|
|
||||||
@computed(
|
@computed(
|
||||||
"usernameValidation.failed",
|
"usernameValidation.failed",
|
||||||
"passwordValidation.failed",
|
"passwordValidation.failed",
|
||||||
"nameValidation.failed",
|
"nameValidation.failed",
|
||||||
"userFieldsValidation.failed"
|
"userFieldsValidation.failed",
|
||||||
|
"inviteEmailAuthValidation.failed",
|
||||||
)
|
)
|
||||||
submitDisabled(
|
submitDisabled(
|
||||||
usernameFailed,
|
usernameFailed,
|
||||||
passwordFailed,
|
passwordFailed,
|
||||||
nameFailed,
|
nameFailed,
|
||||||
userFieldsFailed
|
userFieldsFailed,
|
||||||
|
inviteEmailAuthFailed,
|
||||||
) {
|
) {
|
||||||
return usernameFailed || passwordFailed || nameFailed || userFieldsFailed;
|
return usernameFailed || passwordFailed || nameFailed || userFieldsFailed || inviteEmailAuthFailed;
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
@ -63,6 +84,10 @@ export default Ember.Controller.extend(
|
|||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
externalLogin(provider) {
|
||||||
|
this.login.send("externalLogin", provider);
|
||||||
|
},
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
const userFields = this.userFields;
|
const userFields = this.userFields;
|
||||||
let userCustomFields = {};
|
let userCustomFields = {};
|
||||||
|
@ -20,6 +20,7 @@ const AuthErrors = [
|
|||||||
|
|
||||||
export default Ember.Controller.extend(ModalFunctionality, {
|
export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
createAccount: Ember.inject.controller(),
|
createAccount: Ember.inject.controller(),
|
||||||
|
invitesShow: Ember.inject.controller(),
|
||||||
forgotPassword: Ember.inject.controller(),
|
forgotPassword: Ember.inject.controller(),
|
||||||
application: Ember.inject.controller(),
|
application: Ember.inject.controller(),
|
||||||
|
|
||||||
@ -353,14 +354,23 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createAccountController = this.createAccount;
|
if (this.siteSettings.enable_invite_only_oauth) {
|
||||||
createAccountController.setProperties({
|
const invitesShowController = this.invitesShow;
|
||||||
accountEmail: options.email,
|
invitesShowController.setProperties({
|
||||||
accountUsername: options.username,
|
accountEmail: options.email,
|
||||||
accountName: options.name,
|
accountUsername: options.username,
|
||||||
authOptions: Ember.Object.create(options)
|
accountName: options.name,
|
||||||
});
|
authOptions: Ember.Object.create(options)
|
||||||
|
});
|
||||||
showModal("createAccount");
|
} else {
|
||||||
|
const createAccountController = this.createAccount;
|
||||||
|
createAccountController.setProperties({
|
||||||
|
accountEmail: options.email,
|
||||||
|
accountUsername: options.username,
|
||||||
|
accountName: options.name,
|
||||||
|
authOptions: Ember.Object.create(options)
|
||||||
|
});
|
||||||
|
showModal("createAccount");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
import InputValidation from "discourse/models/input-validation";
|
||||||
|
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||||
|
|
||||||
|
export default Ember.Mixin.create({
|
||||||
|
@computed()
|
||||||
|
nameInstructions() {
|
||||||
|
"";
|
||||||
|
},
|
||||||
|
|
||||||
|
// Validate the name.
|
||||||
|
@computed("accountEmail", "authOptions.email", "authOptions.email_valid", "authOptions.auth_provider")
|
||||||
|
inviteEmailAuthValidation() {
|
||||||
|
if (
|
||||||
|
!this.siteSettings.enable_invite_only_oauth ||
|
||||||
|
(this.siteSettings.enable_invite_only_oauth &&
|
||||||
|
this.get("authOptions.email") === this.email &&
|
||||||
|
this.get("authOptions.email_valid"))
|
||||||
|
) {
|
||||||
|
return InputValidation.create({
|
||||||
|
ok: true,
|
||||||
|
reason: I18n.t("user.email.authenticated", {
|
||||||
|
provider: this.authProviderDisplayName(
|
||||||
|
this.get("authOptions.auth_provider")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return InputValidation.create({
|
||||||
|
failed: true,
|
||||||
|
reason: I18n.t("user.email.invite_email_auth_invalid", {
|
||||||
|
provider: this.authProviderDisplayName(
|
||||||
|
this.get("authOptions.auth_provider")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -14,55 +14,98 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<p>{{i18n 'invites.invited_by'}}</p>
|
<p>{{i18n 'invites.invited_by'}}</p>
|
||||||
<p>{{user-info user=invitedBy}}</p>
|
<p>{{user-info user=invitedBy}}</p>
|
||||||
|
<p>
|
||||||
<p>{{{yourEmailMessage}}}
|
{{{yourEmailMessage}}}
|
||||||
|
{{#if inviteOnlyOauthEnabled }}
|
||||||
|
{{login-buttons externalLogin=(action "externalLogin")}}
|
||||||
|
{{/if}}
|
||||||
{{#if externalAuthsEnabled}}
|
{{#if externalAuthsEnabled}}
|
||||||
{{i18n 'invites.social_login_available'}}
|
{{#unless inviteOnlyOauthEnabled}}
|
||||||
|
{{i18n 'invites.social_login_available'}}
|
||||||
|
{{/unless}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form>
|
{{#if hasAuthOptions}}
|
||||||
<div class="input username-input">
|
{{#if inviteOnlyOauthEnabled }}
|
||||||
<label>{{i18n 'user.username.title'}}</label>
|
{{input-tip validation=inviteEmailAuthValidation id="account-email-validation"}}
|
||||||
{{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="discourse"}}
|
|
||||||
{{input-tip validation=usernameValidation id="username-validation"}}
|
|
||||||
<div class="instructions">{{i18n 'user.username.instructions'}}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if fullnameRequired}}
|
|
||||||
<div class="input name-input">
|
|
||||||
<label>{{i18n 'invites.name_label'}}</label>
|
|
||||||
{{input value=accountName id="new-account-name" name="name"}}
|
|
||||||
<div class="instructions">{{nameInstructions}}</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
<form>
|
||||||
<div class="input password-input">
|
<div class="input username-input">
|
||||||
<label>{{i18n 'invites.password_label'}}</label>
|
<label>{{i18n 'user.username.title'}}</label>
|
||||||
{{password-field value=accountPassword type="password" id="new-account-password" capsLockOn=capsLockOn}}
|
{{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="discourse"}}
|
||||||
{{input-tip validation=passwordValidation}}
|
{{input-tip validation=usernameValidation id="username-validation"}}
|
||||||
<div class="instructions">
|
<div class="instructions">{{i18n 'user.username.instructions'}}</div>
|
||||||
{{passwordInstructions}} {{i18n 'invites.optional_description'}}
|
|
||||||
<div class="caps-lock-warning {{unless capsLockOn 'invisible'}}">
|
|
||||||
{{d-icon "exclamation-triangle"}} {{i18n 'login.caps_lock_warning'}}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if userFields}}
|
{{#if fullnameRequired}}
|
||||||
<div class='user-fields'>
|
<div class="input name-input">
|
||||||
{{#each userFields as |f|}}
|
<label>{{i18n 'invites.name_label'}}</label>
|
||||||
{{user-field field=f.field value=f.value}}
|
{{input value=accountName id="new-account-name" name="name"}}
|
||||||
{{/each}}
|
<div class="instructions">{{nameInstructions}}</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if userFields}}
|
||||||
|
<div class='user-fields'>
|
||||||
|
{{#each userFields as |f|}}
|
||||||
|
{{user-field field=f.field value=f.value}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<button class='btn btn-primary' {{action "submit"}} disabled={{submitDisabled}}>{{i18n 'invites.accept_invite'}}</button>
|
||||||
|
|
||||||
|
{{#if errorMessage}}
|
||||||
|
<br/><br/>
|
||||||
|
<div class='alert alert-error'>{{errorMessage}}</div>
|
||||||
|
{{/if}}
|
||||||
|
</form>
|
||||||
|
{{/if}}
|
||||||
|
{{#unless inviteOnlyOauthEnabled}}
|
||||||
|
<form>
|
||||||
|
<div class="input username-input">
|
||||||
|
<label>{{i18n 'user.username.title'}}</label>
|
||||||
|
{{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="discourse"}}
|
||||||
|
{{input-tip validation=usernameValidation id="username-validation"}}
|
||||||
|
<div class="instructions">{{i18n 'user.username.instructions'}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<button class='btn btn-primary' {{action "submit"}} disabled={{submitDisabled}}>{{i18n 'invites.accept_invite'}}</button>
|
{{#if fullnameRequired}}
|
||||||
|
<div class="input name-input">
|
||||||
|
<label>{{i18n 'invites.name_label'}}</label>
|
||||||
|
{{input value=accountName id="new-account-name" name="name"}}
|
||||||
|
<div class="instructions">{{nameInstructions}}</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if errorMessage}}
|
<div class="input password-input">
|
||||||
<br/><br/>
|
<label>{{i18n 'invites.password_label'}}</label>
|
||||||
<div class='alert alert-error'>{{errorMessage}}</div>
|
{{password-field value=accountPassword type="password" id="new-account-password" capsLockOn=capsLockOn}}
|
||||||
{{/if}}
|
{{input-tip validation=passwordValidation}}
|
||||||
</form>
|
<div class="instructions">
|
||||||
|
{{passwordInstructions}} {{i18n 'invites.optional_description'}}
|
||||||
|
<div class="caps-lock-warning {{unless capsLockOn 'invisible'}}">
|
||||||
|
{{d-icon "exclamation-triangle"}} {{i18n 'login.caps_lock_warning'}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if userFields}}
|
||||||
|
<div class='user-fields'>
|
||||||
|
{{#each userFields as |f|}}
|
||||||
|
{{user-field field=f.field value=f.value}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<button class='btn btn-primary' {{action "submit"}} disabled={{submitDisabled}}>{{i18n 'invites.accept_invite'}}</button>
|
||||||
|
|
||||||
|
{{#if errorMessage}}
|
||||||
|
<br/><br/>
|
||||||
|
<div class='alert alert-error'>{{errorMessage}}</div>
|
||||||
|
{{/if}}
|
||||||
|
</form>
|
||||||
|
{{/unless}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,7 +107,7 @@ class Users::OmniauthCallbacksController < ApplicationController
|
|||||||
def complete_response_data
|
def complete_response_data
|
||||||
if @auth_result.user
|
if @auth_result.user
|
||||||
user_found(@auth_result.user)
|
user_found(@auth_result.user)
|
||||||
elsif SiteSetting.invite_only?
|
elsif invite_required?
|
||||||
@auth_result.requires_invite = true
|
@auth_result.requires_invite = true
|
||||||
else
|
else
|
||||||
session[:authentication] = @auth_result.session_data
|
session[:authentication] = @auth_result.session_data
|
||||||
@ -155,4 +155,10 @@ class Users::OmniauthCallbacksController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# If invite_only and enable_invite_only_oauth allow the user to authenticate if coming from the invite page
|
||||||
|
def invite_required?
|
||||||
|
(SiteSetting.invite_only? && !SiteSetting.enable_invite_only_oauth) ||
|
||||||
|
(SiteSetting.invite_only? && (!@origin.include?('invites') && SiteSetting.enable_invite_only_oauth))
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1013,6 +1013,7 @@ en:
|
|||||||
ok: "We will email you to confirm"
|
ok: "We will email you to confirm"
|
||||||
invalid: "Please enter a valid email address"
|
invalid: "Please enter a valid email address"
|
||||||
authenticated: "Your email has been authenticated by {{provider}}"
|
authenticated: "Your email has been authenticated by {{provider}}"
|
||||||
|
invite_email_auth_invalid: "Your invitation email does not match the email from {{provider}}"
|
||||||
frequency_immediately: "We'll email you immediately if you haven't read the thing we're emailing you about."
|
frequency_immediately: "We'll email you immediately if you haven't read the thing we're emailing you about."
|
||||||
frequency:
|
frequency:
|
||||||
one: "We'll only email you if we haven't seen you in the last minute."
|
one: "We'll only email you if we haven't seen you in the last minute."
|
||||||
@ -1025,7 +1026,7 @@ en:
|
|||||||
cancel: "Cancel"
|
cancel: "Cancel"
|
||||||
not_connected: "(not connected)"
|
not_connected: "(not connected)"
|
||||||
confirm_modal_title: "Connect %{provider} Account"
|
confirm_modal_title: "Connect %{provider} Account"
|
||||||
confirm_description:
|
confirm_description:
|
||||||
account_specific: "Your %{provider} account '%{account_description}' will be used for authentication."
|
account_specific: "Your %{provider} account '%{account_description}' will be used for authentication."
|
||||||
generic: "Your %{provider} account will be used for authentication."
|
generic: "Your %{provider} account will be used for authentication."
|
||||||
|
|
||||||
|
@ -327,6 +327,10 @@ login:
|
|||||||
enable_local_logins:
|
enable_local_logins:
|
||||||
client: true
|
client: true
|
||||||
default: true
|
default: true
|
||||||
|
enable_invite_only_oauth:
|
||||||
|
client: true
|
||||||
|
default: false
|
||||||
|
hidden: true
|
||||||
enable_local_logins_via_email:
|
enable_local_logins_via_email:
|
||||||
client: true
|
client: true
|
||||||
default: true
|
default: true
|
||||||
|
@ -314,7 +314,7 @@ class Guardian
|
|||||||
authenticated? &&
|
authenticated? &&
|
||||||
(SiteSetting.max_invites_per_day.to_i > 0 || is_staff?) &&
|
(SiteSetting.max_invites_per_day.to_i > 0 || is_staff?) &&
|
||||||
!SiteSetting.enable_sso &&
|
!SiteSetting.enable_sso &&
|
||||||
SiteSetting.enable_local_logins &&
|
(SiteSetting.enable_invite_only_oauth || SiteSetting.enable_local_logins) &&
|
||||||
(
|
(
|
||||||
(!SiteSetting.must_approve_users? && @user.has_trust_level?(TrustLevel[2])) ||
|
(!SiteSetting.must_approve_users? && @user.has_trust_level?(TrustLevel[2])) ||
|
||||||
is_staff?
|
is_staff?
|
||||||
|
Loading…
x
Reference in New Issue
Block a user