Skip to content

Commit c6140e3

Browse files
Improve invitation UX with resend, redirects, and pending visibility.
Redirect inactive accept links to login/register with warnings, allow logged-in users to see those banners, add resend and expired-invite admin controls, and show all pending invites with status badges. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9e57f82 commit c6140e3

15 files changed

Lines changed: 595 additions & 141 deletions

File tree

routers/core/account.py

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
DataIntegrityError,
1414
Account,
1515
AccountEmail,
16+
Invitation,
1617
)
1718
from utils.core.dependencies import get_session
1819
from utils.core.models import RefreshToken
@@ -44,6 +45,7 @@
4445
get_account_from_recovery_token,
4546
get_account_from_credentials,
4647
require_unauthenticated_client,
48+
require_unauthenticated_unless_invitation_warning,
4749
get_verified_account,
4850
)
4951
from exceptions.http_exceptions import (
@@ -149,8 +151,9 @@ def logout(
149151
@router.get("/login")
150152
async def read_login(
151153
request: Request,
152-
_: None = Depends(require_unauthenticated_client),
154+
_: None = Depends(require_unauthenticated_unless_invitation_warning),
153155
invitation_token: Optional[str] = Query(None),
156+
user: Optional[User] = Depends(get_optional_user),
154157
session: Session = Depends(get_session),
155158
):
156159
"""
@@ -165,7 +168,7 @@ async def read_login(
165168
request,
166169
"account/login.html",
167170
{
168-
"user": None,
171+
"user": user,
169172
"invitation_token": invitation_token,
170173
"invitation_token_warning": invitation_token_warning,
171174
},
@@ -175,9 +178,10 @@ async def read_login(
175178
@router.get("/register")
176179
async def read_register(
177180
request: Request,
178-
_: None = Depends(require_unauthenticated_client),
181+
_: None = Depends(require_unauthenticated_unless_invitation_warning),
179182
email: Optional[EmailStr] = Query(None),
180183
invitation_token: Optional[str] = Query(None),
184+
user: Optional[User] = Depends(get_optional_user),
181185
session: Session = Depends(get_session),
182186
):
183187
"""
@@ -192,7 +196,7 @@ async def read_register(
192196
request,
193197
"account/register.html",
194198
{
195-
"user": None,
199+
"user": user,
196200
"password_pattern": HTML_PASSWORD_PATTERN,
197201
"email": email,
198202
"invitation_token": invitation_token,
@@ -289,6 +293,18 @@ async def register(
289293
"""
290294
Register a new user account, optionally processing an invitation.
291295
"""
296+
pending_invitation: Optional[Invitation] = None
297+
if invitation_token:
298+
pending_invitation = require_active_invitation_by_token(
299+
session, invitation_token
300+
)
301+
if email != pending_invitation.invitee_email:
302+
logger.warning(
303+
f"Invitation email mismatch for token {invitation_token} during registration. "
304+
f"Account: {email}, Invitation: {pending_invitation.invitee_email}"
305+
)
306+
raise InvitationEmailMismatchError()
307+
292308
# Check if the email is already registered
293309
existing_account: Optional[Account] = session.exec(
294310
select(Account).where(Account.email == email)
@@ -332,37 +348,27 @@ async def register(
332348
redirect_url = dashboard_router.url_path_for("read_dashboard")
333349

334350
# Process invitation if token is provided (BEFORE final commit)
335-
if invitation_token:
351+
if pending_invitation:
336352
logger.info(
337353
f"Registration attempt with invitation token: {invitation_token} for email {email}"
338354
)
339-
invitation = require_active_invitation_by_token(session, invitation_token)
340-
341-
# Verify email matches
342-
if email != invitation.invitee_email:
343-
logger.warning(
344-
f"Invitation email mismatch for token {invitation_token} during registration. "
345-
f"Account: {email}, Invitation: {invitation.invitee_email}"
346-
)
347-
# Consider raising a more generic error to avoid confirming email existence
348-
raise InvitationEmailMismatchError()
349355

350356
# Process the invitation (adds changes to the session)
351357
try:
352358
logger.info(
353-
f"Processing invitation {invitation.id} for new user {new_user.name} ({email}) during registration."
359+
f"Processing invitation {pending_invitation.id} for new user {new_user.name} ({email}) during registration."
354360
)
355-
process_invitation(invitation, new_user, session)
361+
process_invitation(pending_invitation, new_user, session)
356362
# Set redirect to the organization page
357363
redirect_url = org_router.url_path_for(
358-
"read_organization", org_id=invitation.organization_id
364+
"read_organization", org_id=pending_invitation.organization_id
359365
)
360366
logger.info(
361-
f"Redirecting new user {new_user.name} to organization {invitation.organization_id} after accepting invitation {invitation.id}."
367+
f"Redirecting new user {new_user.name} to organization {pending_invitation.organization_id} after accepting invitation {pending_invitation.id}."
362368
)
363369
except Exception as e:
364370
logger.error(
365-
f"Error processing invitation {invitation.id} for new user {new_user.name} ({email}) during registration: {e}",
371+
f"Error processing invitation {pending_invitation.id} for new user {new_user.name} ({email}) during registration: {e}",
366372
exc_info=True,
367373
)
368374
session.rollback()

0 commit comments

Comments
 (0)