Skip to content

Commit

Permalink
change-email: Implement confirmation flow.
Browse files Browse the repository at this point in the history
Fixes zulip#734.
  • Loading branch information
umairwaheed committed Feb 21, 2017
1 parent 8ac4fe1 commit 0775e9c
Show file tree
Hide file tree
Showing 18 changed files with 458 additions and 9 deletions.
24 changes: 24 additions & 0 deletions confirmation/migrations/0003_emailchangeconfirmation.py
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-17 09:16
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('confirmation', '0002_realmcreationkey'),
]

operations = [
migrations.CreateModel(
name='EmailChangeConfirmation',
fields=[
],
options={
'proxy': True,
},
bases=('confirmation.confirmation',),
),
]
23 changes: 21 additions & 2 deletions confirmation/models.py
Expand Up @@ -19,7 +19,7 @@

from confirmation.util import get_status_field
from zerver.lib.utils import generate_random_token
from zerver.models import PreregistrationUser
from zerver.models import PreregistrationUser, EmailChangeStatus
from typing import Optional, Union, Any, Text

B16_RE = re.compile('^[a-f0-9]{40}$')
Expand Down Expand Up @@ -59,7 +59,7 @@ def generate_realm_creation_url():
class ConfirmationManager(models.Manager):

def confirm(self, confirmation_key):
# type: (str) -> Union[bool, PreregistrationUser]
# type: (str) -> Union[bool, PreregistrationUser, EmailChangeStatus]
if B16_RE.search(confirmation_key):
try:
confirmation = self.get(confirmation_key=confirmation_key)
Expand Down Expand Up @@ -139,6 +139,19 @@ def send_confirmation(self, obj, email_address, additional_context=None,
send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [email_address], html_message=html_content)
return self.create(content_object=obj, date_sent=now(), confirmation_key=confirmation_key)

class EmailChangeConfirmationManager(ConfirmationManager):
def get_activation_url(self, key, host=None):
# type: (Text, Optional[str]) -> Text
if host is None:
host = settings.EXTERNAL_HOST
return u'%s%s%s' % (settings.EXTERNAL_URI_SCHEME,
host,
reverse('zerver.views.user_settings.confirm_email_change',
kwargs={'confirmation_key': key}))

def get_link_validity_in_days(self):
# type: () -> int
return getattr(settings, 'EMAIL_CHANGE_CONFIRMATION_DAYS', 1)

class Confirmation(models.Model):
content_type = models.ForeignKey(ContentType)
Expand All @@ -157,6 +170,12 @@ def __unicode__(self):
# type: () -> Text
return _('confirmation email for %s') % (self.content_object,)

class EmailChangeConfirmation(Confirmation):
class Meta(object):
proxy = True

objects = EmailChangeConfirmationManager()

class RealmCreationKey(models.Model):
creation_key = models.CharField(_('activation key'), max_length=40)
date_created = models.DateTimeField(_('created'), default=now)
32 changes: 32 additions & 0 deletions static/js/settings.js
Expand Up @@ -517,12 +517,44 @@ function _setup_page() {
});
});

$('#change_email_button').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
$('#change_email_modal').modal('hide');

var data = {};
data.email = $('.email_change_container').find("input[name='email']").val();

channel.patch({
url: '/json/settings/change',
data: data,
success: function (data) {
if ('account.email' in data) {
settings_change_success(data['account.email']);
} else {
settings_change_error(i18n.t("Error changing settings: No new data supplied."));
}
},
error: function (xhr) {
settings_change_error("Error changing settings", xhr);
},
});
});

$('#default_language').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
$('#default_language_modal').show().attr('aria-hidden', false);
});

$('#change_email').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
$('#change_email_modal').modal('show');
var email = $('#email_value').text();
$('.email_change_container').find("input[name='email']").val(email);
});

$("#user_deactivate_account_button").on('click', function (e) {
e.preventDefault();
e.stopPropagation();
Expand Down
25 changes: 25 additions & 0 deletions static/templates/settings/account-settings.handlebars
Expand Up @@ -4,6 +4,31 @@
{{t "Your account" }}
</div>
<div class="account-settings-form">
<form class="email-change-form">
<p for="change_email" class="inline-block title">
{{t "Email" }}: <span id='email_value'>{{page_params.email}}</span>
<a id="change_email" href="#change_email" title="{{t 'Change email' }}">[Change]</a>
</p>

<div id="change_email_modal" class="modal hide" tabindex="-1" role="dialog"
aria-labelledby="change_email_modal_label" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="change_email_modal_label">{{t "Change email" }}</h3>
</div>
<div class="modal-body">
<div class="input-group email_change_container">
<label for="email">{{t "Email" }}</label>
<input type="text" name="email" value="{{ page_params.email }}" />
</div>
</div>
<div class="modal-footer">
<button id='change_email_button' class="btn btn-success" data-dismiss="modal" aria-hidden="true">{{t "Change" }}</button>
<button class="btn btn-primary" data-dismiss="modal" aria-hidden="true">{{t "Close" }}</button>
</div>
</div>
</form>

<form action="/json/settings/change" method="post"
class="form-horizontal your-account-settings">

Expand Down
32 changes: 32 additions & 0 deletions templates/confirmation/confirm_email_change.html
@@ -0,0 +1,32 @@
{% extends "zerver/portico.html" %}

{% block portico_content %}

<div class="pitch">
<hr/>
{% if confirmed %}
<p>Your new email address is confirmed.</p>
{% else %}

<p class="lead">Whoops, something's not right. We couldn't find your confirmation ID!</p>

{% if verbose_support_offers %}
<p>Make sure you copied the link correctly in to your browser. If you're
still encountering this page, its probably our fault. We're sorry.</p>

<p>Anyway, shoot us a line at
<a href="mailto:{{ support_email }}">{{ support_email }}</a>
and we'll get this resolved shortly.
</p>
{% else %}
<p>Make sure you copied the link correctly in to your browser.</p>

<p>If you're still having problems, please contact your Zulip administrator at
<a href="mailto:{{ support_email }}">{{ support_email }}</a>.
</p>
{% endif %}

{% endif %}
</div>

{% endblock %}
38 changes: 38 additions & 0 deletions templates/confirmation/emailchangestatus_confirmation_email.html
@@ -0,0 +1,38 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Zulip</title>
</head>
<body>
<table width="80%" style="align:center; max-width:800px" align="center">
<tr>
<td style="font-size:16px; font-family:Helvetica;">
<p>Hi!
</p>

<p>Our records show that you requested a change to the email address
you use to sign in to Zulip. Please click the link below to activate
your new address.
<br />
<a href="{{ activate_url }}" style="color:#08c">{{ activate_url }}</a>
</p>

<p>
{% if verbose_support_offers %}
Feel free to give us a shout at <a href="mailto:{{ support_email }}" style="color:#08c">{{ support_email }}</a>, if you have any questions.
{% else %}
If you did not request this change, please contact your Zulip
administrator at <a href="mailto:{{ support_email }}" style="color:#08c">{{ support_email }}</a>.
{% endif %}
</p>

<p>
Cheers,<br />
The Zulip Team
</p>
</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1 @@
[Zulip] Confirm your new email address for {{ realm.name }}
17 changes: 17 additions & 0 deletions templates/confirmation/emailchangestatus_confirmation_email.txt
@@ -0,0 +1,17 @@
Hi!

Our records show that you requested a change to the email address you use to sign in to Zulip.
Please click the link below to activate your new address.

{{ activate_url }}


{% if verbose_support_offers %}
Feel free to give us a shout at <{{ support_email }}> if you have any questions.
{% else %}
If you did not request this change, please contact your Zulip administrator at <{{ support_email }}>.
{% endif %}

Cheers,

The Zulip Team
9 changes: 9 additions & 0 deletions templates/confirmation/notify_change_in_email_body.txt
@@ -0,0 +1,9 @@
Hi,

We just wanted to let you know that the email associated with your Zulip account
was recently changed to {{ new_email }}. If you did not request this change,
please contact us immediately at <{{ support_email }}>.

Best,

The Zulip Team
1 change: 1 addition & 0 deletions templates/confirmation/notify_change_in_email_subject.txt
@@ -0,0 +1 @@
[Zulip] Email address changed for {{ realm.name }}
2 changes: 2 additions & 0 deletions tools/lint-all
Expand Up @@ -375,6 +375,8 @@ def build_custom_checkers(by_lang):
'return json_error(data=error_data, msg=ret_error)'),
('zerver/views/streams.py', 'return json_error(property_conversion)'),
('zerver/views/streams.py', 'return json_error(e.error, data=result, status=404)'),
# error and skipped are already internationalized
('zerver/views/user_settings.py', 'return json_error(error or skipped)'),
# We can't do anything about this.
('zerver/views/realm_filters.py', 'return json_error(e.messages[0], data={"errors": dict(e)})'),
]),
Expand Down
31 changes: 28 additions & 3 deletions zerver/lib/actions.py
Expand Up @@ -37,7 +37,7 @@
realm_filters_for_realm, RealmFilter, receives_offline_notifications, \
ScheduledJob, get_owned_bot_dicts, \
get_old_unclaimed_attachments, get_cross_realm_emails, receives_online_notifications, \
Reaction
Reaction, EmailChangeStatus

