Skip to content

Commit 905a3fb

Browse files
committed
Restrict org admin invitation actions to users with confirmed emails
1 parent 7fb86dc commit 905a3fb

7 files changed

Lines changed: 79 additions & 25 deletions

File tree

frontend/css/gps.css

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ strong {
442442

443443
.empty {
444444
display: flex;
445+
margin: 0 auto;
445446
width: 16.5rem;
446447
padding: 1.5rem;
447448
flex-direction: column;
@@ -453,12 +454,18 @@ strong {
453454
border: 1.282px solid var(--Gray-2, #d8dee2);
454455
}
455456

457+
.empty--no-border {
458+
border: none;
459+
}
460+
456461
.empty p {
457462
align-self: stretch;
458-
margin: 0;
463+
font-family: "Source Sans Pro";
464+
line-height: 1.4;
459465
color: var(--gray-4);
466+
margin: 0 auto;
460467
text-align: center;
461-
font-family: "Source Sans Pro";
468+
max-width: 60ch;
462469
}
463470

464471
.card {

squarelet/organizations/models/invitation.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ class Meta:
103103
def __str__(self):
104104
return f"Invitation: {self.uuid}"
105105

106+
@property
107+
def is_admin_role(self):
108+
return self.role == InvitationRole.admin
109+
106110
def send(self):
107111

108112
if self.request:

squarelet/organizations/tests/test_views.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,13 @@ class TestInvitationAccept(ViewTestMixin):
16411641
view = views.InvitationAccept
16421642
url = "/organizations/{uuid}/invitation/"
16431643

1644+
def _create_verified_email(self, user):
1645+
EmailAddress.objects.update_or_create(
1646+
user=user,
1647+
email=user.email,
1648+
defaults={"verified": True, "primary": True},
1649+
)
1650+
16441651
def test_accept(self, rf, invitation_factory, user_factory):
16451652
invitation = invitation_factory()
16461653
user = user_factory()
@@ -1649,6 +1656,30 @@ def test_accept(self, rf, invitation_factory, user_factory):
16491656
assert invitation.accepted_at is not None
16501657
assert invitation.organization.has_member(user)
16511658

1659+
def test_accept_admin_requires_verified_email(
1660+
self, rf, invitation_factory, user_factory
1661+
):
1662+
"""Users without a verified email cannot accept admin invitations"""
1663+
invitation = invitation_factory(role=InvitationRole.admin)
1664+
user = user_factory()
1665+
response = self.call_view(rf, user, {"action": "accept"}, uuid=invitation.uuid)
1666+
invitation.refresh_from_db()
1667+
assert invitation.accepted_at is None
1668+
assert not invitation.organization.has_member(user)
1669+
assert response.status_code == 302
1670+
1671+
def test_accept_admin_with_verified_email(
1672+
self, rf, invitation_factory, user_factory
1673+
):
1674+
"""Users with a verified email can accept admin invitations"""
1675+
invitation = invitation_factory(role=InvitationRole.admin)
1676+
user = user_factory()
1677+
self._create_verified_email(user)
1678+
self.call_view(rf, user, {"action": "accept"}, uuid=invitation.uuid)
1679+
invitation.refresh_from_db()
1680+
assert invitation.accepted_at is not None
1681+
assert invitation.organization.has_member(user)
1682+
16521683
def test_reject(self, rf, invitation_factory, user_factory):
16531684
invitation = invitation_factory()
16541685
user = user_factory()

squarelet/organizations/views/members.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,16 @@ def post(self, request, *args, **kwargs):
280280
invitation = self.get_object()
281281
action = request.POST.get("action")
282282
if action == "accept":
283+
if (
284+
invitation.role == InvitationRole.admin
285+
and not request.user.emailaddress_set.filter(verified=True).exists()
286+
):
287+
messages.error(
288+
request,
289+
"You must verify your email address before accepting "
290+
"an admin invitation.",
291+
)
292+
return redirect("organizations:invitation", uuid=invitation.uuid)
283293
invitation.accept(request.user)
284294
messages.success(request, "Invitation accepted")
285295

squarelet/templates/organizations/invitation_detail.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{% load static %}
33
{% load thumbnail %}
44
{% load i18n %}
5+
{% load email_verification %}
56

67
{% block title %}Organization: {{ invitation.organization.name }}{% endblock %}
78

@@ -52,6 +53,13 @@ <h2>
5253
out and sign back in with the account you'd like to use.
5354
{% endblocktrans %}
5455
</p>
56+
{% if invitation.is_admin_role and not request.user|has_verified_email %}
57+
<p>
58+
{% blocktrans %}
59+
You must verify your email address before you can accept an admin invitation.
60+
{% endblocktrans %}
61+
</p>
62+
{% endif %}
5563
{% else %}
5664
<p>
5765
{% blocktrans %}
@@ -75,6 +83,17 @@ <h2>
7583
{% endif %}
7684

7785
{% if request.user.is_authenticated %}
86+
{% if invitation.is_admin_role and not request.user|has_verified_email %}
87+
<div class="control-group">
88+
<form class="email-actions" action="{% url "account_email" %}" method="post">
89+
{% csrf_token %}
90+
<input type="hidden" name="email" value="{{request.user.email}}" />
91+
<button class="button primary" type="submit" name="action_send">
92+
{% trans "Verify your email address" %}
93+
</button>
94+
</form>
95+
</div>
96+
{% else %}
7897
<div class="control-group">
7998
<button class="button primary" type="submit" name="action" value="accept">
8099
{% trans "Accept" %}
@@ -84,6 +103,7 @@ <h2>
84103
</button>
85104
</div>
86105
</div>
106+
{% endif %}
87107
{% else %}
88108
<div class="control-group">
89109
<a href="{% url "account_signup" %}?next={{request.path}}" class="button primary">

squarelet/templates/organizations/organization_list.html

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends "base.html" %}
2-
{% load static i18n avatar django_vite email_verification %}
2+
{% load static i18n avatar django_vite %}
33

44
{% block title %}Organizations{% endblock %}
55

@@ -102,23 +102,6 @@ <h2>{% trans "Your organizations" %}</h2>
102102
</section>
103103

104104
<section class="invites">
105-
{% if not request.user|has_verified_email %}
106-
<div class="empty">
107-
<p>
108-
{% blocktrans %}
109-
You must verify your email address before you can send requests
110-
or receive invitations to join an organization.
111-
{% endblocktrans %}
112-
</p>
113-
<form class="email-actions" action="{% url "account_email" %}" method="post">
114-
{% csrf_token %}
115-
<input type="hidden" name="email" value="{{user.email}}" />
116-
<button class="btn primary small" type="submit" name="action_send">
117-
{% trans "Confirm your email address" %}
118-
</button>
119-
</form>
120-
</div>
121-
{% else %}
122105
{% if potential_orgs %}
123106
<div id="pre-approved" class="pre-approved">
124107
<header>
@@ -213,7 +196,6 @@ <h2>{% trans "Pending requests" %}</h2>
213196
</div>
214197
{% endif %}
215198
</div>
216-
{% endif %}
217199
</section>
218200
</main>
219201
</article>

squarelet/templates/organizations/organization_managemembers.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ <h2>
144144
</form>
145145
</div>
146146
{% empty %}
147-
<div class="empty">
148-
<h3>No pending requests</h3>
147+
<div class="empty empty--no-border">
148+
<p>No pending requests</p>
149149
</div>
150150
{% endfor %}
151151
</div>
@@ -207,8 +207,8 @@ <h2>
207207
</div>
208208
</div>
209209
{% empty %}
210-
<div class="empty">
211-
<h3>No pending invitations</h3>
210+
<div class="empty empty--no-border">
211+
<p>No pending invitations</p>
212212
</div>
213213
{% endfor %}
214214
</div>

0 commit comments

Comments
 (0)