FIX: Make UI match server behavior for external-auth invites (#13113)

There are two methods which the server uses to verify an invite is being redeemed with a matching email:
  1) The email token, supplied via a `?t=` parameter
  2) The validity of the email, as provided by the auth provider

Only one of these needs to be true for the invite to be redeemed successfully on the server. The frontend logic was previously only checking (2). This commit updates the frontend logic to match the server.

This commit does not affect the invite redemption logic. It only affects the 'show' endpoint, and the UI.
This commit is contained in:
David Taylor 2021-05-26 09:47:44 +01:00 committed by GitHub
parent d0dfd0c73f
commit f25eda13fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 96 additions and 6 deletions

View File

@ -29,6 +29,7 @@ export default Controller.extend(
invitedBy: readOnly("model.invited_by"), invitedBy: readOnly("model.invited_by"),
email: alias("model.email"), email: alias("model.email"),
hiddenEmail: alias("model.hidden_email"), hiddenEmail: alias("model.hidden_email"),
emailVerifiedByLink: alias("model.email_verified_by_link"),
accountUsername: alias("model.username"), accountUsername: alias("model.username"),
passwordRequired: notEmpty("accountPassword"), passwordRequired: notEmpty("accountPassword"),
successMessage: null, successMessage: null,
@ -127,14 +128,16 @@ export default Controller.extend(
"rejectedEmails.[]", "rejectedEmails.[]",
"authOptions.email", "authOptions.email",
"authOptions.email_valid", "authOptions.email_valid",
"hiddenEmail" "hiddenEmail",
"emailVerifiedByLink"
) )
emailValidation( emailValidation(
email, email,
rejectedEmails, rejectedEmails,
externalAuthEmail, externalAuthEmail,
externalAuthEmailValid, externalAuthEmailValid,
hiddenEmail hiddenEmail,
emailVerifiedByLink
) { ) {
if (hiddenEmail) { if (hiddenEmail) {
return EmberObject.create({ return EmberObject.create({
@ -157,12 +160,12 @@ export default Controller.extend(
}); });
} }
if (externalAuthEmail) { if (externalAuthEmail && externalAuthEmailValid) {
const provider = this.createAccount.authProviderDisplayName( const provider = this.createAccount.authProviderDisplayName(
this.get("authOptions.auth_provider") this.get("authOptions.auth_provider")
); );
if (externalAuthEmail === email && externalAuthEmailValid) { if (externalAuthEmail === email) {
return EmberObject.create({ return EmberObject.create({
ok: true, ok: true,
reason: I18n.t("user.email.authenticated", { reason: I18n.t("user.email.authenticated", {
@ -179,6 +182,13 @@ export default Controller.extend(
} }
} }
if (emailVerifiedByLink) {
return EmberObject.create({
ok: true,
reason: I18n.t("user.email.authenticated_by_invite"),
});
}
if (emailValid(email)) { if (emailValid(email)) {
return EmberObject.create({ return EmberObject.create({
ok: true, ok: true,

View File

@ -22,7 +22,7 @@ function setAuthenticationData(hooks, json) {
}); });
} }
function preloadInvite({ link = false } = {}) { function preloadInvite({ link = false, email_verified_by_link = false } = {}) {
const info = { const info = {
invited_by: { invited_by: {
id: 123, id: 123,
@ -32,6 +32,7 @@ function preloadInvite({ link = false } = {}) {
title: "team", title: "team",
}, },
username: "invited", username: "invited",
email_verified_by_link: email_verified_by_link,
}; };
if (link) { if (link) {
@ -360,3 +361,59 @@ acceptance(
}); });
} }
); );
acceptance(
"Email Invite link with valid authentication data, valid email token, unverified authentication email",
function (needs) {
needs.settings({ enable_local_logins: false });
setAuthenticationData(needs.hooks, {
auth_provider: "facebook",
email: "foobar@example.com",
email_valid: false,
username: "foobar",
name: "barfoo",
});
test("confirm form and buttons", async function (assert) {
preloadInvite({ email_verified_by_link: true });
await visit("/invites/myvalidinvitetoken");
assert.ok(!exists("#new-account-email"), "does not show email field");
assert.equal(
queryAll("#account-email-validation").text().trim(),
I18n.t("user.email.authenticated_by_invite")
);
});
}
);
acceptance(
"Email Invite link with valid authentication data, no email token, unverified authentication email",
function (needs) {
needs.settings({ enable_local_logins: false });
setAuthenticationData(needs.hooks, {
auth_provider: "facebook",
email: "foobar@example.com",
email_valid: false,
username: "foobar",
name: "barfoo",
});
test("confirm form and buttons", async function (assert) {
preloadInvite({ email_verified_by_link: false });
await visit("/invites/myvalidinvitetoken");
assert.ok(!exists("#new-account-email"), "does not show email field");
assert.equal(
queryAll("#account-email-validation").text().trim(),
I18n.t("user.email.ok")
);
});
}
);

View File

@ -31,6 +31,11 @@ class InvitesController < ApplicationController
end end
end end
email_verified_by_link = invite.email_token.present? && params[:t] == invite.email_token
if email_verified_by_link
email = invite.email
end
hidden_email = email != invite.email hidden_email = email != invite.email
info = { info = {
@ -38,7 +43,8 @@ class InvitesController < ApplicationController
email: email, email: email,
hidden_email: hidden_email, hidden_email: hidden_email,
username: hidden_email ? '' : UserNameSuggester.suggest(invite.email), username: hidden_email ? '' : UserNameSuggester.suggest(invite.email),
is_invite_link: invite.is_invite_link? is_invite_link: invite.is_invite_link?,
email_verified_by_link: email_verified_by_link
} }
if staged_user = User.where(staged: true).with_email(invite.email).first if staged_user = User.where(staged: true).with_email(invite.email).first

View File

@ -1311,6 +1311,7 @@ en:
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_auth_email_invalid: "Your invitation email does not match the email authenticated by %{provider}" invite_auth_email_invalid: "Your invitation email does not match the email authenticated by %{provider}"
authenticated_by_invite: "Your email has been authenticated by the invitation"
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."

View File

@ -41,6 +41,22 @@ describe InvitesController do
end end
end end
it 'includes token validity boolean' do
get "/invites/#{invite.invite_key}"
expect(response.body).to have_tag("div#data-preloaded") do |element|
json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
invite_info = JSON.parse(json['invite_info'])
expect(invite_info['email_verified_by_link']).to eq(false)
end
get "/invites/#{invite.invite_key}?t=#{invite.email_token}"
expect(response.body).to have_tag("div#data-preloaded") do |element|
json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
invite_info = JSON.parse(json['invite_info'])
expect(invite_info['email_verified_by_link']).to eq(true)
end
end
it 'fails for logged in users' do it 'fails for logged in users' do
sign_in(Fabricate(:user)) sign_in(Fabricate(:user))