SciPost Code Repository

Skip to content
Snippets Groups Projects
Commit dd95549b authored by George Katsikas's avatar George Katsikas :goat:
Browse files

add profile email verification procedure

fixes #186
parent 898f6a74
No related branches found
No related tags found
No related merge requests found
......@@ -5,14 +5,6 @@ from django.db import migrations, models
from django.utils import timezone
def reset_profile_email_verification_tokens(apps, schema_editor):
ProfileEmail = apps.get_model("profiles", "ProfileEmail")
for email in ProfileEmail.objects.all():
email.verification_token = secrets.token_urlsafe(40)
email.verified = email.primary and email.still_valid
email.save()
class Migration(migrations.Migration):
dependencies = [
("profiles", "0041_profile_orcid_authenticated"),
......@@ -29,13 +21,4 @@ class Migration(migrations.Migration):
name="verification_token",
field=models.CharField(max_length=128, null=True, unique=False),
),
migrations.RunPython(
reset_profile_email_verification_tokens,
reverse_code=migrations.RunPython.noop,
),
migrations.AlterField(
model_name="profileemail",
name="verification_token",
field=models.CharField(max_length=128, unique=True, null=False),
),
]
......@@ -3,6 +3,7 @@ __license__ = "AGPL v3"
import datetime
import secrets
from django.contrib.contenttypes.fields import GenericRelation
from django.db.models import Q
......@@ -12,6 +13,7 @@ from django.db.models.functions import Concat
from django.shortcuts import get_object_or_404
from django.utils import timezone
from mails.utils import DirectMailUtil
from scipost.behaviors import orcid_validator
from scipost.constants import TITLE_CHOICES, TITLE_DR
from scipost.models import Contributor
......@@ -213,7 +215,7 @@ class ProfileEmail(models.Model):
email = models.EmailField()
still_valid = models.BooleanField(default=True)
verified = models.BooleanField(default=False)
verification_token = models.CharField(max_length=128, unique=True)
verification_token = models.CharField(max_length=128, null=True)
token_expiration = models.DateTimeField(default=timezone.now)
added_by = models.ForeignKey(
"scipost.Contributor",
......@@ -232,6 +234,28 @@ class ProfileEmail(models.Model):
def __str__(self):
return self.email
def reset_verification_token(self):
self.verification_token = secrets.token_urlsafe(40)
self.token_expiration = timezone.now() + datetime.timedelta(hours=48)
self.save()
@property
def has_token_expired(self):
return timezone.now() > self.token_expiration
def send_verification_email(self):
if self.has_token_expired:
self.reset_verification_token()
mail_sender = DirectMailUtil("profiles/verify_profile_email", object=self)
mail_sender.send_mail()
def get_verification_url(self):
return reverse(
"profiles:verify_profile_email",
kwargs={"email_id": self.id, "token": self.verification_token},
)
def get_profiles(slug):
"""
......
......@@ -20,6 +20,8 @@
{% if profile_mail.verified %}
<span class="text-success">{% include "bi/check-circle-fill.html" %}</span>
{% elif not profile_mail.has_token_expired %}
<span class="text-warning">{% include "bi/clock-fill.html" %}</span>
{% else %}
<span class="text-danger">{% include "bi/x-circle-fill.html" %}</span>
{% endif %}
......@@ -46,16 +48,17 @@
</button>
{% endif %}
{% comment %} {% if is_mail_owner or perms.scipost.can_verify_profile_emails %}
<!-- TODO: Implement proper verification of emails -->
{% if is_mail_owner or perms.scipost.can_verify_profile_emails %}
<button type="button"
class="btn btn-sm btn-light py-0"
hx-target="closest tr"
hx-swap="outerHTML"
{% if not profile_mail.has_token_expired %}hx-confirm="Your previous verification code has not expired yet. Are you sure you want to resend the verification email?"{% endif %}
hx-confirm="This will send a verification email to the address. Are you sure?"
hx-patch="{% url 'profiles:_hx_profile_email_toggle_verified' profile_mail.id %}">Verify</button>
<!-- TODO END-->
{% endif %} {% endcomment %}
hx-patch="{% url 'profiles:_hx_profile_email_request_verification' profile_mail.id %}"
{% if profile_mail.verified %}disabled{% endif %}
>Verify</button>
{% endif %}
{% if is_mail_owner or perms.scipost.can_mark_profile_emails_primary %}
<button type="button"
......
{% extends "profiles/base.html" %}
{% block breadcrumb_items %}
{{ block.super }}
<span class="breadcrumb-item">Email Verification</span>
{% endblock %}
{% block content %}
<h1>Email Verification</h1>
<div class="w-100 text-center">
<div class="fs-2">
{% if was_previously_verified %}
<p>
<span class="text-success me-3">{% include "bi/check-circle-fill.html" %}</span>
Your email address has already been verified.
</p>
{% elif profile_email.verified %}
<p>
<span class="text-success me-3">{% include "bi/check-circle-fill.html" %}</span>
Your email address has been successfully verified.
</p>
{% else %}
<p>
<span class="text-danger me-3">{% include "bi/x-circle-fill.html" %}</span>
Your email address could not be verified.
</p>
{% if profile_email.has_token_expired %}
<p class="fs-5">
Your token has expired, please request a new one from the <a href="{% url "scipost:personal_page" %}">personal page</a>.
</p>
{% elif not is_token_correct %}
<p class="fs-5">The token you provided is incorrect, please verify that you copied it correctly.</p>
{% endif %}
{% endif %}
</div>
<p class="fs-5 text-muted">You may now close this window.</p>
</div>
{% endblock %}
......@@ -93,6 +93,11 @@ urlpatterns = [
"emails/<int:email_id>/",
include(
[
path(
"delete",
views._hx_profile_email_delete,
name="_hx_profile_email_delete",
),
path(
"make_primary",
views._hx_profile_email_mark_primary,
......@@ -104,14 +109,14 @@ urlpatterns = [
name="_hx_profile_email_toggle_valid",
),
path(
"toggle_verified",
views._hx_profile_email_toggle_verified,
name="_hx_profile_email_toggle_verified",
"request_verification",
views._hx_profile_email_request_verification,
name="_hx_profile_email_request_verification",
),
path(
"delete",
views._hx_profile_email_delete,
name="_hx_profile_email_delete",
"verify/<str:token>",
views.verify_profile_email,
name="verify_profile_email",
),
]
),
......
......@@ -5,6 +5,7 @@ __license__ = "AGPL v3"
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import UserPassesTestMixin
from django.core.exceptions import BadRequest
from django.template.response import TemplateResponse
from django.urls import reverse, reverse_lazy
from django.db import transaction
......@@ -529,12 +530,25 @@ def _hx_profile_email_toggle_valid(request, email_id):
@permission_required_htmx("scipost.can_verify_profile_emails")
def _hx_profile_email_toggle_verified(request, email_id):
def _hx_profile_email_request_verification(request, email_id):
"""Toggle verified/unverified status of ProfileEmail."""
profile_email = get_object_or_404(ProfileEmail, pk=email_id)
if request.method == "PATCH":
profile_email.verified = not profile_email.verified
profile_email.save()
if not request.method == "PATCH":
raise BadRequest("Invalid request method")
if not profile_email.verified:
profile_email.send_verification_email()
messages.success(
request,
f"Verification email sent to {profile_email.email}.",
)
else:
messages.warning(
request,
f"{profile_email.email} is already verified.",
)
return TemplateResponse(
request,
"profiles/_hx_profile_emails_table_row.html",
......@@ -542,6 +556,31 @@ def _hx_profile_email_toggle_verified(request, email_id):
)
def verify_profile_email(request, email_id, token: str):
"""Verify a ProfileEmail."""
profile_email = get_object_or_404(ProfileEmail, pk=email_id)
is_token_correct = profile_email.verification_token == token
was_previously_verified = profile_email.verified
if (
not profile_email.has_token_expired
and is_token_correct
and not was_previously_verified
):
profile_email.verified = True
profile_email.save()
return TemplateResponse(
request,
"profiles/verify_profile_email.html",
{
"profile_email": profile_email,
"is_token_correct": is_token_correct,
"was_previously_verified": was_previously_verified,
},
)
@permission_required_htmx("scipost.can_delete_profile_emails")
def _hx_profile_email_delete(request, email_id):
"""Delete ProfileEmail."""
......
<svg xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-clock-fill"
viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71z" />
</svg>
<p>Dear {{ object.profile.get_title_display }} {{ object.profile.last_name }},</p>
<p>To verify your email address, please click the following link:</p>
<p>
<a style="font-weight: bold;
font-size: 2em"
href="https://{{ domain }}{{ object.get_verification_url }}">Verify Email</a>
</p>
<p>
If the direct link is not working, you can also copy and paste the following link in your browser: https://{{ domain }}{{ object.get_verification_url }}
</p>
<p>
If you did not request this verification, it is possible that a SciPost Administrator has requested it on your behalf.
Rest assured that your email address and SciPost account are safe.
</p>
<p>
Best regards,
<br />
The SciPost Team.
</p>
{% include 'email/_footer.html' %}
{
"subject": "SciPost: Email address verification",
"recipient_list": [
"email"
],
"bcc": [
"registration@"
],
"from_name": "SciPost Registration",
"from_email": "registration@"
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment