demo-orgs: Add email and password process for demo organization owners.

Creates process for demo organization owners to add an email address
and password to their account.

Uses the same flow as changing an email (via user settings) at the
beginning, but then sends a different email template to the user
for the email confirmation process.

We also encourage users to set their full name field in the modal for
adding an email in a demo organization. We disable the submit button
on the form if either input is empty, email or full name.

When the user clicks the 'confirm and set password' button in the
email sent to confirm the email address sent via the form, their
email is updated via confirm_email_change, but the user is redirected
to the reset password page for their account (instead of the page for
confirming an email change has happened).

Once the user successfully sets a password, then they will be
prompted to log in with their newly configured email and password.
This commit is contained in:
Lauryn Menard 2023-07-20 21:05:37 +02:00 committed by Tim Abbott
parent 2e00ca4197
commit 91b40a45fe
9 changed files with 219 additions and 2 deletions

View File

@ -0,0 +1,14 @@
{% extends "zerver/emails/email_base_default.html" %}
{% block illustration %}
<img src="{{ email_images_base_uri }}/email_logo.png" alt=""/>
{% endblock %}
{% block content %}
<p>{% trans %}Hi,{% endtrans %}</p>
<p>{% trans realm_uri=macros.link_tag(realm_uri), new_email=macros.email_tag(new_email) %}We received a request to add the email address {{ new_email }} to your Zulip demo organization account on {{ realm_uri }}. To confirm this update and set a password for this account, please click below:{% endtrans %}
<a class="button" href="{{ activate_url }}">{{_('Confirm and set password') }}</a></p>
<p>{% trans support_email=macros.email_tag(support_email) %}If you did not request this change, please contact us immediately at {{ support_email }}.{% endtrans %}</p>
{% endblock %}

View File

@ -0,0 +1 @@
{{ _("Verify your new email address for your demo Zulip organization") }}

View File

@ -0,0 +1,15 @@
{% trans -%}
Hi,
{%- endtrans %}
{% trans -%}
We received a request to add the email address {{ new_email }} to your Zulip demo organization account on {{ realm_uri }}. To confirm this update and set a password for this account, please click below:
{%- endtrans %}
{{ activate_url }}
{% trans -%}
If you did not request this change, please contact us immediately at <{{ support_email }}>.
{%- endtrans %}

View File

@ -2,6 +2,7 @@ import $ from "jquery";
import render_change_email_modal from "../templates/change_email_modal.hbs"; import render_change_email_modal from "../templates/change_email_modal.hbs";
import render_confirm_deactivate_own_user from "../templates/confirm_dialog/confirm_deactivate_own_user.hbs"; import render_confirm_deactivate_own_user from "../templates/confirm_dialog/confirm_deactivate_own_user.hbs";
import render_demo_organization_add_email_modal from "../templates/demo_organization_add_email_modal.hbs";
import render_dialog_change_password from "../templates/dialog_change_password.hbs"; import render_dialog_change_password from "../templates/dialog_change_password.hbs";
import render_settings_api_key_modal from "../templates/settings/api_key_modal.hbs"; import render_settings_api_key_modal from "../templates/settings/api_key_modal.hbs";
import render_settings_custom_user_profile_field from "../templates/settings/custom_user_profile_field.hbs"; import render_settings_custom_user_profile_field from "../templates/settings/custom_user_profile_field.hbs";
@ -743,6 +744,91 @@ export function set_up() {
} }
}); });
function do_demo_organization_add_email(e) {
e.preventDefault();
e.stopPropagation();
const $change_email_error = $("#demo_organization_add_email_modal").find("#dialog_error");
const data = {};
data.email = $("#demo_organization_add_email").val();
data.full_name = $("#demo_organization_update_full_name").val();
const opts = {
success_continuation() {
if (page_params.development_environment) {
const email_msg = render_settings_dev_env_email_access();
ui_report.success(
email_msg,
$("#dev-account-settings-status").expectOne(),
4000,
);
}
dialog_widget.close_modal();
},
error_continuation() {
dialog_widget.hide_dialog_spinner();
},
$error_msg_element: $change_email_error,
success_msg_html: $t_html(
{defaultMessage: "Check your email ({email}) to confirm the new address."},
{email: data.email},
),
sticky: true,
};
settings_ui.do_settings_change(
channel.patch,
"/json/settings",
data,
$("#account-settings-status").expectOne(),
opts,
);
}
$("#demo_organization_add_email_button").on("click", (e) => {
e.preventDefault();
e.stopPropagation();
function demo_organization_add_email_post_render() {
// Disable submit button if either input is an empty string.
const $add_email_element = $("#demo_organization_add_email");
const $add_name_element = $("#demo_organization_update_full_name");
const $demo_organization_submit_button = $(
"#demo_organization_add_email_modal .dialog_submit_button",
);
$demo_organization_submit_button.prop("disabled", true);
$("#demo_organization_add_email_form input").on("input", () => {
$demo_organization_submit_button.prop(
"disabled",
$add_email_element.val().trim() === "" || $add_name_element.val().trim() === "",
);
});
}
if (
page_params.demo_organization_scheduled_deletion_date &&
page_params.is_owner &&
page_params.delivery_email === ""
) {
dialog_widget.launch({
html_heading: $t_html({defaultMessage: "Add email"}),
html_body: render_demo_organization_add_email_modal({
delivery_email: page_params.delivery_email,
full_name: page_params.full_name,
}),
html_submit_button: $t_html({defaultMessage: "Add"}),
loading_spinner: true,
id: "demo_organization_add_email_modal",
form_id: "demo_organization_add_email_form",
on_click: do_demo_organization_add_email,
on_shown() {
ui_util.place_caret_at_end($("#demo_organization_add_email_form input")[0]);
},
post_render: demo_organization_add_email_post_render,
});
}
});
$("#profile-settings").on("click", ".custom_user_field .remove_date", (e) => { $("#profile-settings").on("click", ".custom_user_field .remove_date", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View File

@ -0,0 +1,11 @@
<form id="demo_organization_add_email_form" class="new-style">
<div class="tip">{{t "If you haven't updated your name, it's a good idea to do so before inviting other users to join you!" }}</div>
<div class="input-group">
<label for="demo_organization_add_email">{{t "Email" }}</label>
<input id="demo_organization_add_email" type="text" name="email" class="modal_text_input" value="{{delivery_email}}" autocomplete="off" spellcheck="false" autofocus="autofocus"/>
</div>
<div class="input-group">
<label for="demo_organization_update_full_name">{{t "Full name" }}</label>
<input id="demo_organization_update_full_name" name="full_name" type="text" class="modal_text_input" value="{{full_name}}" maxlength="60" />
</div>
</form>

View File

@ -6,6 +6,7 @@
<h3 class="inline-block">{{t "Account" }}</h3> <h3 class="inline-block">{{t "Account" }}</h3>
<div class="alert-notification" id="account-settings-status"></div> <div class="alert-notification" id="account-settings-status"></div>
<form class="grid"> <form class="grid">
{{#if user_has_email_set}}
<div class="input-group"> <div class="input-group">
<label class="inline-block title">{{t "Email" }}</label> <label class="inline-block title">{{t "Email" }}</label>
<div id="change_email_button_container" class="inline-block {{#unless user_can_change_email}}disabled_setting_tooltip{{/unless}}"> <div id="change_email_button_container" class="inline-block {{#unless user_can_change_email}}disabled_setting_tooltip{{/unless}}">
@ -15,6 +16,22 @@
</button> </button>
</div> </div>
</div> </div>
{{else}}
{{! Demo organizations before the owner has configured an email address. }}
<div class="input-group">
<p>
{{#tr}}
Add your email to <z-link-invite-users-help>invite other users</z-link-invite-users-help>
or <z-link-convert-demo-organization-help>convert to a permanent Zulip organization</z-link-convert-demo-organization-help>.
{{#*inline "z-link-invite-users-help"}}<a href="/help/invite-new-users" target="_blank" rel="noopener noreferrer">{{> @partial-block}}</a>{{/inline}}
{{#*inline "z-link-convert-demo-organization-help"}}<a href="/help/demo-organizations#convert-a-demo-organization-to-a-permanent-organization" target="_blank" rel="noopener noreferrer">{{> @partial-block}}</a>{{/inline}}
{{/tr}}
</p>
<button id="demo_organization_add_email_button" type="button" class="button rounded sea-green">
{{t "Add email"}}
</button>
</div>
{{/if}}
</form> </form>
{{#if page_params.two_fa_enabled }} {{#if page_params.two_fa_enabled }}

View File

@ -151,8 +151,20 @@ def do_start_email_change_process(user_profile: UserProfile, new_email: str) ->
activate_url=activation_url, activate_url=activation_url,
) )
language = user_profile.default_language language = user_profile.default_language
email_template = "zerver/emails/confirm_new_email"
if old_email == "":
# The assertions here are to help document the only circumstance under which
# this condition should be possible.
assert (
user_profile.realm.demo_organization_scheduled_deletion_date is not None
and user_profile.is_realm_owner
)
email_template = "zerver/emails/confirm_demo_organization_email"
send_email( send_email(
"zerver/emails/confirm_new_email", template_prefix=email_template,
to_emails=[new_email], to_emails=[new_email],
from_name=FromAddress.security_email_from_name(language=language), from_name=FromAddress.security_email_from_name(language=language),
from_address=FromAddress.tokenized_no_reply_address(), from_address=FromAddress.tokenized_no_reply_address(),

View File

@ -296,3 +296,48 @@ class EmailChangeTestCase(ZulipTestCase):
with self.assertRaises(UserProfile.DoesNotExist): with self.assertRaises(UserProfile.DoesNotExist):
get_user_by_delivery_email(old_email, user_profile.realm) get_user_by_delivery_email(old_email, user_profile.realm)
self.assertEqual(get_user_by_delivery_email(new_email, user_profile.realm), user_profile) self.assertEqual(get_user_by_delivery_email(new_email, user_profile.realm), user_profile)
def test_configure_demo_organization_owner_email(self) -> None:
desdemona = self.example_user("desdemona")
desdemona.realm.demo_organization_scheduled_deletion_date = now() + datetime.timedelta(
days=30
)
desdemona.realm.save()
assert desdemona.realm.demo_organization_scheduled_deletion_date is not None
self.login("desdemona")
desdemona.delivery_email = ""
desdemona.save()
self.assertEqual(desdemona.delivery_email, "")
data = {"email": "desdemona-new@zulip.com"}
url = "/json/settings"
self.assert_length(mail.outbox, 0)
result = self.client_patch(url, data)
self.assert_json_success(result)
self.assert_length(mail.outbox, 1)
email_message = mail.outbox[0]
self.assertEqual(
email_message.subject,
"Verify your new email address for your demo Zulip organization",
)
body = email_message.body
self.assertIn(
"We received a request to add the email address",
body,
)
self.assertEqual(self.email_envelope_from(email_message), settings.NOREPLY_EMAIL_ADDRESS)
self.assertRegex(
self.email_display_from(email_message),
rf"^Zulip Account Security <{self.TOKENIZED_NOREPLY_REGEX}>\Z",
)
self.assertEqual(email_message.extra_headers["List-Id"], "Zulip Dev <zulip.testserver>")
confirmation_url = [s for s in body.split("\n") if s][2]
response = self.client_get(confirmation_url, follow=True)
self.assertEqual(response.status_code, 200)
self.assert_in_success_response(["Set a new password"], response)
user_profile = get_user_profile_by_id(desdemona.id)
self.assertEqual(user_profile.delivery_email, "desdemona-new@zulip.com")

View File

@ -3,9 +3,10 @@ from typing import Any, Dict, Optional
from django.conf import settings from django.conf import settings
from django.contrib.auth import authenticate, update_session_auth_hash from django.contrib.auth import authenticate, update_session_auth_hash
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import UploadedFile from django.core.files.uploadedfile import UploadedFile
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import SafeString from django.utils.safestring import SafeString
@ -28,6 +29,7 @@ from zerver.actions.user_settings import (
do_start_email_change_process, do_start_email_change_process,
) )
from zerver.decorator import human_users_only from zerver.decorator import human_users_only
from zerver.forms import generate_password_reset_url
from zerver.lib.avatar import avatar_url from zerver.lib.avatar import avatar_url
from zerver.lib.email_validation import ( from zerver.lib.email_validation import (
get_realm_email_validator, get_realm_email_validator,
@ -88,6 +90,20 @@ def confirm_email_change(request: HttpRequest, confirmation_key: str) -> HttpRes
context = {"realm_name": user_profile.realm.name, "new_email": new_email} context = {"realm_name": user_profile.realm.name, "new_email": new_email}
language = user_profile.default_language language = user_profile.default_language
if old_email == "":
# The assertions here are to help document the only circumstance under which
# this condition should be possible.
assert (
user_profile.realm.demo_organization_scheduled_deletion_date is not None
and user_profile.is_realm_owner
)
# Because demo organizations are created without setting an email and password
# we want to redirect to setting a password after configuring and confirming
# an email for the owner's account.
reset_password_url = generate_password_reset_url(user_profile, default_token_generator)
return HttpResponseRedirect(reset_password_url)
send_email( send_email(
"zerver/emails/notify_change_in_email", "zerver/emails/notify_change_in_email",
to_emails=[old_email], to_emails=[old_email],