Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1e7a58e
fix
giancarloromeo Apr 17, 2026
96ae0a4
update message
giancarloromeo Apr 17, 2026
117d8c7
fix
giancarloromeo Apr 17, 2026
a09c0ef
fix
giancarloromeo Apr 17, 2026
cdd0b50
fix
giancarloromeo Apr 17, 2026
a800fe4
fix
giancarloromeo Apr 17, 2026
df96e48
update
giancarloromeo Apr 17, 2026
6a9232a
fix
giancarloromeo Apr 17, 2026
20a6b63
fix
giancarloromeo Apr 17, 2026
98c389e
update msg
giancarloromeo Apr 17, 2026
721cecb
fix
giancarloromeo Apr 17, 2026
d55b1ec
fix
giancarloromeo Apr 17, 2026
80e4030
fix
giancarloromeo Apr 17, 2026
60f1cbc
fix
giancarloromeo Apr 17, 2026
ec5e75c
fix
giancarloromeo Apr 17, 2026
e100e85
fix
giancarloromeo Apr 17, 2026
45e0c6b
fic
giancarloromeo Apr 17, 2026
7aed2a9
update
giancarloromeo Apr 17, 2026
b4a3633
update
giancarloromeo Apr 17, 2026
2c7dd8e
fix
giancarloromeo Apr 17, 2026
94789df
fix
giancarloromeo Apr 17, 2026
22d3e1a
Merge branch 'feat/targeted-wrong-password-msg-merged-accounts' of gi…
giancarloromeo Apr 17, 2026
be5fc21
add fixtures
giancarloromeo Apr 17, 2026
f19c781
fix
giancarloromeo Apr 17, 2026
596bcb4
fix
giancarloromeo Apr 17, 2026
0e949b6
fix
giancarloromeo Apr 17, 2026
ae63a8c
fix
giancarloromeo Apr 17, 2026
e02103f
fix
giancarloromeo Apr 17, 2026
04d6ddb
fix
giancarloromeo Apr 17, 2026
0b3ea93
Merge branch 'master' into feat/targeted-wrong-password-msg-merged-ac…
giancarloromeo Apr 20, 2026
261967d
rename
giancarloromeo Apr 21, 2026
e1ba853
add TIP:
giancarloromeo Apr 21, 2026
6c97670
fix
giancarloromeo Apr 21, 2026
71cffca
Merge branch 'master' into feat/targeted-wrong-password-msg-merged-ac…
giancarloromeo Apr 21, 2026
2a06993
fix
giancarloromeo Apr 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ class Vendor(TypedDict, total=False):
company_name: str
company_address: str
company_links: list[tuple[str, str]] # list of (link_name, link_url)
marketing_fallback_products_on_wrong_password: (
list[str] # list of product names to check (in order); on wrong password, if the user has an account
# in any of these products, suggest using the password from the first matching product
# (accounts were merged/unified across platforms)
)


class IssueTracker(TypedDict, total=True):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import logging

from aiohttp import web
from common_library.logging.logging_errors import create_troubleshooting_log_kwargs
from common_library.user_messages import user_message
from servicelib.aiohttp import status
from servicelib.aiohttp.rest_middlewares import handle_aiohttp_web_http_error
Expand All @@ -9,19 +12,29 @@
exception_handling_decorator,
to_exceptions_handlers_map,
)
from ....groups import api as groups_service
from ....products import products_service, products_web
from ....products.errors import ProductNotFoundError
from ....users.exceptions import AlreadyPreRegisteredError
from ...constants import MSG_2FA_UNAVAILABLE, MSG_WRONG_PASSWORD
from ...constants import (
MSG_2FA_UNAVAILABLE,
MSG_WRONG_PASSWORD,
MSG_WRONG_PASSWORD_MERGED_ACCOUNTS,
)
from ...errors import (
SendingVerificationEmailError,
SendingVerificationSmsError,
WrongPasswordError,
)

_logger = logging.getLogger(__name__)

_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {
AlreadyPreRegisteredError: HttpErrorInfo(
status.HTTP_409_CONFLICT,
user_message(
"An account for the email {email} has been submitted. If you haven't received any updates, please contact support.",
"An account for the email {email} has been submitted. "
"If you haven't received any updates, please contact support.",
_version=1,
),
),
Expand All @@ -36,6 +49,45 @@
}


