l
The last name of the user logged in
e
- The email address of the user logged in
+ The primary email address of the user logged in
+ se
+ A comma separated list of secondary email addresses for the user logged in
d
base64 encoded data block to be passed along in confirmation (optional)
su
u
Username
e
- Email
+ Primary email
f
First name
l
Last name
+ se
+ Array of secondary email addresses
from django.contrib.auth.models import User
from pgweb.core.models import UserProfile
from pgweb.contributors.models import Contributor
+from .models import SecondaryEmail
from .recaptcha import ReCaptchaField
class UserForm(forms.ModelForm):
- def __init__(self, *args, **kwargs):
+ primaryemail = forms.ChoiceField(choices=[], required=True, label='Primary email address')
+
+ def __init__(self, can_change_email, secondaryaddresses, *args, **kwargs):
super(UserForm, self).__init__(*args, **kwargs)
self.fields['first_name'].required = True
self.fields['last_name'].required = True
+ if can_change_email:
+ self.fields['primaryemail'].choices = [(self.instance.email, self.instance.email), ] + [(a.email, a.email) for a in secondaryaddresses if a.confirmed]
+ if not secondaryaddresses:
+ self.fields['primaryemail'].help_text = "To change the primary email address, first add it as a secondary address below"
+ else:
+ self.fields['primaryemail'].choices = [(self.instance.email, self.instance.email), ]
+ self.fields['primaryemail'].help_text = "You cannot change the primary email of this account since it is connected to an external authentication system"
+ self.fields['primaryemail'].widget.attrs['disabled'] = True
+ self.fields['primaryemail'].required = False
class Meta:
model = User
- fields = ('first_name', 'last_name', )
+ fields = ('primaryemail', 'first_name', 'last_name', )
class ContributorForm(forms.ModelForm):
exclude = ('ctype', 'lastname', 'firstname', 'user', )
-class ChangeEmailForm(forms.Form):
- email = forms.EmailField()
- email2 = forms.EmailField(label="Repeat email")
+class AddEmailForm(forms.Form):
+ email1 = forms.EmailField(label="New email", required=False)
+ email2 = forms.EmailField(label="Repeat email", required=False)
def __init__(self, user, *args, **kwargs):
- super(ChangeEmailForm, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
self.user = user
- def clean_email(self):
- email = self.cleaned_data['email'].lower()
+ def clean_email1(self):
+ email = self.cleaned_data['email1'].lower()
if email == self.user.email:
raise forms.ValidationError("This is your existing email address!")
if User.objects.filter(email=email).exists():
raise forms.ValidationError("A user with this email address is already registered")
+ try:
+ s = SecondaryEmail.objects.get(email=email)
+ if s.user == self.user:
+ raise forms.ValidationError("This email address is already connected to your account")
+ else:
+ raise forms.ValidationError("A user with this email address is already registered")
+ except SecondaryEmail.DoesNotExist:
+ pass
+
return email
def clean_email2(self):
# If the primary email checker had an exception, the data will be gone
# from the cleaned_data structure
- if 'email' not in self.cleaned_data:
+ if 'email1' not in self.cleaned_data:
return self.cleaned_data['email2'].lower()
- email1 = self.cleaned_data['email'].lower()
+ email1 = self.cleaned_data['email1'].lower()
email2 = self.cleaned_data['email2'].lower()
if email1 != email2:
--- /dev/null
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.27 on 2020-08-06 16:12
+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 = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('account', '0004_cauth_last_login'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='SecondaryEmail',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('email', models.EmailField(max_length=75, unique=True)),
+ ('confirmed', models.BooleanField(default=False)),
+ ('token', models.CharField(max_length=100)),
+ ('sentat', models.DateTimeField(auto_now=True)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ('email', ),
+ },
+ ),
+ migrations.DeleteModel(
+ name='EmailChangeToken',
+ ),
+ ]
unique_together = (('user', 'org'), )
-class EmailChangeToken(models.Model):
- user = models.OneToOneField(User, null=False, blank=False, on_delete=models.CASCADE)
- email = models.EmailField(max_length=75, null=False, blank=False)
+class SecondaryEmail(models.Model):
+ user = models.ForeignKey(User, null=False, blank=False, on_delete=models.CASCADE)
+ email = models.EmailField(max_length=75, null=False, blank=False, unique=True)
+ confirmed = models.BooleanField(null=False, blank=False, default=False)
token = models.CharField(max_length=100, null=False, blank=False)
sentat = models.DateTimeField(null=False, blank=False, auto_now=True)
+
+ class Meta:
+ ordering = ('email', )
# Profile
url(r'^profile/$', pgweb.account.views.profile),
- url(r'^profile/change_email/$', pgweb.account.views.change_email),
- url(r'^profile/change_email/([0-9a-f]+)/$', pgweb.account.views.confirm_change_email),
+ url(r'^profile/add_email/([0-9a-f]+)/$', pgweb.account.views.confirm_add_email),
# List of items to edit
url(r'^edit/(.*)/$', pgweb.account.views.listobjects),
from django.contrib.auth import logout as django_logout
from django.conf import settings
from django.db import transaction, connection
-from django.db.models import Q
+from django.db.models import Q, Prefetch
import base64
import urllib.parse
from pgweb.downloads.models import Product
from pgweb.profserv.models import ProfessionalService
-from .models import CommunityAuthSite, CommunityAuthConsent, EmailChangeToken
+from .models import CommunityAuthSite, CommunityAuthConsent, SecondaryEmail
from .forms import PgwebAuthenticationForm
from .forms import CommunityAuthConsentForm
from .forms import SignupForm, SignupOauthForm
from .forms import UserForm, UserProfileForm, ContributorForm
-from .forms import ChangeEmailForm, PgwebPasswordResetForm
+from .forms import AddEmailForm, PgwebPasswordResetForm
import logging
log = logging.getLogger(__name__)
contribform = None
+ secondaryaddresses = SecondaryEmail.objects.filter(user=request.user)
+
if request.method == 'POST':
# Process this form
- userform = UserForm(data=request.POST, instance=request.user)
+ userform = UserForm(can_change_email, secondaryaddresses, data=request.POST, instance=request.user)
profileform = UserProfileForm(data=request.POST, instance=profile)
+ secondaryemailform = AddEmailForm(request.user, data=request.POST)
if contrib:
contribform = ContributorForm(data=request.POST, instance=contrib)
- if userform.is_valid() and profileform.is_valid() and (not contrib or contribform.is_valid()):
- userform.save()
+ if userform.is_valid() and profileform.is_valid() and secondaryemailform.is_valid() and (not contrib or contribform.is_valid()):
+ user = userform.save()
+
+ # Email takes some magic special handling, since we only allow picking of existing secondary emails, but it's
+ # not a foreign key (due to how the django auth model works).
+ if can_change_email and userform.cleaned_data['primaryemail'] != user.email:
+ # Changed it!
+ oldemail = user.email
+ # Create a secondary email for the old primary one
+ SecondaryEmail(user=user, email=oldemail, confirmed=True, token='').save()
+ # Flip the main email
+ user.email = userform.cleaned_data['primaryemail']
+ user.save(update_fields=['email', ])
+ # Finally remove the old secondary address, since it can`'t be both primary and secondary at the same time
+ SecondaryEmail.objects.filter(user=user, email=user.email).delete()
+ log.info("User {} changed primary email from {} to {}".format(user.username, oldemail, user.email))
+
profileform.save()
if contrib:
contribform.save()
- return HttpResponseRedirect("/account/")
+ if secondaryemailform.cleaned_data.get('email1', ''):
+ sa = SecondaryEmail(user=request.user, email=secondaryemailform.cleaned_data['email1'], token=generate_random_token())
+ sa.save()
+ send_template_mail(
+ settings.ACCOUNTS_NOREPLY_FROM,
+ sa.email,
+ 'Your postgresql.org community account',
+ 'account/email_add_email.txt',
+ {'secondaryemail': sa, 'user': request.user, }
+ )
+
+ for k, v in request.POST.items():
+ if k.startswith('deladdr_') and v == '1':
+ ii = int(k[len('deladdr_'):])
+ SecondaryEmail.objects.filter(user=request.user, id=ii).delete()
+
+ return HttpResponseRedirect(".")
else:
# Generate form
- userform = UserForm(instance=request.user)
+ userform = UserForm(can_change_email, secondaryaddresses, instance=request.user)
profileform = UserProfileForm(instance=profile)
+ secondaryemailform = AddEmailForm(request.user)
if contrib:
contribform = ContributorForm(instance=contrib)
return render_pgweb(request, 'account', 'account/userprofileform.html', {
'userform': userform,
'profileform': profileform,
+ 'secondaryemailform': secondaryemailform,
+ 'secondaryaddresses': secondaryaddresses,
+ 'secondarypending': any(not a.confirmed for a in secondaryaddresses),
'contribform': contribform,
- 'can_change_email': can_change_email,
})
@login_required
@transaction.atomic
-def change_email(request):
- tokens = EmailChangeToken.objects.filter(user=request.user)
- token = len(tokens) and tokens[0] or None
-
- if request.user.password == OAUTH_PASSWORD_STORE:
- # Link shouldn't exist in this case, so just throw an unfriendly
- # error message.
- return HttpSimpleResponse(request, "Account error", "This account cannot change email address as it's connected to a third party login site.")
-
- if request.method == 'POST':
- form = ChangeEmailForm(request.user, data=request.POST)
- if form.is_valid():
- # If there is an existing token, delete it
- if token:
- token.delete()
-
- # Create a new token
- token = EmailChangeToken(user=request.user,
- email=form.cleaned_data['email'].lower(),
- token=generate_random_token())
- token.save()
+def confirm_add_email(request, tokenhash):
+ addr = get_object_or_404(SecondaryEmail, user=request.user, token=tokenhash)
- send_template_mail(
- settings.ACCOUNTS_NOREPLY_FROM,
- form.cleaned_data['email'],
- 'Your postgresql.org community account',
- 'account/email_change_email.txt',
- {'token': token, 'user': request.user, }
- )
- return HttpResponseRedirect('done/')
- else:
- form = ChangeEmailForm(request.user)
-
- return render_pgweb(request, 'account', 'account/emailchangeform.html', {
- 'form': form,
- 'token': token,
- })
-
-
-@login_required
-@transaction.atomic
-def confirm_change_email(request, tokenhash):
- tokens = EmailChangeToken.objects.filter(user=request.user, token=tokenhash)
- token = len(tokens) and tokens[0] or None
-
- if request.user.password == OAUTH_PASSWORD_STORE:
- # Link shouldn't exist in this case, so just throw an unfriendly
- # error message.
- return HttpSimpleResponse(request, "Account error", "This account cannot change email address as it's connected to a third party login site.")
-
- if token:
- # Valid token find, so change the email address
- request.user.email = token.email.lower()
- request.user.save()
- token.delete()
-
- return render_pgweb(request, 'account', 'account/emailchangecompleted.html', {
- 'token': tokenhash,
- 'success': token and True or False,
- })
+ # Valid token found, so mark the address as confirmed.
+ addr.confirmed = True
+ addr.token = ''
+ addr.save()
+ return HttpResponseRedirect('/account/profile/')
@login_required
'f': request.user.first_name.encode('utf-8'),
'l': request.user.last_name.encode('utf-8'),
'e': request.user.email.encode('utf-8'),
+ 'se': ','.join([a.email for a in SecondaryEmail.objects.filter(user=request.user, confirmed=True).order_by('email')]).encode('utf8'),
}
if d:
info['d'] = d.encode('utf-8')
else:
raise Http404('No search term specified')
- users = User.objects.filter(q)
+ users = User.objects.prefetch_related(Prefetch('secondaryemail_set', queryset=SecondaryEmail.objects.filter(confirmed=True))).filter(q)
- j = json.dumps([{'u': u.username, 'e': u.email, 'f': u.first_name, 'l': u.last_name} for u in users])
+ j = json.dumps([{
+ 'u': u.username,
+ 'e': u.email,
+ 'f': u.first_name,
+ 'l': u.last_name,
+ 'se': [a.email for a in u.secondaryemail_set.all()],
+ } for u in users])
return HttpResponse(_encrypt_site_response(site, j))
from datetime import datetime, timedelta
-from pgweb.account.models import EmailChangeToken
+from pgweb.account.models import SecondaryEmail
class Command(BaseCommand):
# Clean up old email change tokens
with transaction.atomic():
- EmailChangeToken.objects.filter(sentat__lt=datetime.now() - timedelta(hours=24)).delete()
+ SecondaryEmail.objects.filter(confirmed=False, sentat__lt=datetime.now() - timedelta(hours=24)).delete()
--- /dev/null
+Somebody, probably you, attempted to add this email address to
+the PostgreSQL community account {{user.username}}.
+
+To confirm the addition of this email address, please click
+the following link:
+
+{{link_root}}/account/profile/add_email/{{secondaryemail.token}}/
+
+If you do not approve of this, you can ignore this email.
+++ /dev/null
-Somebody, probably you, attempted to change the email of the
-PostgreSQL community account {{user.username}} to this email address.
-
-To confirm this change of email address, please click the following
-link:
-
-{{link_root}}/account/profile/change_email/{{token.token}}/
-
+++ /dev/null
-{%extends "base/page.html"%}
-{%block title%}{%if success%}Email changed{%else%}Change email{%endif%}{%endblock%}
-{%block contents%}
-
-{%if success%}
-<h1>Email changed <i class="fas fa-envelope"></i></h1>
-<p>
-Your email has successfully been changed to {{user.email}}.
-</p>
-<p>
-Please note that if you are using your account from a different
-community site than <em>www.postgresql.org</em>, you may need to log
-out and back in again for the email to be updated on that site.
-</p>
-{%else%}
-<h1>Change email <i class="fas fa-envelope"></i></h1>
-<p>
-The token <code>{{token}}</code> was not found.
-</p>
-<p>
-This can be because it expired (tokens are valid for approximately
-24 hours), or because you did not paste the complete URL without any
-spaces.
-</p>
-<p>
-Double check the URL, and if it is correct, restart the process by
-clicking "change" in your profile to generate a new token and try again.
-</p>
-{%endif%}
-
-<p>
-<a href="/account/profile/">Return</a> to your profile.
-</p>
-{%endblock%}
+++ /dev/null
-{%extends "base/page.html"%}
-{% load pgfilters %}
-{%block title%}Change email{%endblock%}
-{%block contents%}
-<h1>Change email <i class="fas fa-envelope"></i></h1>
-{%if token%}
-<h2>Awaiting confirmation</h2>
-<p>
-A confirmation token was sent to {{token.email}} on {{token.sentat|date:"Y-m-d H:i"}}.
-Wait for this token to arrive, and then click the link that is sent
-in the email to confirm the change of email.
-</p>
-<p>
-The token will be valid for approximately 24 hours, after which it will
-be automatically deleted.
-</p>
-<p>
-To create a new token (and a new email), fill out the form below again.
-Note that once a new token is created, the old token will no longer be
-valid for use.
-</p>
-
-<h2>Change email</h2>
-{%else%}
-<p>
-To change your email address, input the new address below. Once you
-click "Change email", a verification token will be sent to the new email address,
-and once you click the link in that email, your email will be changed.
-</p>
-{%endif%}
-
-<form method="post" action=".">{% csrf_token %}
- {% if form.errors %}
- <div class="alert alert-danger" role="alert">
- Please correct the errors below, and re-submit the form.
- </div>
- {% endif %}
- {% for field in form %}
- <div class="form-group row">
- {% if field.errors %}
- {% for e in field.errors %}
- <div class="col-lg-12 alert alert-danger">{{e}}</div>
- {% endfor %}
- {% endif %}
- <label class="col-form-label col-sm-3" for="{{ field.id }}">
- {{ field.label|title }}
- {% if field.help_text %}
- <p><small>{{ field.help_text }}</small></p>
- {% endif %}
- </label>
- <div class="col-sm-9">
- {{ field|field_class:"form-control" }}
- </div>
- </div>
- {% endfor %}
- <div class="submit-row">
- <input class="btn btn-primary" type="submit" value="Change Email" />
- </div>
-</form>
-{%endblock%}
{{ user.username }}
</div>
</div>
- <div class="form-group row">
- <label class="col-form-label col-sm-3">Email:</label>
- <div class="col-sm-9">
- {{ user.email }}
- {% if can_change_email %}
- (<em><a href="change_email/">change</a></em>)
- {% else %}
- <p><em>The email address of this account cannot be changed, because the account does
- not have a local password, most likely because it's connected to a third
- party system (such as Google or Facebook).</em></p>
- {% endif %}
- </div>
- </div>
{% for field in userform %}
<div class="form-group row">
{% if field.errors %}
</div>
</div>
{% endfor %}
+
+ <h2>Secondary email addresses</h2>
+ <p>You can add one or more secondary email addresses to your account, which can be used for example to subscribe to mailing lists.</p>
+ {%if secondaryaddresses%}
+ <p>Note that deleting any address here will cascade to connected system and can for example lead to being unsubscribed from mailing lists automatically.</p>
+ <p></p>
+ <p>The following secondary addresses are currently registered with your account:</p>
+ <ul>
+ {% for a in secondaryaddresses %}
+ <li>{{a.email}}{%if not a.confirmed%} <em>(awaiting confirmation since {{a.sentat}})</em>{%endif%} (<input type="checkbox" name="deladdr_{{a.id}}" value="1"> Delete)</li>
+ {%endfor%}
+ </ul>
+ {%if secondarypending %}
+ <p>
+ One or more of the secondary addresses on your account are listed as pending. An email has been sent to the address to confirm that
+ you are in control of the address. Open the link in this email (while logged in to this account) to confirm the account. If an email
+ address is not confirmed within approximately 24 hours, it will be deleted. If you have not received the confirmation token, you
+ can delete the address and re-add it, to have the system re-send the verification email.
+ </p>
+ {%endif%}
+ {%endif%}
+ <h3>Add new email address</h3>
+ {%for field in secondaryemailform%}
+ <div class="form-group row">
+ {% if field.errors %}
+ {% for e in field.errors %}
+ <div class="col-lg-12 alert alert-danger">{{e}}</div>
+ {% endfor %}
+ {% endif %}
+ <label class="col-form-label col-sm-3" for="{{ field.id }}">
+ {{ field.label }}
+ {% if field.help_text %}
+ <p><small>{{ field.help_text }}</small></p>
+ {% endif %}
+ </label>
+ <div class="col-sm-9">
+ {{ field|field_class:"form-control" }}
+ </div>
+ </div>
+ {%endfor%}
+
{% if contribform %}
<h2>Edit contributor information</h2>
<p>You can edit the information that's shown on the <a href="/community/contributors/" target="_blank">contributors</a> page. Please be careful as your changes will take effect immediately!
</div>
{% endfor %}
{% endif %}
+
<div class="submit-row">
<input class="btn btn-primary" type="submit" value="Save" />
</div>