|
46 | 46 | get_account_from_recovery_token, |
47 | 47 | get_account_from_credentials, |
48 | 48 | require_unauthenticated_client, |
| 49 | + require_unauthenticated_unless_invitation_warning, |
49 | 50 | get_verified_account, |
50 | 51 | ) |
51 | 52 | from exceptions.http_exceptions import ( |
|
55 | 56 | EmailNotVerifiedError, |
56 | 57 | MaxEmailsReachedError, |
57 | 58 | PasswordValidationError, |
58 | | - InvalidInvitationTokenError, |
59 | 59 | InvitationEmailMismatchError, |
60 | 60 | InvitationProcessingError, |
61 | 61 | ) |
62 | 62 | from routers.core.dashboard import router as dashboard_router |
63 | 63 | from routers.core.user import router as user_router |
64 | 64 | from routers.core.organization import router as org_router |
65 | | -from utils.core.invitations import process_invitation |
| 65 | +from utils.core.invitations import ( |
| 66 | + process_invitation, |
| 67 | + require_active_invitation_by_token, |
| 68 | + get_invitation_token_warning, |
| 69 | +) |
66 | 70 | from utils.core.rate_limit import ( |
67 | 71 | check_login_ip_rate_limit, |
68 | 72 | check_login_email_rate_limit, |
@@ -152,38 +156,57 @@ def logout( |
152 | 156 | @router.get("/login") |
153 | 157 | async def read_login( |
154 | 158 | request: Request, |
155 | | - _: None = Depends(require_unauthenticated_client), |
| 159 | + _: None = Depends(require_unauthenticated_unless_invitation_warning), |
156 | 160 | invitation_token: Optional[str] = Query(None), |
| 161 | + user: Optional[User] = Depends(get_optional_user), |
| 162 | + session: Session = Depends(get_session), |
157 | 163 | ): |
158 | 164 | """ |
159 | 165 | Render login page or redirect to dashboard if already logged in. |
160 | 166 | """ |
| 167 | + invitation_token_warning = ( |
| 168 | + get_invitation_token_warning(session, invitation_token) |
| 169 | + if invitation_token |
| 170 | + else None |
| 171 | + ) |
161 | 172 | return templates.TemplateResponse( |
162 | 173 | request, |
163 | 174 | "account/login.html", |
164 | | - {"user": None, "invitation_token": invitation_token}, |
| 175 | + { |
| 176 | + "user": user, |
| 177 | + "invitation_token": invitation_token, |
| 178 | + "invitation_token_warning": invitation_token_warning, |
| 179 | + }, |
165 | 180 | ) |
166 | 181 |
|
167 | 182 |
|
168 | 183 | @router.get("/register") |
169 | 184 | async def read_register( |
170 | 185 | request: Request, |
171 | | - _: None = Depends(require_unauthenticated_client), |
| 186 | + _: None = Depends(require_unauthenticated_unless_invitation_warning), |
172 | 187 | email: Optional[EmailStr] = Query(None), |
173 | 188 | invitation_token: Optional[str] = Query(None), |
| 189 | + user: Optional[User] = Depends(get_optional_user), |
| 190 | + session: Session = Depends(get_session), |
174 | 191 | ): |
175 | 192 | """ |
176 | 193 | Render registration page or redirect to dashboard if already logged in. |
177 | 194 | """ |
| 195 | + invitation_token_warning = ( |
| 196 | + get_invitation_token_warning(session, invitation_token) |
| 197 | + if invitation_token |
| 198 | + else None |
| 199 | + ) |
178 | 200 | return templates.TemplateResponse( |
179 | 201 | request, |
180 | 202 | "account/register.html", |
181 | 203 | { |
182 | | - "user": None, |
| 204 | + "user": user, |
183 | 205 | "password_pattern": HTML_PASSWORD_PATTERN, |
184 | 206 | "email": email, |
185 | 207 | "invitation_token": invitation_token, |
186 | 208 | "host_name": os.getenv("HOST_NAME", "our platform"), |
| 209 | + "invitation_token_warning": invitation_token_warning, |
187 | 210 | }, |
188 | 211 | ) |
189 | 212 |
|
@@ -279,6 +302,18 @@ async def register( |
279 | 302 | """ |
280 | 303 | Register a new user account, optionally processing an invitation. |
281 | 304 | """ |
| 305 | + pending_invitation: Optional[Invitation] = None |
| 306 | + if invitation_token: |
| 307 | + pending_invitation = require_active_invitation_by_token( |
| 308 | + session, invitation_token |
| 309 | + ) |
| 310 | + if email != pending_invitation.invitee_email: |
| 311 | + logger.warning( |
| 312 | + f"Invitation email mismatch for token {invitation_token} during registration. " |
| 313 | + f"Account: {email}, Invitation: {pending_invitation.invitee_email}" |
| 314 | + ) |
| 315 | + raise InvitationEmailMismatchError() |
| 316 | + |
282 | 317 | # Check if the email is already registered |
283 | 318 | existing_account: Optional[Account] = session.exec( |
284 | 319 | select(Account).where(Account.email == email) |
@@ -326,46 +361,27 @@ async def register( |
326 | 361 | redirect_url = dashboard_router.url_path_for("read_dashboard") |
327 | 362 |
|
328 | 363 | # Process invitation if token is provided (BEFORE final commit) |
329 | | - if invitation_token: |
| 364 | + if pending_invitation: |
330 | 365 | logger.info( |
331 | 366 | f"Registration attempt with invitation token: {invitation_token} for email {email}" |
332 | 367 | ) |
333 | | - # Fetch the invitation |
334 | | - statement = select(Invitation).where(Invitation.token == invitation_token) |
335 | | - invitation = session.exec(statement).first() |
336 | | - |
337 | | - if not invitation or not invitation.is_active(): |
338 | | - logger.warning( |
339 | | - f"Invalid or inactive invitation token provided during registration: {invitation_token}" |
340 | | - ) |
341 | | - # Consider raising a more generic error to avoid exposing token validity |
342 | | - raise InvalidInvitationTokenError() |
343 | | - |
344 | | - # Verify email matches |
345 | | - if email != invitation.invitee_email: |
346 | | - logger.warning( |
347 | | - f"Invitation email mismatch for token {invitation_token} during registration. " |
348 | | - f"Account: {email}, Invitation: {invitation.invitee_email}" |
349 | | - ) |
350 | | - # Consider raising a more generic error to avoid confirming email existence |
351 | | - raise InvitationEmailMismatchError() |
352 | 368 |
|
353 | 369 | # Process the invitation (adds changes to the session) |
354 | 370 | try: |
355 | 371 | logger.info( |
356 | | - f"Processing invitation {invitation.id} for new user {new_user.name} ({email}) during registration." |
| 372 | + f"Processing invitation {pending_invitation.id} for new user {new_user.name} ({email}) during registration." |
357 | 373 | ) |
358 | | - process_invitation(invitation, new_user, session) |
| 374 | + process_invitation(pending_invitation, new_user, session) |
359 | 375 | # Set redirect to the organization page |
360 | 376 | redirect_url = org_router.url_path_for( |
361 | | - "read_organization", org_id=invitation.organization_id |
| 377 | + "read_organization", org_id=pending_invitation.organization_id |
362 | 378 | ) |
363 | 379 | logger.info( |
364 | | - f"Redirecting new user {new_user.name} to organization {invitation.organization_id} after accepting invitation {invitation.id}." |
| 380 | + f"Redirecting new user {new_user.name} to organization {pending_invitation.organization_id} after accepting invitation {pending_invitation.id}." |
365 | 381 | ) |
366 | 382 | except Exception as e: |
367 | 383 | logger.error( |
368 | | - f"Error processing invitation {invitation.id} for new user {new_user.name} ({email}) during registration: {e}", |
| 384 | + f"Error processing invitation {pending_invitation.id} for new user {new_user.name} ({email}) during registration: {e}", |
369 | 385 | exc_info=True, |
370 | 386 | ) |
371 | 387 | session.rollback() |
@@ -447,15 +463,7 @@ async def login( |
447 | 463 | logger.info( |
448 | 464 | f"Login attempt with invitation token: {invitation_token} for account {account.email}" |
449 | 465 | ) |
450 | | - # Fetch the invitation |
451 | | - statement = select(Invitation).where(Invitation.token == invitation_token) |
452 | | - invitation = session.exec(statement).first() |
453 | | - |
454 | | - if not invitation or not invitation.is_active(): |
455 | | - logger.warning( |
456 | | - f"Invalid or inactive invitation token provided during login: {invitation_token}" |
457 | | - ) |
458 | | - raise InvalidInvitationTokenError() |
| 466 | + invitation = require_active_invitation_by_token(session, invitation_token) |
459 | 467 |
|
460 | 468 | # Verify email matches (check primary and any verified secondary emails) |
461 | 469 | account_emails = session.exec( |
|
0 commit comments