Skip to content

Commit 8243f05

Browse files
Add hx-confirm for destructive org actions and standardize invitation exceptions.
Mirror formettle PR #101 follow-up: require confirmation before cancelling invitations or removing members, and use InsufficientPermissionsError and RoleNotFoundError in the invitation router. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent cdfea6d commit 8243f05

6 files changed

Lines changed: 30 additions & 13 deletions

File tree

routers/core/invitation.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
InvitationEmailSendError,
2626
InvalidInvitationTokenError,
2727
InvitationNotFoundError,
28+
InsufficientPermissionsError,
29+
RoleNotFoundError,
2830
)
2931
from exceptions.exceptions import EmailSendFailedError
3032
from utils.core.htmx import is_htmx_request, append_toast
@@ -83,15 +85,12 @@ async def create_invitation(
8385

8486
# Check if the current user has permission to invite users to this organization
8587
if not current_user.has_permission(ValidPermissions.INVITE_USER, organization):
86-
raise HTTPException(
87-
status_code=403,
88-
detail="You don't have permission to invite users to this organization",
89-
)
88+
raise InsufficientPermissionsError()
9089

9190
# Verify the role exists and belongs to this organization
9291
role = session.get(Role, role_id)
9392
if not role:
94-
raise HTTPException(status_code=404, detail="Role not found")
93+
raise RoleNotFoundError()
9594
if role.organization_id != organization_id:
9695
raise InvalidRoleForOrganizationError()
9796

@@ -205,10 +204,7 @@ async def delete_invitation(
205204
raise OrganizationNotFoundError()
206205

207206
if not current_user.has_permission(ValidPermissions.INVITE_USER, organization):
208-
raise HTTPException(
209-
status_code=403,
210-
detail="You don't have permission to manage invitations for this organization",
211-
)
207+
raise InsufficientPermissionsError()
212208

213209
invitation = session.get(Invitation, invitation_id)
214210
if not invitation or invitation.organization_id != organization_id:

templates/organization/modals/members_card.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
<form method="POST" action="{{ url_for('remove_user_from_organization') }}" class="d-inline"
6060
hx-post="{{ url_for('remove_user_from_organization') }}"
6161
hx-target="#members-card-content"
62-
hx-swap="innerHTML">
62+
hx-swap="innerHTML"
63+
hx-confirm="Remove {{ member.name }} from this organization?">
6364
<input type="hidden" name="user_id" value="{{ member.id }}">
6465
<input type="hidden" name="organization_id" value="{{ organization.id }}">
6566
<button type="submit" class="btn btn-sm btn-outline-danger" {% if member.id == user.id %}disabled{% endif %}>

templates/organization/partials/invitations_list.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
<form method="POST" action="{{ url_for('delete_invitation') }}" class="invitation-cancel-form mb-0"
1010
hx-post="{{ url_for('delete_invitation') }}"
1111
hx-target="#members-card-content"
12-
hx-swap="innerHTML">
12+
hx-swap="innerHTML"
13+
hx-confirm="Cancel invitation for {{ inv.invitee_email }}?">
1314
<input type="hidden" name="invitation_id" value="{{ inv.id }}">
1415
<input type="hidden" name="organization_id" value="{{ organization.id }}">
1516
<button type="submit" class="btn btn-sm btn-outline-danger">Cancel</button>

templates/organization/partials/member_row.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
<form method="POST" action="{{ url_for('remove_user_from_organization') }}" class="d-inline"
3030
hx-post="{{ url_for('remove_user_from_organization') }}"
3131
hx-target="#members-card-content"
32-
hx-swap="innerHTML">
32+
hx-swap="innerHTML"
33+
hx-confirm="Remove {{ member.name }} from this organization?">
3334
<input type="hidden" name="user_id" value="{{ member.id }}">
3435
<input type="hidden" name="organization_id" value="{{ organization.id }}">
3536
<button type="submit" class="btn btn-sm btn-outline-danger" {% if member.id == user.id %}disabled{% endif %}>

templates/organization/partials/members_table.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
<form method="POST" action="{{ url_for('remove_user_from_organization') }}" class="d-inline"
4848
hx-post="{{ url_for('remove_user_from_organization') }}"
4949
hx-target="#members-card-content"
50-
hx-swap="innerHTML">
50+
hx-swap="innerHTML"
51+
hx-confirm="Remove {{ member.name }} from this organization?">
5152
<input type="hidden" name="user_id" value="{{ member.id }}">
5253
<input type="hidden" name="organization_id" value="{{ organization.id }}">
5354
<button type="submit" class="btn btn-sm btn-outline-danger" {% if member.id == user.id %}disabled{% endif %}>

tests/test_templates.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@ def test_invite_member_form_has_hx_post():
9999
assert "hx-post" in content
100100

101101

102+
def test_pending_invitations_include_cancel_confirm():
103+
content = Path("templates/organization/partials/invitations_list.html").read_text()
104+
assert "url_for('delete_invitation')" in content
105+
assert "hx-confirm" in content
106+
107+
108+
def test_remove_member_forms_include_confirm():
109+
for path in (
110+
"templates/organization/modals/members_card.html",
111+
"templates/organization/partials/members_table.html",
112+
"templates/organization/partials/member_row.html",
113+
):
114+
content = Path(path).read_text()
115+
assert "url_for('remove_user_from_organization')" in content
116+
assert "hx-confirm" in content
117+
118+
102119
def test_edit_organization_form_has_hx_post():
103120
content = Path(
104121
"templates/organization/modals/edit_organization_modal.html"

0 commit comments

Comments
 (0)