async def _try_show_login_fallbacks_on_wrong_password(
app: web.Application, *, user_id: int, product_name: str
) -> str | None:
"""Returns the suggested product display name when a login tip should be shown.

Checks if the current product has ``marketing_fallback_products_on_wrong_password``
configured with a list of product names. If the user belongs to any of those
products, returns the display name of the matching product.
"""
try:
current_product = products_service.get_product(app, product_name=product_name)
vendor = current_product.vendor or {}
tip_products: list[str] = vendor.get("marketing_fallback_products_on_wrong_password", [])
if not tip_products:
return None

for check_product_name in tip_products:
try:
check_product = products_service.get_product(app, product_name=check_product_name)
except ProductNotFoundError:
continue
if check_product.group_id is not None and await groups_service.is_user_in_group(
app, user_id=user_id, group_id=check_product.group_id
):
return check_product.display_name
except Exception as exc: # pylint: disable=broad-except
_logger.exception(
**create_troubleshooting_log_kwargs(
"Unexpected error checking fallback products for wrong password",
error=exc,
error_context={
"user_id": user_id,
"product_name": product_name,
},
)
)
return None


async def _handle_legacy_error_response(request: web.Request, exception: Exception):
"""
This handlers keeps compatibility with error responses that include deprecated
Expand All @@ -47,9 +99,20 @@ async def _handle_legacy_error_response(request: web.Request, exception: Excepti
exception, WrongPasswordError
), f"Expected WrongPasswordError, got {type(exception)}"

user_id = exception.error_context().get("user_id")
assert user_id is not None, "user_id must be present in error context" # nosec

msg = MSG_WRONG_PASSWORD
product_name = products_web.get_product_name(request)
suggested_product = await _try_show_login_fallbacks_on_wrong_password(
request.app, user_id=user_id, product_name=product_name
)
if suggested_product:
msg = MSG_WRONG_PASSWORD_MERGED_ACCOUNTS.format(suggested_product=suggested_product)

Comment thread
giancarloromeo marked this conversation as resolved.
return handle_aiohttp_web_http_error(
request=request,
exception=web.HTTPUnauthorized(text=MSG_WRONG_PASSWORD),
exception=web.HTTPUnauthorized(text=msg),
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
MSG_LOGGED_IN: Final[str] = user_message("You have successfully signed in.", _version=1)
MSG_LOGGED_OUT: Final[str] = user_message("You have successfully signed out.", _version=1)
MSG_OFTEN_RESET_PASSWORD: Final[str] = user_message(
"You've recently requested a password reset. Please check your email for the reset link or wait before requesting another one.",
"You've recently requested a password reset. "
"Please check your email for the reset link or wait before requesting another one.",
_version=1,
)
MSG_PASSWORD_CHANGE_NOT_ALLOWED: Final[str] = user_message(
Expand Down Expand Up @@ -63,19 +64,22 @@
)
MSG_UNKNOWN_EMAIL: Final[str] = user_message("This email address is not registered.", _version=1)
MSG_USER_DELETED: Final[str] = user_message(
"This account is scheduled for deletion. To reactivate it or for more information, please contact support: {support_email}",
"This account is scheduled for deletion. "
"To reactivate it or for more information, please contact support: {support_email}",
_version=1,
)
MSG_USER_BANNED: Final[str] = user_message(
"Access to this account is no longer available. Please contact support for more information: {support_email}",
_version=1,
)
MSG_USER_EXPIRED: Final[str] = user_message(
"This account has expired and access is no longer available. Please contact support for assistance: {support_email}",
"This account has expired and access is no longer available. "
"Please contact support for assistance: {support_email}",
_version=1,
)
MSG_USER_DISABLED: Final[str] = user_message(
"This account has been disabled and cannot be registered again. Please contact support for details: {support_email}",
"This account has been disabled and cannot be registered again. "
"Please contact support for details: {support_email}",
_version=1,
)
MSG_WRONG_2FA_CODE__INVALID: Final[str] = user_message(
Expand All @@ -91,6 +95,12 @@
"The password does not match the one associated with this email address. Please try again.",
_version=3,
)
MSG_WRONG_PASSWORD_MERGED_ACCOUNTS: Final[str] = user_message(
"The password does not match the one associated with this email address."
"TIP: Your accounts across multiple platforms have been unified into a single login."
" Please try using the password from your {suggested_product} account.",
_version=1,
)
MSG_WEAK_PASSWORD: Final[str] = user_message(
"Your password must contain at least {LOGIN_PASSWORD_MIN_LENGTH} characters for security.",
_version=2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ class SendingVerificationEmailError(LoginError):


class WrongPasswordError(LoginError):
msg_template = "Invalid password provided"
msg_template = "Invalid password provided for user {user_id}"
Loading
Loading