FEATURE: Allow invites redemption with Omniauth providers.

This commit is contained in:
Alan Guo Xiang Tan 2021-03-02 15:13:04 +08:00
parent ebe4896e48
commit ce04db8610
21 changed files with 621 additions and 139 deletions

View File

@ -1,5 +1,5 @@
import { alias, notEmpty, or, readOnly } from "@ember/object/computed";
import Controller from "@ember/controller";
import Controller, { inject as controller } from "@ember/controller";
import DiscourseURL from "discourse/lib/url";
import EmberObject from "@ember/object";
import I18n from "I18n";
@ -14,6 +14,7 @@ import { emailValid } from "discourse/lib/utilities";
import { findAll as findLoginMethods } from "discourse/models/login-method";
import getUrl from "discourse-common/lib/get-url";
import { isEmpty } from "@ember/utils";
import { wavingHandURL } from "discourse/lib/waving-hand-url";
export default Controller.extend(
PasswordValidation,
@ -21,6 +22,8 @@ export default Controller.extend(
NameValidation,
UserFieldsValidation,
{
createAccount: controller(),
invitedBy: readOnly("model.invited_by"),
email: alias("model.email"),
accountUsername: alias("model.username"),
@ -28,6 +31,7 @@ export default Controller.extend(
successMessage: null,
errorMessage: null,
userFields: null,
authOptions: null,
inviteImageUrl: getUrl("/images/envelope.svg"),
isInviteLink: readOnly("model.is_invite_link"),
submitDisabled: or(
@ -45,6 +49,20 @@ export default Controller.extend(
this.rejectedEmails = [];
},
authenticationComplete(options) {
const props = {
accountUsername: options.username,
accountName: options.name,
authOptions: EmberObject.create(options),
};
if (this.isInviteLink) {
props.email = options.email;
}
this.setProperties(props);
},
@discourseComputed
welcomeTitle() {
return I18n.t("invites.welcome_to", {
@ -62,6 +80,25 @@ export default Controller.extend(
return findLoginMethods().length > 0;
},
@discourseComputed
externalAuthsOnly() {
return (
!this.siteSettings.enable_local_logins && this.externalAuthsEnabled
);
},
@discourseComputed(
"externalAuthsOnly",
"authOptions",
"emailValidation.failed"
)
shouldDisplayForm(externalAuthsOnly, authOptions, emailValidationFailed) {
return (
this.siteSettings.enable_local_logins ||
(externalAuthsOnly && authOptions && !emailValidationFailed)
);
},
@discourseComputed
fullnameRequired() {
return (
@ -69,8 +106,18 @@ export default Controller.extend(
);
},
@discourseComputed("email", "rejectedEmails.[]")
emailValidation(email, rejectedEmails) {
@discourseComputed(
"email",
"rejectedEmails.[]",
"authOptions.email",
"authOptions.email_valid"
)
emailValidation(
email,
rejectedEmails,
externalAuthEmail,
externalAuthEmailValid
) {
// If blank, fail without a reason
if (isEmpty(email)) {
return EmberObject.create({
@ -85,6 +132,28 @@ export default Controller.extend(
});
}
if (externalAuthEmail) {
const provider = this.createAccount.authProviderDisplayName(
this.get("authOptions.auth_provider")
);
if (externalAuthEmail === email && externalAuthEmailValid) {
return EmberObject.create({
ok: true,
reason: I18n.t("user.email.authenticated", {
provider,
}),
});
} else {
return EmberObject.create({
failed: true,
reason: I18n.t("user.email.invite_auth_email_invalid", {
provider,
}),
});
}
}
if (emailValid(email)) {
return EmberObject.create({
ok: true,
@ -98,6 +167,9 @@ export default Controller.extend(
});
},
@discourseComputed
wavingHandURL: () => wavingHandURL(),
actions: {
submit() {
const userFields = this.userFields;
@ -158,6 +230,12 @@ export default Controller.extend(
this.set("errorMessage", extractError(error));
});
},
externalLogin(provider) {
provider.doLogin({
origin: window.location.href,
});
},
},
}
);

View File

@ -13,10 +13,14 @@ export default {
if (lastAuthResult) {
const router = container.lookup("router:main");
router.one("didTransition", () => {
const controllerName =
router.currentPath === "invites.show" ? "invites-show" : "login";
next(() => {
let loginController = container.lookup("controller:login");
loginController.authenticationComplete(JSON.parse(lastAuthResult));
let controller = container.lookup(`controller:${controllerName}`);
controller.authenticationComplete(JSON.parse(lastAuthResult));
});
});
}

View File

@ -1,5 +1,9 @@
<div class="container invites-show clearfix">
<h2>{{welcomeTitle}}</h2>
<div class="login-welcome-header">
<h1 class="login-title">{{welcomeTitle}}</h1>
<img src={{wavingHandURL}} alt="" class="waving-hand">
<p class="login-subheader">{{i18n "create_account.subheader_title"}}</p>
</div>
<div class="two-col">
<div class="col-image">
@ -19,18 +23,28 @@
{{#unless isInviteLink}}
<p>
{{html-safe yourEmailMessage}}
{{#if externalAuthsEnabled}}
{{#unless externalAuthsOnly}}
{{i18n "invites.social_login_available"}}
{{/if}}
{{/unless}}
</p>
{{/unless}}
<form>
{{#if externalAuthsOnly}}
{{#if authOptions}}
{{#unless isInviteLink}}
{{input-tip validation=emailValidation id="account-email-validation"}}
{{/unless}}
{{else}}
{{login-buttons externalLogin=(action "externalLogin")}}
{{/if}}
{{/if}}
{{#if shouldDisplayForm}}
<form>
{{#if isInviteLink}}
<div class="input email-input">
<label>{{i18n "user.email.title"}}</label>
{{input type="email" value=email id="new-account-email" name="email" autofocus="autofocus"}}
{{input type="email" value=email id="new-account-email" name="email" autofocus="autofocus" disabled=externalAuthsOnly}}
{{input-tip validation=emailValidation id="account-email-validation"}}
<div class="instructions">{{i18n "user.email.instructions"}}</div>
</div>
@ -51,6 +65,7 @@
</div>
{{/if}}
{{#unless externalAuthsOnly}}
<div class="input password-input">
<label>{{i18n "invites.password_label"}}</label>
{{password-field value=accountPassword type="password" id="new-account-password" capsLockOn=capsLockOn}}
@ -62,6 +77,7 @@
</div>
</div>
</div>
{{/unless}}
{{#if userFields}}
<div class="user-fields">
@ -85,6 +101,7 @@
{{/if}}
</form>
{{/if}}
{{/if}}
</div>
</div>
</div>

View File

@ -5,12 +5,39 @@ import {
} from "discourse/tests/helpers/qunit-helpers";
import { fillIn, visit } from "@ember/test-helpers";
import PreloadStore from "discourse/lib/preload-store";
import I18n from "I18n";
import { test } from "qunit";
acceptance("Invite Accept", function (needs) {
acceptance("Invite accept", function (needs) {
needs.settings({ full_name_required: true });
test("Invite Acceptance Page", async function (assert) {
test("email invite link", async function (assert) {
PreloadStore.store("invite_info", {
invited_by: {
id: 123,
username: "foobar",
avatar_template: "/user_avatar/localhost/neil/{size}/25_1.png",
name: "foobar",
title: "team",
},
email: "foobar@example.com",
username: "invited",
is_invite_link: false,
});
await visit("/invites/myvalidinvitetoken");
assert.ok(
queryAll(".col-form")
.text()
.includes(I18n.t("invites.social_login_available")),
"shows social login hint"
);
assert.ok(!exists("#new-account-email"), "hides the email input");
});
test("invite link", async function (assert) {
PreloadStore.store("invite_info", {
invited_by: {
id: 123,
@ -84,3 +111,175 @@ acceptance("Invite Accept", function (needs) {
);
});
});
acceptance("Invite accept when local login is disabled", function (needs) {
needs.settings({ enable_local_logins: false });
const preloadStore = function (isInviteLink) {
const info = {
invited_by: {
id: 123,
username: "foobar",
avatar_template: "/user_avatar/localhost/neil/{size}/25_1.png",
name: "foobar",
title: "team",
},
username: "invited",
};
if (isInviteLink) {
info.email = "null";
info.is_invite_link = true;
} else {
info.email = "foobar@example.com";
info.is_invite_link = false;
}
PreloadStore.store("invite_info", info);
};
test("invite link", async function (assert) {
preloadStore(true);
await visit("/invites/myvalidinvitetoken");
assert.ok(exists(".btn-social.facebook"), "shows Facebook login button");
assert.ok(!exists("form"), "does not display the form");
});
test("invite link with authentication data", async function (assert) {
preloadStore(true);
// Simulate authticated with Facebook
const node = document.createElement("meta");
node.dataset.authenticationData = JSON.stringify({
auth_provider: "facebook",
email: "blah@example.com",
email_valid: true,
username: "foobar",
name: "barfoo",
});
node.id = "data-authentication";
document.querySelector("head").appendChild(node);
await visit("/invites/myvalidinvitetoken");
assert.ok(
!exists(".btn-social.facebook"),
"does not show Facebook login button"
);
assert.ok(!exists("#new-account-password"), "does not show password field");
assert.ok(
exists("#new-account-email[disabled]"),
"email field is disabled"
);
assert.equal(
queryAll("#account-email-validation").text().trim(),
I18n.t("user.email.authenticated", { provider: "Facebook" })
);
assert.equal(
queryAll("#new-account-username").val(),
"foobar",
"username is prefilled"
);
assert.equal(
queryAll("#new-account-name").val(),
"barfoo",
"name is prefilled"
);
document
.querySelector("head")
.removeChild(document.getElementById("data-authentication"));
});
test("email invite link", async function (assert) {
preloadStore(false);
await visit("/invites/myvalidinvitetoken");
assert.ok(exists(".btn-social.facebook"), "shows Facebook login button");
assert.ok(!exists("form"), "does not display the form");
});
test("email invite link with authentication data when email does not match", async function (assert) {
preloadStore(false);
// Simulate authticated with Facebook
const node = document.createElement("meta");
node.dataset.authenticationData = JSON.stringify({
auth_provider: "facebook",
email: "blah@example.com",
email_valid: true,
username: "foobar",
name: "barfoo",
});
node.id = "data-authentication";
document.querySelector("head").appendChild(node);
await visit("/invites/myvalidinvitetoken");
assert.equal(
queryAll("#account-email-validation").text().trim(),
I18n.t("user.email.invite_auth_email_invalid", { provider: "Facebook" })
);
assert.ok(!exists("form"), "does not display the form");
document
.querySelector("head")
.removeChild(document.getElementById("data-authentication"));
});
test("email invite link with authentication data", async function (assert) {
preloadStore(false);
// Simulate authticated with Facebook
const node = document.createElement("meta");
node.dataset.authenticationData = JSON.stringify({
auth_provider: "facebook",
email: "foobar@example.com",
email_valid: true,
username: "foobar",
name: "barfoo",
});
node.id = "data-authentication";
document.querySelector("head").appendChild(node);
await visit("/invites/myvalidinvitetoken");
assert.ok(
!exists(".btn-social.facebook"),
"does not show Facebook login button"
);
assert.ok(!exists("#new-account-password"), "does not show password field");
assert.ok(!exists("#new-account-email"), "does not show email field");
assert.equal(
queryAll("#account-email-validation").text().trim(),
I18n.t("user.email.authenticated", { provider: "Facebook" })
);
assert.equal(
queryAll("#new-account-username").val(),
"foobar",
"username is prefilled"
);
assert.equal(
queryAll("#new-account-name").val(),
"barfoo",
"name is prefilled"
);
document
.querySelector("head")
.removeChild(document.getElementById("data-authentication"));
});
});

View File

@ -33,7 +33,8 @@
// Create Account + Login
.d-modal.create-account,
.d-modal.login-modal {
.d-modal.login-modal,
.invites-show {
.modal-inner-container {
position: relative;
}
@ -267,6 +268,14 @@
.two-col {
position: relative;
display: flex;
margin-top: 5px;
}
#login-buttons {
.btn {
background-color: var(--primary-low);
color: var(--primary);
}
}
.col-image {

View File

@ -8,6 +8,7 @@ class InvitesController < ApplicationController
skip_before_action :preload_json, except: [:show]
skip_before_action :redirect_to_login_if_required
before_action :ensure_invites_allowed, only: [:show, :perform_accept_invitation]
before_action :ensure_new_registrations_allowed, only: [:show, :perform_accept_invitation]
before_action :ensure_not_logged_in, only: [:show, :perform_accept_invitation]
@ -168,7 +169,8 @@ class InvitesController < ApplicationController
name: params[:name],
password: params[:password],
user_custom_fields: params[:user_custom_fields],
ip_address: request.remote_ip
ip_address: request.remote_ip,
session: session
}
attrs[:email] =
@ -284,6 +286,12 @@ class InvitesController < ApplicationController
private
def ensure_invites_allowed
if SiteSetting.enable_discourse_connect || (!SiteSetting.enable_local_logins && Discourse.enabled_auth_providers.count == 0)
raise Discourse::NotFound
end
end
def ensure_new_registrations_allowed
unless SiteSetting.allow_new_registrations
flash[:error] = I18n.t('login.new_registrations_disabled')

View File

@ -119,7 +119,12 @@ class Users::OmniauthCallbacksController < ApplicationController
end
def invite_required?
SiteSetting.invite_only?
if SiteSetting.invite_only?
path = Discourse.route_for(@origin)
return true unless path
return true if path[:controller] != "invites" && path[:action] != "show"
!Invite.exists?(invite_key: path[:id])
end
end
def user_found(user)

View File

@ -447,8 +447,6 @@ class UsersController < ApplicationController
elsif current_user&.staff?
message = if SiteSetting.enable_discourse_connect
I18n.t("invite.disabled_errors.discourse_connect_enabled")
elsif !SiteSetting.enable_local_logins
I18n.t("invite.disabled_errors.local_logins_disabled")
end
render_invite_error(message)

View File

@ -162,11 +162,20 @@ class Invite < ActiveRecord::Base
invite.reload
end
def redeem(email: nil, username: nil, name: nil, password: nil, user_custom_fields: nil, ip_address: nil)
def redeem(email: nil, username: nil, name: nil, password: nil, user_custom_fields: nil, ip_address: nil, session: nil)
if !expired? && !destroyed? && link_valid?
raise UserExists.new I18n.t("invite_link.email_taken") if is_invite_link? && UserEmail.exists?(email: email)
email = self.email if email.blank? && !is_invite_link?
InviteRedeemer.new(invite: self, email: email, username: username, name: name, password: password, user_custom_fields: user_custom_fields, ip_address: ip_address).redeem
InviteRedeemer.new(
invite: self,
email: email,
username: username,
name: name,
password: password,
user_custom_fields: user_custom_fields,
ip_address: ip_address,
session: session
).redeem
end
end
@ -251,8 +260,6 @@ class Invite < ActiveRecord::Base
if SiteSetting.enable_discourse_connect?
errors.add(:email, I18n.t("invite.disabled_errors.discourse_connect_enabled"))
elsif !SiteSetting.enable_local_logins?
errors.add(:email, I18n.t("invite.disabled_errors.local_logins_disabled"))
end
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
InviteRedeemer = Struct.new(:invite, :email, :username, :name, :password, :user_custom_fields, :ip_address, keyword_init: true) do
InviteRedeemer = Struct.new(:invite, :email, :username, :name, :password, :user_custom_fields, :ip_address, :session, keyword_init: true) do
def redeem
Invite.transaction do
@ -14,7 +14,7 @@ InviteRedeemer = Struct.new(:invite, :email, :username, :name, :password, :user_
end
# extracted from User cause it is very specific to invites
def self.create_user_from_invite(email:, invite:, username: nil, name: nil, password: nil, user_custom_fields: nil, ip_address: nil)
def self.create_user_from_invite(email:, invite:, username: nil, name: nil, password: nil, user_custom_fields: nil, ip_address: nil, session: nil)
user = User.where(staged: true).with_email(email.strip.downcase).first
user.unstage! if user
@ -61,7 +61,20 @@ InviteRedeemer = Struct.new(:invite, :email, :username, :name, :password, :user_
user.password_required!
end
authenticator = UserAuthenticator.new(user, session, require_password: false)
if !authenticator.has_authenticator? && !SiteSetting.enable_local_logins
raise ActiveRecord::RecordNotSaved.new(I18n.t("login.incorrect_username_email_or_password"))
end
authenticator.start
if authenticator.email_valid? && !authenticator.authenticated?
raise ActiveRecord::RecordNotSaved.new(I18n.t("login.incorrect_username_email_or_password"))
end
user.save!
authenticator.finish
if invite.emailed_status != Invite.emailed_status_types[:not_required] && email == invite.email
user.email_tokens.create!(email: user.email)
@ -110,7 +123,16 @@ InviteRedeemer = Struct.new(:invite, :email, :username, :name, :password, :user_
def get_invited_user
result = get_existing_user
result ||= InviteRedeemer.create_user_from_invite(email: email, invite: invite, username: username, name: name, password: password, user_custom_fields: user_custom_fields, ip_address: ip_address)
result ||= InviteRedeemer.create_user_from_invite(
email: email,
invite: invite,
username: username,
name: name,
password: password,
user_custom_fields: user_custom_fields,
ip_address: ip_address,
session: session
)
result.send_welcome_message = false
result
end

View File

@ -2,20 +2,21 @@
class UserAuthenticator
def initialize(user, session, authenticator_finder = Users::OmniauthCallbacksController)
def initialize(user, session, authenticator_finder: Users::OmniauthCallbacksController, require_password: true)
@user = user
@session = session
if session[:authentication] && session[:authentication].is_a?(Hash)
if session&.dig(:authentication) && session[:authentication].is_a?(Hash)
@auth_result = Auth::Result.from_session_data(session[:authentication], user: user)
end
@authenticator_finder = authenticator_finder
@require_password = require_password
end
def start
if authenticated?
@user.active = true
@auth_result.apply_user_attributes!
else
elsif @require_password
@user.password_required!
end
@ -31,7 +32,7 @@ class UserAuthenticator
authenticator.after_create_account(@user, @auth_result)
confirm_email
end
@session[:authentication] = @auth_result = nil if @session[:authentication]
@session[:authentication] = @auth_result = nil if @session&.dig(:authentication)
end
def email_valid?

View File

@ -1293,6 +1293,7 @@ en:
required: "Please enter an email address"
invalid: "Please enter a valid email address"
authenticated: "Your email has been authenticated by %{provider}"
invite_auth_email_invalid: "Your invitation email does not match the email authenticated by %{provider}"
frequency_immediately: "We'll email you immediately if you haven't read the thing we're emailing you about."
frequency:
one: "We'll only email you if we haven't seen you in the last minute."

View File

@ -241,7 +241,6 @@ en:
cant_invite_to_group: "You are not allowed to invite users to specified group(s). Make sure you are owner of the group(s) you are trying to invite to."
disabled_errors:
discourse_connect_enabled: "Invites are disabled because DiscourseConnect is enabled."
local_logins_disabled: "Invites are disabled because the 'enable local logins' setting is disabled."
invalid_access: "You are not permitted to view the requested resource."
bulk_invite:
@ -1681,7 +1680,7 @@ en:
discourse_connect_not_approved_url: "Redirect unapproved DiscourseConnect accounts to this URL"
discourse_connect_allows_all_return_paths: "Do not restrict the domain for return_paths provided by DiscourseConnect (by default return path must be on current site)"
enable_local_logins: "Enable local username and password login based accounts. This must be enabled for invites to work. WARNING: if disabled, you may be unable to log in if you have not previously configured at least one alternate login method."
enable_local_logins: "Enable local username and password login based accounts. WARNING: if disabled, you may be unable to log in if you have not previously configured at least one alternate login method."
enable_local_logins_via_email: "Allow users to request a one-click login link to be sent to them via email."
allow_new_registrations: "Allow new user registrations. Uncheck this to prevent anyone from creating a new account."
enable_signup_cta: "Show a notice to returning anonymous users prompting them to sign up for an account."

View File

@ -354,7 +354,6 @@ class Guardian
authenticated? &&
(SiteSetting.max_invites_per_day.to_i > 0 || is_staff?) &&
!SiteSetting.enable_discourse_connect &&
SiteSetting.enable_local_logins &&
(
(!SiteSetting.must_approve_users? && @user.has_trust_level?(SiteSetting.min_trust_level_to_allow_invite.to_i)) ||
is_staff?
@ -395,9 +394,7 @@ class Guardian
end
def can_bulk_invite_to_forum?(user)
user.admin? &&
!SiteSetting.enable_discourse_connect &&
SiteSetting.enable_local_logins
user.admin? && !SiteSetting.enable_discourse_connect
end
def can_resend_all_invites?(user)

View File

@ -511,8 +511,10 @@ describe Guardian do
expect(Guardian.new(user).can_invite_to_forum?).to be_falsey
end
it 'returns false when the local logins are disabled' do
SiteSetting.enable_local_logins = false
it 'returns false when DiscourseConnect is enabled' do
SiteSetting.discourse_connect_url = "https://www.example.com/sso"
SiteSetting.enable_discourse_connect = true
expect(Guardian.new(user).can_invite_to_forum?).to be_falsey
expect(Guardian.new(moderator).can_invite_to_forum?).to be_falsey
end

View File

@ -374,6 +374,97 @@ describe InvitesController do
expect(invite.redeemed?).to be_truthy
end
it 'returns the right response when local login is disabled and no external auth is configured' do
SiteSetting.enable_local_logins = false
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(404)
end
it 'returns the right response when DiscourseConnect is enabled' do
invite
SiteSetting.discourse_connect_url = "https://www.example.com/sso"
SiteSetting.enable_discourse_connect = true
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(404)
end
describe 'with authentication session' do
let(:authenticated_email) { "foobar@example.com" }
before do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(
provider: 'google_oauth2',
uid: '12345',
info: OmniAuth::AuthHash::InfoHash.new(
email: authenticated_email,
name: 'First Last'
),
extra: {
raw_info: OmniAuth::AuthHash.new(
email_verified: true,
email: authenticated_email,
family_name: "Last",
given_name: "First",
gender: "male",
name: "First Last",
)
},
)
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2]
SiteSetting.enable_google_oauth2_logins = true
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
end
after do
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2] = nil
OmniAuth.config.test_mode = false
end
it 'should associate the invited user with authenticator records' do
invite.update!(email: authenticated_email)
SiteSetting.auth_overrides_name = true
expect do
put "/invites/show/#{invite.invite_key}.json",
params: { name: 'somename' }
expect(response.status).to eq(200)
end.to change { User.with_email(authenticated_email).exists? }.to(true)
user = User.find_by_email(authenticated_email)
expect(user.name).to eq('First Last')
expect(user.user_associated_accounts.first.provider_name)
.to eq("google_oauth2")
end
it 'returns the right response even if local logins has been disabled' do
SiteSetting.enable_local_logins = false
invite.update!(email: authenticated_email)
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
end
it 'returns the right response if authenticated email does not match invite email' do
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(412)
end
end
context 'when redeem returns a user' do
fab!(:user) { Fabricate(:coding_horror) }
@ -447,27 +538,6 @@ describe InvitesController do
expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(1)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
end
it "does not send password reset email if sso is enabled" do
invite # create the invite before enabling SSO
SiteSetting.discourse_connect_url = "https://www.example.com/sso"
SiteSetting.enable_discourse_connect = true
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
end
it "does not send password reset email if local login is disabled" do
invite # create the invite before enabling SSO
SiteSetting.enable_local_logins = false
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
end
end
context "with password" do

View File

@ -12,6 +12,7 @@ RSpec.describe Users::OmniauthCallbacksController do
after do
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2] = nil
Rails.application.env_config["omniauth.origin"] = nil
OmniAuth.config.test_mode = false
end
@ -221,6 +222,48 @@ RSpec.describe Users::OmniauthCallbacksController do
data = JSON.parse(cookies[:authentication_data])
expect(data["destination_url"]).to eq(destination_url)
end
describe 'when site is invite_only' do
before do
SiteSetting.invite_only = true
end
it 'should return the right response without any origin' do
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
data = JSON.parse(response.cookies["authentication_data"])
expect(data["requires_invite"]).to eq(true)
end
it 'returns the right response for an invalid origin' do
Rails.application.env_config["omniauth.origin"] = "/invitesinvites"
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
end
it 'should return the right response when origin is invites page' do
origin = Rails.application.routes.url_helpers.invite_url(
Fabricate(:invite).invite_key,
host: Discourse.base_url
)
Rails.application.env_config["omniauth.origin"] = origin
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
expect(response).to redirect_to(origin)
data = JSON.parse(response.cookies["authentication_data"])
expect(data["requires_invite"]).to eq(nil)
end
end
end
describe 'when user has been verified' do

View File

@ -1755,10 +1755,15 @@ describe UsersController do
expect(response.status).to eq(403)
end
end
end
context 'when DiscourseConnect has been enabled' do
before do
SiteSetting.discourse_connect_url = "https://www.example.com/sso"
SiteSetting.enable_discourse_connect = true
end
context 'when local logins are disabled' do
it 'explains why invites are disabled to staff users' do
SiteSetting.enable_local_logins = false
inviter = sign_in(Fabricate(:admin))
Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required])
@ -1766,11 +1771,10 @@ describe UsersController do
expect(response.status).to eq(200)
expect(response.parsed_body['error']).to include(I18n.t(
'invite.disabled_errors.local_logins_disabled'
'invite.disabled_errors.discourse_connect_enabled'
))
end
end
end
context 'with redeemed invites' do
it 'returns invites' do

View File

@ -2,7 +2,8 @@
require 'rails_helper'
def github_auth(email_valid)
describe UserAuthenticator do
def github_auth(email_valid)
{
email: "user53@discourse.org",
username: "joedoe546",
@ -16,16 +17,33 @@ def github_auth(email_valid)
},
skip_email_validation: false
}
end
describe UserAuthenticator do
context "#finish" do
fab!(:group) { Fabricate(:group, automatic_membership_email_domains: "discourse.org") }
end
before do
SiteSetting.enable_github_logins = true
end
describe "#start" do
describe 'without authentication session' do
it "should apply the right user attributes" do
user = User.new
UserAuthenticator.new(user, {}).start
expect(user.password_required?).to eq(true)
end
it "allows password requirement to be skipped" do
user = User.new
UserAuthenticator.new(user, {}, require_password: false).start
expect(user.password_required?).to eq(false)
end
end
end
context "#finish" do
fab!(:group) { Fabricate(:group, automatic_membership_email_domains: "discourse.org") }
it "confirms email and adds the user to appropraite groups based on email" do
user = Fabricate(:user, email: "user53@discourse.org")
expect(group.usernames).not_to include(user.username)