From 02e95a419949ccdcc8ca70cb2129c85727eb1616 Mon Sep 17 00:00:00 2001 From: Cory Hawklvelt Date: Sun, 26 Apr 2026 18:36:58 +0930 Subject: [PATCH] feat(organizations): email inviter when membership invite is accepted When a user accepts an org invite, send a notification email to the person who sent the invite with membership details (member name, email, org name, role) and an optional View Organization button. Added build_invite_accepted_html() template to email_templates.py, wired it into the accept_invite() handler, and added a test case. --- gatehouse_app/api/v1/organizations/invites.py | 24 ++++++++++ gatehouse_app/services/email_templates.py | 48 +++++++++++++++++++ test_email.py | 22 ++++++++- 3 files changed, 93 insertions(+), 1 deletion(-) diff --git a/gatehouse_app/api/v1/organizations/invites.py b/gatehouse_app/api/v1/organizations/invites.py index 762d5a2..1ac718a 100644 --- a/gatehouse_app/api/v1/organizations/invites.py +++ b/gatehouse_app/api/v1/organizations/invites.py @@ -243,6 +243,30 @@ def accept_invite(token): invite.accept() + if invite.invited_by and invite.invited_by.email: + from gatehouse_app.services.email_templates import build_invite_accepted_html + from gatehouse_app.services.notification_service import NotificationService + + member_display = user.full_name or user.email + inviter_display = invite.invited_by.full_name or invite.invited_by.email + org_link = f"{current_app.config.get('APP_URL', '')}/organizations/{invite.organization_id}" + + html_body = build_invite_accepted_html( + inviter_name=inviter_display, + member_name=member_display, + member_email=user.email, + org_name=invite.organization.name, + role=invite.role, + org_link=org_link, + ) + + NotificationService._send_email_async( + to_address=invite.invited_by.email, + subject=f"{member_display} accepted your invitation to {invite.organization.name}", + body=f"{member_display} has accepted your invitation to join {invite.organization.name} on Secuird.", + html_body=html_body, + ) + has_webauthn = user.has_webauthn_enabled() has_totp = user.has_totp_enabled() diff --git a/gatehouse_app/services/email_templates.py b/gatehouse_app/services/email_templates.py index ec4d81d..4e6fc9c 100644 --- a/gatehouse_app/services/email_templates.py +++ b/gatehouse_app/services/email_templates.py @@ -562,3 +562,51 @@ def build_contact_enquiry_html(

{message_display}

''' return get_base_html(content, f"Secuird Website: {type_label}", f"New {type_label} from {submitter_email}") + + +def build_invite_accepted_html( + inviter_name: str, + member_name: str, + member_email: str, + org_name: str, + role: str, + org_link: Optional[str] = None, +) -> str: + """Build invite accepted notification email. + + Args: + inviter_name: Name of the person who sent the invite + member_name: Name of the person who accepted + member_email: Email of the person who accepted + org_name: Organization name + role: Role assigned to the member + org_link: Optional link to view the organization + + Returns: + HTML email string + """ + content = f''' +

Invitation Accepted

+

+ {member_name} has accepted your invitation to join {org_name} on Secuird. +

+ {get_alert_box(f"{member_name} ({member_email}) has joined {org_name}", "success", "✅")} + + + + +
+

Membership Details

+ + {get_detail_row("Member", member_name)} + {get_detail_row("Email", member_email)} + {get_detail_row("Organization", org_name)} + {get_detail_row("Role", role)} +
+
+ ''' + if org_link: + content += get_action_button(org_link, "View Organization", PRIMARY_COLOR) + + return get_base_html(content, f"Invitation accepted: {org_name}", f"{member_name} has joined {org_name}") + diff --git a/test_email.py b/test_email.py index 6061a5c..25bb98f 100644 --- a/test_email.py +++ b/test_email.py @@ -148,8 +148,28 @@ def test_html_email(): success = provider.send(message) print(f"Result: {'✅ SUCCESS' if success else '❌ FAILED'}") + # Test 8: Invite Accepted + print("\n--- Test 8: Invite Accepted ---") + html_body = email_templates.build_invite_accepted_html( + inviter_name="Admin User", + member_name="New Member", + member_email="newmember@example.com", + org_name="Acme Corporation", + role="Member", + org_link="https://secuird.tech/organizations/org-123", + ) + message = EmailMessage( + to="cory@hawkvelt.id.au", + subject="New Member accepted your invitation to Acme Corporation", + body="Plain text version: New Member has accepted your invitation.", + html_body=html_body, + from_address="Secuird ", + ) + success = provider.send(message) + print(f"Result: {'✅ SUCCESS' if success else '❌ FAILED'}") + print("\n" + "=" * 50) - print("All 7 email templates sent!") + print("All 8 email templates sent!") print("=" * 50)