from zerver.lib.alert_words import alert_words_in_realm
from zerver.lib.avatar import avatar_url
Expand All @@ -50,7 +50,7 @@
from django.core.mail import EmailMessage
from django.utils.timezone import now

from confirmation.models import Confirmation
from confirmation.models import Confirmation, EmailChangeConfirmation
import six
from six.moves import filter
from six.moves import map
Expand Down Expand Up @@ -694,14 +694,39 @@ def do_deactivate_stream(stream, log=True):

def do_change_user_email(user_profile, new_email):
# type: (UserProfile, Text) -> None
old_email = user_profile.email
user_profile.email = new_email
user_profile.save(update_fields=["email"])

payload = dict(new_email=new_email)
send_event(dict(type='update_email', op='update', payload=payload),
[user_profile.id])

def do_start_email_change_process(user_profile, new_email):
# type: (UserProfile, Text) -> None
old_email = user_profile.email
user_profile.email = new_email

log_event({'type': 'user_email_changed',
'old_email': old_email,
'new_email': new_email})

context = {'support_email': settings.ZULIP_ADMINISTRATOR,
'verbose_support_offers': settings.VERBOSE_SUPPORT_OFFERS,
'realm': user_profile.realm,
}

with transaction.atomic():
obj = EmailChangeStatus.objects.create(new_email=new_email,
old_email=old_email,
user_profile=user_profile)

EmailChangeConfirmation.objects.send_confirmation(
obj, new_email,
additional_context=context,
host=user_profile.realm.host,
)


def compute_irc_user_fullname(email):
# type: (NonBinaryStr) -> NonBinaryStr
return email.split("@")[0] + " (IRC)"
Expand Down
29 changes: 29 additions & 0 deletions zerver/migrations/0053_emailchangestatus.py
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-17 09:18
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('zerver', '0052_auto_fix_realmalias_realm_nullable'),
]

operations = [
migrations.CreateModel(
name='EmailChangeStatus',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('new_email', models.EmailField(max_length=254)),
('old_email', models.EmailField(max_length=254)),
('updated_at', models.DateTimeField(auto_now=True)),
('status', models.IntegerField(default=0)),
('realm', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='zerver.Realm')),
('user_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
12 changes: 12 additions & 0 deletions zerver/models.py
Expand Up @@ -678,6 +678,18 @@ class PreregistrationUser(models.Model):

realm = models.ForeignKey(Realm, null=True) # type: Optional[Realm]

class EmailChangeStatus(models.Model):
new_email = models.EmailField() # type: Text
old_email = models.EmailField() # type: Text
updated_at = models.DateTimeField(auto_now=True) # type: datetime.datetime
user_profile = models.ForeignKey(UserProfile) # type: UserProfile

# status: whether an object has been confirmed.
# if confirmed, set to confirmation.settings.STATUS_ACTIVE
status = models.IntegerField(default=0) # type: int

realm = models.ForeignKey(Realm, null=True) # type: Optional[Realm]

class PushDeviceToken(models.Model):
APNS = 1
GCM = 2
Expand Down

0 comments on commit 0775e9c

Please sign in to comment.