Skip to content

fix(email): stop duplicate legacy invitation email and fix its onboarding link - #36455

Merged
mubashir1osmani merged 12 commits into
litellm_internal_stagingfrom
litellm_fix_invitation_email_link
Aug 11, 2026
Merged

fix(email): stop duplicate legacy invitation email and fix its onboarding link#36455
mubashir1osmani merged 12 commits into
litellm_internal_stagingfrom
litellm_fix_invitation_email_link

Conversation

@mubashir1osmani

@mubashir1osmani mubashir1osmani commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

TLDR

Problem this solves:

  • Invitation emails direct users to Swagger docs or the bare base URL instead of the onboarding page
  • On a v2 deployment (an enterprise email logger registered), both the modern and the legacy invitation emails fired for the same user, so the invited user got two emails, and the legacy one used the bare base URL with a "Get Started here" button
  • PROXY_BASE_URL with a trailing slash produced double-slash URLs that break routing

How it solves it:

  • Route the v2 invitation send through a single helper that reports whether any registered enterprise email logger actually delivered, then only fall back to the legacy email when v2 did not send (enterprise package absent, no email logger configured, or every send raised). This removes the duplicate on v2 deployments while keeping legacy as a real fallback for OSS
  • Fix the legacy fallback so when it does run it builds a proper /ui/onboarding?invitation_id=... link, looked up or created for the user with the trailing slash stripped before the query, and labels the button "Accept Invitation"
  • Strip trailing slashes from base_url before constructing invitation links in the v2 path

email flow:

Screenshot 2026-08-10 at 2 37 58 PM Screenshot 2026-08-10 at 2 39 29 PM Screenshot 2026-08-10 at 3 24 41 PM

(small ui fix): show icon for email fields
Screenshot 2026-08-10 at 1 25 40 PM

Relevant issues

Linear ticket

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

Will test locally with a running proxy instance to verify correct invitation link construction and logging

Type

🐛 Bug Fix

Changes

enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py (v2 path):

  1. _construct_invitation_link strips trailing slashes from base_url before concatenating
  2. logging added to trace invitation link construction and fallbacks

litellm/proxy/hooks/user_management_event_hooks.py (dispatch):

  1. v2 and legacy invitation sends no longer both fire; the v2 send runs through _send_v2_user_invitation_emails which returns whether any logger delivered, and legacy only runs as a fallback when v2 did not send

litellm/integrations/SlackAlerting/slack_alerting.py (legacy fallback):

  1. the legacy invitation email now builds /ui/onboarding?invitation_id=... (looked up or created for the user, trailing slash stripped) instead of the bare base URL

litellm/integrations/email_templates/templates.py:

  1. legacy invitation button relabeled from "Get Started here" to "Accept Invitation"

tests/test_litellm/proxy/hooks/test_send_invite_email.py:

  1. regression tests that v2 suppresses the legacy duplicate on success, and that a failing v2 send still falls back to legacy

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Problem: When PROXY_BASE_URL has a trailing slash (e.g., http://example.com/),
the invitation link becomes http://example.com//ui/onboarding?invitation_id=...
with a double slash, which can cause routing issues and direct users to the
swagger page instead of the onboarding page.

Additionally, if invitation creation fails for any reason (no user_id, prisma
unavailable, etc.), the email falls back to base_url which points to swagger
at the root path instead of the onboarding page.

Changes:
- Strip trailing slashes from base_url before constructing invitation links
- Add comprehensive debug/warning logging to trace invitation link construction
- Log when fallbacks occur so admins can diagnose why users land on swagger
- Add info-level logging for successful invitation link creation

This ensures:
1. No double-slash URLs regardless of PROXY_BASE_URL format
2. Clear visibility when invitation creation fails and why
3. Easier debugging of email delivery issues
@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR consolidates invitation-email dispatch so the legacy path is used only as a fallback, constructs onboarding invitation URLs consistently, normalizes dashboard URLs, and adds credential visibility controls to email settings.

  • Routes successful enterprise invitation delivery through a single dispatch path.
  • Builds onboarding links for modern and legacy invitation emails.
  • Normalizes login and onboarding dashboard URLs.
  • Adds regression coverage for invitation fallback behavior and UI credential controls.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py Constructs the modern invitation URL and passes it to the invitation email template without logging the invitation credential.
litellm/proxy/hooks/user_management_event_hooks.py Tracks enterprise email delivery outcomes and invokes the legacy sender only when no enterprise logger succeeds.
litellm/integrations/SlackAlerting/slack_alerting.py Adds invitation lookup or creation so legacy emails link to the onboarding flow.
litellm/proxy/proxy_server.py Normalizes login and onboarding dashboard URL construction to avoid trailing-slash inconsistencies.
ui/litellm-dashboard/src/components/email_settings.tsx Adds per-field visibility controls for credential-like email settings.

Reviews (4): Last reviewed commit: "chore: satisfy LIT002 in invitation-emai..." | Re-trigger Greptile

@veria-ai

veria-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

Add eye/eye-off toggle buttons to all email alert configuration fields
so admins can reveal masked credentials to verify values without editing

Changes:
- Add useState hook to track visibility state per field
- Add Eye/EyeOff icons from lucide-react
- Wrap input in relative container with toggle button
- Toggle input type between password and text
- Add test coverage for visibility toggle behavior
@mubashir1osmani
mubashir1osmani force-pushed the litellm_fix_invitation_email_link branch from a85a762 to 062797b Compare August 10, 2026 20:10
Replace absolute positioning with shadcn InputGroup components for
proper integration of the eye icon inside the input box

Changes:
- Use InputGroup, InputGroupInput, InputGroupAddon, InputGroupButton
- Eye icon now properly integrated inside input border
- Remove custom absolute positioning hack
- Update test to handle input-group-control data-slot selector
@mubashir1osmani
mubashir1osmani force-pushed the litellm_fix_invitation_email_link branch from bd1a8f3 to 0c93f6e Compare August 10, 2026 20:20
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...tellm/integrations/SlackAlerting/slack_alerting.py 20.00% 12 Missing ⚠️
litellm/proxy/hooks/user_management_event_hooks.py 88.23% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Logging the full invitation URL with invitation_id exposes a bearer
credential. If logs are accessible to unauthorized principals, they
could use the logged invitation_id to claim the invitation, set a
password, and hijack the account

Removed:
- Debug log showing full invitation_link with invitation_id
- Debug log showing base_url and invitation_id parameters
- Info log showing full invitation_link

Kept only:
- Info log confirming invitation creation succeeded (no credential)
- Warning logs for error paths (no credential exposure)
URLs should not have a trailing slash before the query string.
Changed from patterns like /ui/?login=success to /ui?login=success

Fixed in:
- Email invitation links (/ui/onboarding?invitation_id=...)
- SSO login redirects (/ui?login=success)
- Onboarding token links (/ui/onboarding?token=...)
@mubashir1osmani
mubashir1osmani force-pushed the litellm_fix_invitation_email_link branch from 0c93f6e to 9d9435b Compare August 10, 2026 20:40
…tion button

The email template button now explicitly uses {invitation_link} instead
of relying on {base_url} being overwritten. This ensures the Accept
Invitation button takes users to the onboarding page where they can
create their password and login
@mubashir1osmani
mubashir1osmani force-pushed the litellm_fix_invitation_email_link branch from 206bc5f to b66edf2 Compare August 10, 2026 21:13
@mubashir1osmani

Copy link
Copy Markdown
Collaborator Author

@greptile-apps

@mubashir1osmani

Copy link
Copy Markdown
Collaborator Author

@greptile-apps review this

@codspeed-hq

codspeed-hq Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_fix_invitation_email_link (4350541) with litellm_internal_staging (ade805e)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (20354bf) during the generation of this report, so ade805e was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

When a new user is created with send_invite_email=true, both the modern
enterprise (v2) email logger and the legacy v1 SlackAlerting path fired
unconditionally, so a v2 deployment sent two invitation emails: the modern
"Accept Invitation" one and the legacy "Get Started here" one built off the
bare base URL.

Route the v2 send through a single helper that returns whether any registered
enterprise email logger actually delivered, and only fall back to the legacy
email when v2 did not send (enterprise package absent, no email logger
configured, or every send raised). This keeps the legacy path as a real
fallback for OSS deployments without regressing them.

Also fix the legacy fallback itself so it is not broken when it does run: build
a proper /ui/onboarding?invitation_id=... link (looked up or created for the
user, trailing slash stripped before the query) instead of the bare base URL,
and relabel the button from "Get Started here" to "Accept Invitation".

Adds regression tests covering both that v2 suppresses the legacy duplicate on
success and that a failing v2 send still falls back to legacy.
@mubashir1osmani mubashir1osmani changed the title fix(email): handle trailing slash in PROXY_BASE_URL for invitation links fix(email): stop duplicate legacy invitation email and fix its onboarding link Aug 10, 2026
The eye-icon refactor masked every email setting field (SMTP_HOST, SMTP_PORT,
EMAIL_LOGO_URL) as a password and gave each one a show/hide toggle, so the
first "Show credential" button belonged to SMTP_HOST rather than the password.
That broke the credential-visibility unit test and hid non-secret values from
admins for no reason.

Render the masked input and the eye toggle only for sensitive keys
(PASSWORD/SECRET/KEY/TOKEN); everything else stays a plain text input. This
restores the intended behavior and makes the unit test pass.
The /login, /v2/login and onboarding routes now build the dashboard redirect
as /ui?login=success (no trailing slash before the query), matching the URL
construction fix on this branch. Update the pinned test expectations that still
asserted the old /ui/?login=success so they match the code.
The broad except in _construct_user_invitation_link is intentional: any
DB or invitation-creation failure falls back to the base URL. Annotate it so
the strict-rule budget gate does not count it as a new blind-except.
Build the enterprise email logger set as a tuple and unpack a generator into
asyncio.gather instead of list literals, and mark the two prisma find_many dict
kwargs (which the client requires as dicts) with reasoned mutable-ok.
@mubashir1osmani

Copy link
Copy Markdown
Collaborator Author

@greptile-apps

@mubashir1osmani
mubashir1osmani merged commit b0fac57 into litellm_internal_staging Aug 11, 2026
80 of 81 checks passed
@mubashir1osmani
mubashir1osmani deleted the litellm_fix_invitation_email_link branch August 11, 2026 06:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants