From ffef3956dbb2d36190eae5549533db5be012fada Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:38:30 +0200 Subject: [PATCH 01/19] moving pre-registration --- .../pytest_simcore/helpers/webserver_login.py | 2 +- .../login/_auth_service.py | 2 +- .../_controller/rest/_rest_exceptions.py | 2 +- .../login/_controller/rest/auth.py | 4 +-- .../login/_controller/rest/change.py | 16 +++++----- .../login/_controller/rest/confirmation.py | 10 +++---- .../login/_controller/rest/registration.py | 18 +++++------ .../login/_controller/rest/twofa.py | 8 ++--- .../login/_invitations_service.py | 10 +++---- .../login/_login_service.py | 2 +- .../login/_models.py | 2 +- .../login/_security_service.py | 2 +- .../login/{_constants.py => constants.py} | 0 .../simcore_service_webserver/login/plugin.py | 4 +-- .../login/settings.py | 2 +- .../login_account/__init__.py | 3 ++ .../login_account/_controller/__init__.py | 0 .../_controller/rest/__init__.py | 0 .../_controller/rest/preregistration.py | 30 +++++++------------ .../_preregistration_service.py | 0 .../unit/with_dbs/03/login/test_login_auth.py | 2 +- .../03/login/test_login_change_email.py | 2 +- .../03/login/test_login_change_password.py | 2 +- .../03/login/test_login_preregistration.py | 2 +- .../03/login/test_login_registration.py | 4 +-- .../03/login/test_login_reset_password.py | 10 +++---- .../with_dbs/03/login/test_login_twofa.py | 10 +++---- .../03/login/test_login_twofa_resend.py | 2 +- .../tests/unit/with_dbs/03/test_email.py | 4 +-- .../03/test_session_access_policies.py | 2 +- 30 files changed, 76 insertions(+), 81 deletions(-) rename services/web/server/src/simcore_service_webserver/login/{_constants.py => constants.py} (100%) create mode 100644 services/web/server/src/simcore_service_webserver/login_account/__init__.py create mode 100644 services/web/server/src/simcore_service_webserver/login_account/_controller/__init__.py create mode 100644 services/web/server/src/simcore_service_webserver/login_account/_controller/rest/__init__.py rename services/web/server/src/simcore_service_webserver/{login => login_account}/_controller/rest/preregistration.py (88%) rename services/web/server/src/simcore_service_webserver/{login => login_account}/_preregistration_service.py (100%) diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/webserver_login.py b/packages/pytest-simcore/src/pytest_simcore/helpers/webserver_login.py index 147018fa2c7..6d843ceacdf 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/webserver_login.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/webserver_login.py @@ -5,11 +5,11 @@ from aiohttp.test_utils import TestClient from servicelib.aiohttp import status -from simcore_service_webserver.login._constants import MSG_LOGGED_IN from simcore_service_webserver.login._invitations_service import create_invitation_token from simcore_service_webserver.login._login_repository_legacy import ( get_plugin_storage, ) +from simcore_service_webserver.login.constants import MSG_LOGGED_IN from simcore_service_webserver.security import security_service from yarl import URL diff --git a/services/web/server/src/simcore_service_webserver/login/_auth_service.py b/services/web/server/src/simcore_service_webserver/login/_auth_service.py index 99d1d4698f5..ef0ba489338 100644 --- a/services/web/server/src/simcore_service_webserver/login/_auth_service.py +++ b/services/web/server/src/simcore_service_webserver/login/_auth_service.py @@ -12,8 +12,8 @@ from ..products.models import Product from ..security import security_service from . import _login_service -from ._constants import MSG_UNKNOWN_EMAIL, MSG_WRONG_PASSWORD from ._login_repository_legacy import AsyncpgStorage, get_plugin_storage +from .constants import MSG_UNKNOWN_EMAIL, MSG_WRONG_PASSWORD async def get_user_by_email(app: web.Application, *, email: str) -> dict[str, Any]: diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/_rest_exceptions.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/_rest_exceptions.py index 162acab01a0..1878ba44951 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/_rest_exceptions.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/_rest_exceptions.py @@ -8,7 +8,7 @@ to_exceptions_handlers_map, ) from ....users.exceptions import AlreadyPreRegisteredError -from ..._constants import MSG_2FA_UNAVAILABLE +from ...constants import MSG_2FA_UNAVAILABLE from ...errors import SendingVerificationEmailError, SendingVerificationSmsError _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py index b84b416bb85..eaa1e7923b6 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py @@ -23,7 +23,8 @@ from ....users import preferences_api as user_preferences_api from ....utils_aiohttp import NextPage from ... import _auth_service, _login_service, _security_service, _twofa_service -from ..._constants import ( +from ..._models import InputSchema +from ...constants import ( CODE_2FA_EMAIL_CODE_REQUIRED, CODE_2FA_SMS_CODE_REQUIRED, CODE_PHONE_NUMBER_REQUIRED, @@ -37,7 +38,6 @@ MSG_WRONG_2FA_CODE__EXPIRED, MSG_WRONG_2FA_CODE__INVALID, ) -from ..._models import InputSchema from ...decorators import login_required from ...settings import LoginSettingsForProduct, get_plugin_settings from ._rest_exceptions import handle_rest_requests_exceptions diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py index 6f0a6992338..2753a05c29c 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py @@ -19,14 +19,6 @@ from ....utils import HOUR from ....utils_rate_limiting import global_rate_limit_route from ... import _confirmation_service, _confirmation_web -from ..._constants import ( - MSG_CANT_SEND_MAIL, - MSG_CHANGE_EMAIL_REQUESTED, - MSG_EMAIL_SENT, - MSG_OFTEN_RESET_PASSWORD, - MSG_PASSWORD_CHANGED, - MSG_WRONG_PASSWORD, -) from ..._emails_service import get_template_path, send_email_from_template from ..._login_repository_legacy import AsyncpgStorage, get_plugin_storage from ..._login_service import ( @@ -36,6 +28,14 @@ validate_user_status, ) from ..._models import InputSchema, create_password_match_validator +from ...constants import ( + MSG_CANT_SEND_MAIL, + MSG_CHANGE_EMAIL_REQUESTED, + MSG_EMAIL_SENT, + MSG_OFTEN_RESET_PASSWORD, + MSG_PASSWORD_CHANGED, + MSG_WRONG_PASSWORD, +) from ...decorators import login_required from ...settings import LoginOptions, get_plugin_options diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py index d263081a9a0..0bf3a1bd5fb 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py @@ -34,11 +34,6 @@ from ....utils_aiohttp import create_redirect_to_page_response from ....utils_rate_limiting import global_rate_limit_route from ... import _confirmation_service, _security_service, _twofa_service -from ..._constants import ( - MSG_PASSWORD_CHANGE_NOT_ALLOWED, - MSG_PASSWORD_CHANGED, - MSG_UNAUTHORIZED_PHONE_CONFIRMATION, -) from ..._invitations_service import ConfirmedInvitationData from ..._login_repository_legacy import ( AsyncpgStorage, @@ -54,6 +49,11 @@ notify_user_confirmation, ) from ..._models import InputSchema, check_confirm_password_match +from ...constants import ( + MSG_PASSWORD_CHANGE_NOT_ALLOWED, + MSG_PASSWORD_CHANGED, + MSG_UNAUTHORIZED_PHONE_CONFIRMATION, +) from ...settings import ( LoginOptions, LoginSettingsForProduct, diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py index 920edfc1ea3..c7ff5fdbbb6 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py @@ -33,15 +33,6 @@ from ....utils_aiohttp import NextPage, envelope_json_response from ....utils_rate_limiting import global_rate_limit_route from ... import _auth_service, _confirmation_web, _security_service, _twofa_service -from ..._constants import ( - CODE_2FA_SMS_CODE_REQUIRED, - MAX_2FA_CODE_RESEND, - MAX_2FA_CODE_TRIALS, - MSG_2FA_CODE_SENT, - MSG_CANT_SEND_MAIL, - MSG_UNAUTHORIZED_REGISTER_PHONE, - MSG_WEAK_PASSWORD, -) from ..._emails_service import get_template_path, send_email_from_template from ..._invitations_service import ( ConfirmedInvitationData, @@ -61,6 +52,15 @@ notify_user_confirmation, ) from ..._models import InputSchema, check_confirm_password_match +from ...constants import ( + CODE_2FA_SMS_CODE_REQUIRED, + MAX_2FA_CODE_RESEND, + MAX_2FA_CODE_TRIALS, + MSG_2FA_CODE_SENT, + MSG_CANT_SEND_MAIL, + MSG_UNAUTHORIZED_REGISTER_PHONE, + MSG_WEAK_PASSWORD, +) from ...settings import ( LoginOptions, LoginSettingsForProduct, diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py index a0f7020d961..5a27725840d 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py @@ -13,16 +13,16 @@ from ....products.models import Product from ....session.access_policies import session_access_required from ... import _twofa_service -from ..._constants import ( +from ..._login_repository_legacy import AsyncpgStorage, get_plugin_storage +from ..._login_service import envelope_response +from ..._models import InputSchema +from ...constants import ( CODE_2FA_EMAIL_CODE_REQUIRED, CODE_2FA_SMS_CODE_REQUIRED, MSG_2FA_CODE_SENT, MSG_EMAIL_SENT, MSG_UNKNOWN_EMAIL, ) -from ..._login_repository_legacy import AsyncpgStorage, get_plugin_storage -from ..._login_service import envelope_response -from ..._models import InputSchema from ...settings import LoginSettingsForProduct, get_plugin_settings from ._rest_exceptions import handle_rest_requests_exceptions diff --git a/services/web/server/src/simcore_service_webserver/login/_invitations_service.py b/services/web/server/src/simcore_service_webserver/login/_invitations_service.py index a7cd5d62040..0b99e459970 100644 --- a/services/web/server/src/simcore_service_webserver/login/_invitations_service.py +++ b/services/web/server/src/simcore_service_webserver/login/_invitations_service.py @@ -41,16 +41,16 @@ ) from ..products.models import Product from . import _confirmation_service -from ._constants import ( - MSG_EMAIL_ALREADY_REGISTERED, - MSG_INVITATIONS_CONTACT_SUFFIX, - MSG_USER_DISABLED, -) from ._login_repository_legacy import ( AsyncpgStorage, BaseConfirmationTokenDict, ConfirmationTokenDict, ) +from .constants import ( + MSG_EMAIL_ALREADY_REGISTERED, + MSG_INVITATIONS_CONTACT_SUFFIX, + MSG_USER_DISABLED, +) from .settings import LoginOptions _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/login/_login_service.py b/services/web/server/src/simcore_service_webserver/login/_login_service.py index 8aa0160b6c9..055fa7661bc 100644 --- a/services/web/server/src/simcore_service_webserver/login/_login_service.py +++ b/services/web/server/src/simcore_service_webserver/login/_login_service.py @@ -12,7 +12,7 @@ from simcore_postgres_database.models.users import UserRole from ..db.models import ConfirmationAction, UserStatus -from ._constants import ( +from .constants import ( MSG_ACTIVATION_REQUIRED, MSG_USER_BANNED, MSG_USER_DELETED, diff --git a/services/web/server/src/simcore_service_webserver/login/_models.py b/services/web/server/src/simcore_service_webserver/login/_models.py index c63430b59e6..b7243d76039 100644 --- a/services/web/server/src/simcore_service_webserver/login/_models.py +++ b/services/web/server/src/simcore_service_webserver/login/_models.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, ConfigDict, SecretStr, ValidationInfo -from ._constants import MSG_PASSWORD_MISMATCH +from .constants import MSG_PASSWORD_MISMATCH class InputSchema(BaseModel): diff --git a/services/web/server/src/simcore_service_webserver/login/_security_service.py b/services/web/server/src/simcore_service_webserver/login/_security_service.py index 8588c08039d..ed974c1cec1 100644 --- a/services/web/server/src/simcore_service_webserver/login/_security_service.py +++ b/services/web/server/src/simcore_service_webserver/login/_security_service.py @@ -7,8 +7,8 @@ from servicelib.logging_utils import get_log_record_extra, log_context from ..security import security_web -from ._constants import MSG_LOGGED_IN from ._login_service import flash_response +from .constants import MSG_LOGGED_IN _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/login/_constants.py b/services/web/server/src/simcore_service_webserver/login/constants.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/login/_constants.py rename to services/web/server/src/simcore_service_webserver/login/constants.py diff --git a/services/web/server/src/simcore_service_webserver/login/plugin.py b/services/web/server/src/simcore_service_webserver/login/plugin.py index f5a03d7cb48..32e7c387f45 100644 --- a/services/web/server/src/simcore_service_webserver/login/plugin.py +++ b/services/web/server/src/simcore_service_webserver/login/plugin.py @@ -22,22 +22,22 @@ from ..email.plugin import setup_email from ..email.settings import get_plugin_settings as get_email_plugin_settings from ..invitations.plugin import setup_invitations +from ..login_account._controller.rest import preregistration from ..login_auth.plugin import setup_login_auth from ..products import products_service from ..products.models import ProductName from ..products.plugin import setup_products from ..redis import setup_redis from ..rest.plugin import setup_rest -from ._constants import APP_LOGIN_SETTINGS_PER_PRODUCT_KEY from ._controller.rest import ( auth, change, confirmation, - preregistration, registration, twofa, ) from ._login_repository_legacy import APP_LOGIN_STORAGE_KEY, AsyncpgStorage +from .constants import APP_LOGIN_SETTINGS_PER_PRODUCT_KEY from .settings import ( APP_LOGIN_OPTIONS_KEY, LoginOptions, diff --git a/services/web/server/src/simcore_service_webserver/login/settings.py b/services/web/server/src/simcore_service_webserver/login/settings.py index 909b3a64eb6..697822affab 100644 --- a/services/web/server/src/simcore_service_webserver/login/settings.py +++ b/services/web/server/src/simcore_service_webserver/login/settings.py @@ -10,7 +10,7 @@ from settings_library.twilio import TwilioSettings from simcore_postgres_database.models.products import ProductLoginSettingsDict -from ._constants import APP_LOGIN_SETTINGS_PER_PRODUCT_KEY +from .constants import APP_LOGIN_SETTINGS_PER_PRODUCT_KEY _DAYS: Final[float] = 1.0 # in days _MINUTES: Final[float] = 1.0 / 24.0 / 60.0 # in days diff --git a/services/web/server/src/simcore_service_webserver/login_account/__init__.py b/services/web/server/src/simcore_service_webserver/login_account/__init__.py new file mode 100644 index 00000000000..aded3db0d87 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login_account/__init__.py @@ -0,0 +1,3 @@ +""" +Account management +""" diff --git a/services/web/server/src/simcore_service_webserver/login_account/_controller/__init__.py b/services/web/server/src/simcore_service_webserver/login_account/_controller/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/__init__.py b/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/preregistration.py b/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py similarity index 88% rename from services/web/server/src/simcore_service_webserver/login/_controller/rest/preregistration.py rename to services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py index bad370c31a0..2da3e351fe1 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/preregistration.py +++ b/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py @@ -6,17 +6,23 @@ AccountRequestInfo, UnregisterCheck, ) -from models_library.users import UserID -from pydantic import BaseModel, Field from servicelib.aiohttp import status from servicelib.aiohttp.application_keys import APP_FIRE_AND_FORGET_TASKS_KEY from servicelib.aiohttp.requests_validation import parse_request_body_as from servicelib.logging_utils import get_log_record_extra, log_context -from servicelib.request_keys import RQT_USERID_KEY from servicelib.utils import fire_and_forget_task from ...._meta import API_VTAG -from ....constants import RQ_PRODUCT_KEY +from ....login._controller.rest._rest_exceptions import handle_rest_requests_exceptions +from ....login._login_service import flash_response, notify_user_logout +from ....login.constants import ( + CAPTCHA_SESSION_KEY, + MSG_LOGGED_OUT, + MSG_WRONG_CAPTCHA__INVALID, +) +from ....login.settings import LoginSettingsForProduct, get_plugin_settings +from ....login_auth.decorators import login_required +from ....models import AuthenticatedRequestContext from ....products import products_web from ....products.models import Product from ....security import security_service, security_web @@ -27,15 +33,6 @@ from ....utils import MINUTE from ....utils_rate_limiting import global_rate_limit_route from ... import _preregistration_service -from ..._constants import ( - CAPTCHA_SESSION_KEY, - MSG_LOGGED_OUT, - MSG_WRONG_CAPTCHA__INVALID, -) -from ..._login_service import flash_response, notify_user_logout -from ...decorators import login_required -from ...settings import LoginSettingsForProduct, get_plugin_settings -from ._rest_exceptions import handle_rest_requests_exceptions _logger = logging.getLogger(__name__) @@ -97,17 +94,12 @@ async def request_product_account(request: web.Request): return web.json_response(status=status.HTTP_204_NO_CONTENT) -class _AuthenticatedContext(BaseModel): - user_id: UserID = Field(..., alias=RQT_USERID_KEY) # type: ignore[literal-required] - product_name: str = Field(..., alias=RQ_PRODUCT_KEY) # type: ignore[literal-required] - - @routes.post(f"/{API_VTAG}/auth/unregister", name="unregister_account") @login_required @permission_required("user.profile.delete") @handle_rest_requests_exceptions async def unregister_account(request: web.Request): - req_ctx = _AuthenticatedContext.model_validate(request) + req_ctx = AuthenticatedRequestContext.model_validate(request) body = await parse_request_body_as(UnregisterCheck, request) product: Product = products_web.get_current_product(request) diff --git a/services/web/server/src/simcore_service_webserver/login/_preregistration_service.py b/services/web/server/src/simcore_service_webserver/login_account/_preregistration_service.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/login/_preregistration_service.py rename to services/web/server/src/simcore_service_webserver/login_account/_preregistration_service.py diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_auth.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_auth.py index 1a0b01184fb..f9246da56ec 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_auth.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_auth.py @@ -23,7 +23,7 @@ from simcore_postgres_database.models.users import UserRole from simcore_service_webserver.constants import APP_SETTINGS_KEY from simcore_service_webserver.db.models import UserStatus -from simcore_service_webserver.login._constants import ( +from simcore_service_webserver.login.constants import ( MSG_ACTIVATION_REQUIRED, MSG_LOGGED_IN, MSG_UNKNOWN_EMAIL, diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_change_email.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_change_email.py index ce08c455852..c0257fa5e20 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_change_email.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_change_email.py @@ -8,7 +8,7 @@ from pytest_simcore.helpers.webserver_login import LoggedUser, NewUser, parse_link from servicelib.aiohttp import status from simcore_service_webserver.constants import INDEX_RESOURCE_NAME -from simcore_service_webserver.login._constants import ( +from simcore_service_webserver.login.constants import ( MSG_CHANGE_EMAIL_REQUESTED, MSG_LOGGED_IN, MSG_LOGGED_OUT, diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_change_password.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_change_password.py index a9b5308a8b4..e433635e604 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_change_password.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_change_password.py @@ -9,7 +9,7 @@ from pytest_simcore.helpers.webserver_login import LoggedUser from servicelib.aiohttp import status from servicelib.rest_responses import unwrap_envelope -from simcore_service_webserver.login._constants import ( +from simcore_service_webserver.login.constants import ( MSG_LOGGED_IN, MSG_PASSWORD_CHANGED, MSG_PASSWORD_MISMATCH, diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_preregistration.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_preregistration.py index 9418b324c64..85b4343f9fb 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_preregistration.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_preregistration.py @@ -20,7 +20,7 @@ from pytest_simcore.helpers.webserver_users import NewUser, UserInfoDict from servicelib.aiohttp import status from simcore_postgres_database.models.users import UserRole -from simcore_service_webserver.login._constants import MSG_USER_DELETED +from simcore_service_webserver.login.constants import MSG_USER_DELETED from simcore_service_webserver.products.products_service import get_product diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py index cc8e68689ab..1262b78ed97 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py @@ -20,13 +20,13 @@ from simcore_service_webserver.db.models import UserStatus from simcore_service_webserver.groups.api import auto_add_user_to_product_group from simcore_service_webserver.login._confirmation_web import _url_for_confirmation -from simcore_service_webserver.login._constants import ( +from simcore_service_webserver.login._login_repository_legacy import AsyncpgStorage +from simcore_service_webserver.login.constants import ( MSG_EMAIL_ALREADY_REGISTERED, MSG_LOGGED_IN, MSG_PASSWORD_MISMATCH, MSG_WEAK_PASSWORD, ) -from simcore_service_webserver.login._login_repository_legacy import AsyncpgStorage from simcore_service_webserver.login.settings import ( LoginOptions, LoginSettingsForProduct, diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_reset_password.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_reset_password.py index 2451e0adee7..a7d27b6175b 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_reset_password.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_reset_password.py @@ -18,7 +18,11 @@ from servicelib.utils_secrets import generate_password from simcore_service_webserver.db.models import ConfirmationAction, UserStatus from simcore_service_webserver.groups import api as groups_service -from simcore_service_webserver.login._constants import ( +from simcore_service_webserver.login._login_repository_legacy import ( + AsyncpgStorage, + ConfirmationTokenDict, +) +from simcore_service_webserver.login.constants import ( MSG_ACTIVATION_REQUIRED, MSG_EMAIL_SENT, MSG_LOGGED_IN, @@ -26,10 +30,6 @@ MSG_USER_BANNED, MSG_USER_EXPIRED, ) -from simcore_service_webserver.login._login_repository_legacy import ( - AsyncpgStorage, - ConfirmationTokenDict, -) from simcore_service_webserver.login.settings import LoginOptions from simcore_service_webserver.users import api as users_service from yarl import URL diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa.py index b91b30075ce..15f77ed013c 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa.py @@ -21,11 +21,6 @@ from simcore_postgres_database.models.products import ProductLoginSettingsDict, products from simcore_service_webserver.application_settings import ApplicationSettings from simcore_service_webserver.db.models import UserStatus -from simcore_service_webserver.login._constants import ( - CODE_2FA_SMS_CODE_REQUIRED, - MSG_2FA_UNAVAILABLE, - MSG_LOGGED_IN, -) from simcore_service_webserver.login._login_repository_legacy import AsyncpgStorage from simcore_service_webserver.login._twofa_service import ( _do_create_2fa_code, @@ -35,6 +30,11 @@ get_redis_validation_code_client, send_email_code, ) +from simcore_service_webserver.login.constants import ( + CODE_2FA_SMS_CODE_REQUIRED, + MSG_2FA_UNAVAILABLE, + MSG_LOGGED_IN, +) from simcore_service_webserver.products import products_web from simcore_service_webserver.products.errors import UnknownProductError from simcore_service_webserver.products.models import Product diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa_resend.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa_resend.py index 07ff518ed20..112982a7d68 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa_resend.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa_resend.py @@ -13,11 +13,11 @@ from servicelib.aiohttp import status from simcore_postgres_database.models.products import ProductLoginSettingsDict, products from simcore_service_webserver.application_settings import ApplicationSettings -from simcore_service_webserver.login._constants import CODE_2FA_SMS_CODE_REQUIRED from simcore_service_webserver.login._controller.rest.auth import ( CodePageParams, NextPage, ) +from simcore_service_webserver.login.constants import CODE_2FA_SMS_CODE_REQUIRED @pytest.fixture diff --git a/services/web/server/tests/unit/with_dbs/03/test_email.py b/services/web/server/tests/unit/with_dbs/03/test_email.py index 15c8618ee45..5e65165ce15 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_email.py +++ b/services/web/server/tests/unit/with_dbs/03/test_email.py @@ -30,10 +30,10 @@ from simcore_service_webserver.email._core import _remove_comments, _render_template from simcore_service_webserver.email._handlers import EmailTestFailed, EmailTestPassed from simcore_service_webserver.email.plugin import setup_email -from simcore_service_webserver.login._controller.rest.preregistration import ( +from simcore_service_webserver.login_account._controller.rest.preregistration import ( _get_ipinfo, ) -from simcore_service_webserver.login._preregistration_service import ( +from simcore_service_webserver.login_account._preregistration_service import ( _json_encoder_and_dumps, ) diff --git a/services/web/server/tests/unit/with_dbs/03/test_session_access_policies.py b/services/web/server/tests/unit/with_dbs/03/test_session_access_policies.py index 9a20ee62e4c..bdfe1c7dffe 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_session_access_policies.py +++ b/services/web/server/tests/unit/with_dbs/03/test_session_access_policies.py @@ -14,7 +14,7 @@ from servicelib.aiohttp import status from simcore_service_webserver.application_settings import ApplicationSettings from simcore_service_webserver.constants import APP_SETTINGS_KEY -from simcore_service_webserver.login._constants import ( +from simcore_service_webserver.login.constants import ( MAX_2FA_CODE_RESEND, MAX_2FA_CODE_TRIALS, MSG_UNAUTHORIZED_LOGIN_2FA, From 5ee7e41edd021be15590f279c4c657983dab4c14 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:52:01 +0200 Subject: [PATCH 02/19] splitting login logic --- .../login/_controller/rest/auth.py | 9 +- .../login/_controller/rest/change.py | 2 +- .../login/_controller/rest/confirmation.py | 2 +- .../login/_controller/rest/registration.py | 16 ++- .../login/_controller/rest/twofa.py | 2 +- .../login/_login_service.py | 28 ----- .../login/_registration_service.py | 111 ++++++++++++++++++ .../login/web_utils.py | 27 +++++ .../_controller/rest/preregistration.py | 3 +- 9 files changed, 159 insertions(+), 41 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/login/_registration_service.py create mode 100644 services/web/server/src/simcore_service_webserver/login/web_utils.py diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py index eaa1e7923b6..f404efe30ae 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py @@ -40,6 +40,7 @@ ) from ...decorators import login_required from ...settings import LoginSettingsForProduct, get_plugin_settings +from ...web_utils import envelope_response, flash_response from ._rest_exceptions import handle_rest_requests_exceptions log = logging.getLogger(__name__) @@ -134,7 +135,7 @@ async def login(request: web.Request): user_2fa_authentification_method == TwoFactorAuthentificationMethod.SMS and not user["phone"] ): - return _login_service.envelope_response( + return envelope_response( # LoginNextPage { "name": CODE_PHONE_NUMBER_REQUIRED, @@ -169,7 +170,7 @@ async def login(request: web.Request): user_id=user["id"], ) - return _login_service.envelope_response( + return envelope_response( # LoginNextPage { "name": CODE_2FA_SMS_CODE_REQUIRED, @@ -196,7 +197,7 @@ async def login(request: web.Request): product=product, user_id=user["id"], ) - return _login_service.envelope_response( + return envelope_response( { "name": CODE_2FA_EMAIL_CODE_REQUIRED, "parameters": { @@ -282,7 +283,7 @@ async def logout(request: web.Request) -> web.Response: f"{logout_.client_session_id=}", extra=get_log_record_extra(user_id=user_id), ): - response = _login_service.flash_response(MSG_LOGGED_OUT, "INFO") + response = flash_response(MSG_LOGGED_OUT, "INFO") await _login_service.notify_user_logout( request.app, user_id, logout_.client_session_id ) diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py index 2753a05c29c..7f6b21f5c2c 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py @@ -24,7 +24,6 @@ from ..._login_service import ( ACTIVE, CHANGE_EMAIL, - flash_response, validate_user_status, ) from ..._models import InputSchema, create_password_match_validator @@ -38,6 +37,7 @@ ) from ...decorators import login_required from ...settings import LoginOptions, get_plugin_options +from ...web_utils import flash_response _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py index 0bf3a1bd5fb..f8f61e05163 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py @@ -45,7 +45,6 @@ CHANGE_EMAIL, REGISTRATION, RESET_PASSWORD, - flash_response, notify_user_confirmation, ) from ..._models import InputSchema, check_confirm_password_match @@ -60,6 +59,7 @@ get_plugin_options, get_plugin_settings, ) +from ...web_utils import flash_response _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py index c7ff5fdbbb6..ef9186fa172 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py @@ -32,7 +32,13 @@ from ....utils import MINUTE from ....utils_aiohttp import NextPage, envelope_json_response from ....utils_rate_limiting import global_rate_limit_route -from ... import _auth_service, _confirmation_web, _security_service, _twofa_service +from ... import ( + _auth_service, + _confirmation_web, + _registration_service, + _security_service, + _twofa_service, +) from ..._emails_service import get_template_path, send_email_from_template from ..._invitations_service import ( ConfirmedInvitationData, @@ -46,9 +52,6 @@ get_plugin_storage, ) from ..._login_service import ( - envelope_response, - flash_response, - get_user_name_from_email, notify_user_confirmation, ) from ..._models import InputSchema, check_confirm_password_match @@ -67,6 +70,7 @@ get_plugin_options, get_plugin_settings, ) +from ...web_utils import envelope_response, flash_response _logger = logging.getLogger(__name__) @@ -398,7 +402,9 @@ async def register_phone(request: web.Request): twilio_auth=settings.LOGIN_TWILIO, twilio_messaging_sid=product.twilio_messaging_sid, twilio_alpha_numeric_sender=product.twilio_alpha_numeric_sender_id, - first_name=get_user_name_from_email(registration.email), + first_name=_registration_service.get_user_name_from_email( + registration.email + ), ) return envelope_response( diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py index 5a27725840d..9d4823d2c21 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py @@ -14,7 +14,6 @@ from ....session.access_policies import session_access_required from ... import _twofa_service from ..._login_repository_legacy import AsyncpgStorage, get_plugin_storage -from ..._login_service import envelope_response from ..._models import InputSchema from ...constants import ( CODE_2FA_EMAIL_CODE_REQUIRED, @@ -24,6 +23,7 @@ MSG_UNKNOWN_EMAIL, ) from ...settings import LoginSettingsForProduct, get_plugin_settings +from ...web_utils import envelope_response from ._rest_exceptions import handle_rest_requests_exceptions _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/login/_login_service.py b/services/web/server/src/simcore_service_webserver/login/_login_service.py index 055fa7661bc..3dd6364ff95 100644 --- a/services/web/server/src/simcore_service_webserver/login/_login_service.py +++ b/services/web/server/src/simcore_service_webserver/login/_login_service.py @@ -1,14 +1,10 @@ -from dataclasses import asdict from typing import Any from aiohttp import web -from common_library.json_serialization import json_dumps from models_library.products import ProductName -from models_library.rest_error import LogMessageType from models_library.users import UserID from pydantic import PositiveInt from servicelib.aiohttp import observer -from servicelib.aiohttp.status import HTTP_200_OK from simcore_postgres_database.models.users import UserRole from ..db.models import ConfirmationAction, UserStatus @@ -109,27 +105,3 @@ async def notify_user_logout( client_session_id, app, ) - - -def flash_response( - message: str, level: str = "INFO", *, status: int = HTTP_200_OK -) -> web.Response: - return envelope_response( - data=asdict(LogMessageType(message, level)), - status=status, - ) - - -def envelope_response(data: Any, *, status: int = HTTP_200_OK) -> web.Response: - return web.json_response( - { - "data": data, - "error": None, - }, - dumps=json_dumps, - status=status, - ) - - -def get_user_name_from_email(email: str) -> str: - return email.split("@")[0] diff --git a/services/web/server/src/simcore_service_webserver/login/_registration_service.py b/services/web/server/src/simcore_service_webserver/login/_registration_service.py new file mode 100644 index 00000000000..e1ec08647e6 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login/_registration_service.py @@ -0,0 +1,111 @@ +from typing import Any + +from aiohttp import web +from models_library.products import ProductName +from models_library.users import UserID +from pydantic import PositiveInt +from servicelib.aiohttp import observer +from simcore_postgres_database.models.users import UserRole + +from ..db.models import ConfirmationAction, UserStatus +from .constants import ( + MSG_ACTIVATION_REQUIRED, + MSG_USER_BANNED, + MSG_USER_DELETED, + MSG_USER_EXPIRED, +) + + +def _to_names(enum_cls, names) -> list[str]: + """ensures names are in enum be retrieving each of them""" + return [getattr(enum_cls, att).name for att in names.split()] + + +CONFIRMATION_PENDING, ACTIVE, BANNED, EXPIRED, DELETED = ( + UserStatus.CONFIRMATION_PENDING.name, + UserStatus.ACTIVE.name, + UserStatus.BANNED.name, + UserStatus.EXPIRED.name, + UserStatus.DELETED.name, +) +_EXPECTED_ENUMS = 5 +assert len(UserStatus) == _EXPECTED_ENUMS # nosec + + +ANONYMOUS, GUEST, USER, TESTER = _to_names(UserRole, "ANONYMOUS GUEST USER TESTER") + +REGISTRATION, RESET_PASSWORD, CHANGE_EMAIL = _to_names( + ConfirmationAction, "REGISTRATION RESET_PASSWORD CHANGE_EMAIL" +) + + +def validate_user_status(*, user: dict, support_email: str): + """ + + Raises: + web.HTTPUnauthorized + """ + assert "role" in user # nosec + + user_status: str = user["status"] + + if user_status == DELETED: + raise web.HTTPUnauthorized( + text=MSG_USER_DELETED.format(support_email=support_email), + ) # 401 + + if user_status == BANNED or user["role"] == ANONYMOUS: + raise web.HTTPUnauthorized( + text=MSG_USER_BANNED.format(support_email=support_email), + ) # 401 + + if user_status == EXPIRED: + raise web.HTTPUnauthorized( + text=MSG_USER_EXPIRED.format(support_email=support_email), + ) # 401 + + if user_status == CONFIRMATION_PENDING: + raise web.HTTPUnauthorized( + text=MSG_ACTIVATION_REQUIRED, + ) # 401 + + assert user_status == ACTIVE # nosec + + +async def notify_user_confirmation( + app: web.Application, + user_id: UserID, + product_name: ProductName, + extra_credits_in_usd: PositiveInt | None, +): + """Broadcast that user with 'user_id' has login for the first-time in 'product_name'""" + # NOTE: Follow up in https://github.com/ITISFoundation/osparc-simcore/issues/4822 + await observer.emit( + app, + "SIGNAL_ON_USER_CONFIRMATION", + user_id=user_id, + product_name=product_name, + extra_credits_in_usd=extra_credits_in_usd, + ) + + +async def notify_user_logout( + app: web.Application, user_id: UserID, client_session_id: Any | None = None +): + """Broadcasts logout of 'user_id' in 'client_session_id'. + + If 'client_session_id' is None, then all sessions are considered + + Listeners (e.g. sockets) will trigger logout mechanisms + """ + await observer.emit( + app, + "SIGNAL_USER_LOGOUT", + user_id, + client_session_id, + app, + ) + + +def get_user_name_from_email(email: str) -> str: + return email.split("@")[0] diff --git a/services/web/server/src/simcore_service_webserver/login/web_utils.py b/services/web/server/src/simcore_service_webserver/login/web_utils.py new file mode 100644 index 00000000000..31b57e4cbeb --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login/web_utils.py @@ -0,0 +1,27 @@ +from dataclasses import asdict +from typing import Any + +from aiohttp import web +from common_library.json_serialization import json_dumps +from models_library.rest_error import LogMessageType +from servicelib.aiohttp.status import HTTP_200_OK + + +def envelope_response(data: Any, *, status: int = HTTP_200_OK) -> web.Response: + return web.json_response( + { + "data": data, + "error": None, + }, + dumps=json_dumps, + status=status, + ) + + +def flash_response( + message: str, level: str = "INFO", *, status: int = HTTP_200_OK +) -> web.Response: + return envelope_response( + data=asdict(LogMessageType(message, level)), + status=status, + ) diff --git a/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py b/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py index 2da3e351fe1..d4f18c2ae36 100644 --- a/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py +++ b/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py @@ -14,13 +14,14 @@ from ...._meta import API_VTAG from ....login._controller.rest._rest_exceptions import handle_rest_requests_exceptions -from ....login._login_service import flash_response, notify_user_logout +from ....login._login_service import notify_user_logout from ....login.constants import ( CAPTCHA_SESSION_KEY, MSG_LOGGED_OUT, MSG_WRONG_CAPTCHA__INVALID, ) from ....login.settings import LoginSettingsForProduct, get_plugin_settings +from ....login.web_utils import flash_response from ....login_auth.decorators import login_required from ....models import AuthenticatedRequestContext from ....products import products_web From 28c07888ea897eb2d220ac7bbbe8111e686ea301 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:14:23 +0200 Subject: [PATCH 03/19] rest exceptions --- .../login/__init__.py | 5 - .../login/_registration_service.py | 109 ------------------ .../simcore_service_webserver/login/plugin.py | 2 + .../_controller/rest/_rest_exceptions.py | 15 +++ .../_controller/rest/preregistration.py | 18 ++- .../login_account/_preregistration_service.py | 2 +- .../login_account/plugin.py | 13 +++ 7 files changed, 39 insertions(+), 125 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/login_account/_controller/rest/_rest_exceptions.py create mode 100644 services/web/server/src/simcore_service_webserver/login_account/plugin.py diff --git a/services/web/server/src/simcore_service_webserver/login/__init__.py b/services/web/server/src/simcore_service_webserver/login/__init__.py index 7d14213b2ee..e69de29bb2d 100644 --- a/services/web/server/src/simcore_service_webserver/login/__init__.py +++ b/services/web/server/src/simcore_service_webserver/login/__init__.py @@ -1,5 +0,0 @@ -""" webserver's login subsystem - - - This sub-package is based on aiohttp-login https://github.com/imbolc/aiohttp-login -""" diff --git a/services/web/server/src/simcore_service_webserver/login/_registration_service.py b/services/web/server/src/simcore_service_webserver/login/_registration_service.py index e1ec08647e6..25a0ad56cfc 100644 --- a/services/web/server/src/simcore_service_webserver/login/_registration_service.py +++ b/services/web/server/src/simcore_service_webserver/login/_registration_service.py @@ -1,111 +1,2 @@ -from typing import Any - -from aiohttp import web -from models_library.products import ProductName -from models_library.users import UserID -from pydantic import PositiveInt -from servicelib.aiohttp import observer -from simcore_postgres_database.models.users import UserRole - -from ..db.models import ConfirmationAction, UserStatus -from .constants import ( - MSG_ACTIVATION_REQUIRED, - MSG_USER_BANNED, - MSG_USER_DELETED, - MSG_USER_EXPIRED, -) - - -def _to_names(enum_cls, names) -> list[str]: - """ensures names are in enum be retrieving each of them""" - return [getattr(enum_cls, att).name for att in names.split()] - - -CONFIRMATION_PENDING, ACTIVE, BANNED, EXPIRED, DELETED = ( - UserStatus.CONFIRMATION_PENDING.name, - UserStatus.ACTIVE.name, - UserStatus.BANNED.name, - UserStatus.EXPIRED.name, - UserStatus.DELETED.name, -) -_EXPECTED_ENUMS = 5 -assert len(UserStatus) == _EXPECTED_ENUMS # nosec - - -ANONYMOUS, GUEST, USER, TESTER = _to_names(UserRole, "ANONYMOUS GUEST USER TESTER") - -REGISTRATION, RESET_PASSWORD, CHANGE_EMAIL = _to_names( - ConfirmationAction, "REGISTRATION RESET_PASSWORD CHANGE_EMAIL" -) - - -def validate_user_status(*, user: dict, support_email: str): - """ - - Raises: - web.HTTPUnauthorized - """ - assert "role" in user # nosec - - user_status: str = user["status"] - - if user_status == DELETED: - raise web.HTTPUnauthorized( - text=MSG_USER_DELETED.format(support_email=support_email), - ) # 401 - - if user_status == BANNED or user["role"] == ANONYMOUS: - raise web.HTTPUnauthorized( - text=MSG_USER_BANNED.format(support_email=support_email), - ) # 401 - - if user_status == EXPIRED: - raise web.HTTPUnauthorized( - text=MSG_USER_EXPIRED.format(support_email=support_email), - ) # 401 - - if user_status == CONFIRMATION_PENDING: - raise web.HTTPUnauthorized( - text=MSG_ACTIVATION_REQUIRED, - ) # 401 - - assert user_status == ACTIVE # nosec - - -async def notify_user_confirmation( - app: web.Application, - user_id: UserID, - product_name: ProductName, - extra_credits_in_usd: PositiveInt | None, -): - """Broadcast that user with 'user_id' has login for the first-time in 'product_name'""" - # NOTE: Follow up in https://github.com/ITISFoundation/osparc-simcore/issues/4822 - await observer.emit( - app, - "SIGNAL_ON_USER_CONFIRMATION", - user_id=user_id, - product_name=product_name, - extra_credits_in_usd=extra_credits_in_usd, - ) - - -async def notify_user_logout( - app: web.Application, user_id: UserID, client_session_id: Any | None = None -): - """Broadcasts logout of 'user_id' in 'client_session_id'. - - If 'client_session_id' is None, then all sessions are considered - - Listeners (e.g. sockets) will trigger logout mechanisms - """ - await observer.emit( - app, - "SIGNAL_USER_LOGOUT", - user_id, - client_session_id, - app, - ) - - def get_user_name_from_email(email: str) -> str: return email.split("@")[0] diff --git a/services/web/server/src/simcore_service_webserver/login/plugin.py b/services/web/server/src/simcore_service_webserver/login/plugin.py index 32e7c387f45..c28da119df6 100644 --- a/services/web/server/src/simcore_service_webserver/login/plugin.py +++ b/services/web/server/src/simcore_service_webserver/login/plugin.py @@ -11,6 +11,7 @@ ) from settings_library.email import SMTPSettings from settings_library.postgres import PostgresSettings +from simcore_service_webserver.login_account.plugin import setup_login_account from ..constants import ( APP_PUBLIC_CONFIG_PER_PRODUCT, @@ -147,6 +148,7 @@ def setup_login(app: web.Application): app.router.add_routes(auth.routes) setup_login_auth(app) + setup_login_account(app) app.router.add_routes(confirmation.routes) app.router.add_routes(registration.routes) diff --git a/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/_rest_exceptions.py b/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/_rest_exceptions.py new file mode 100644 index 00000000000..529f0a949b0 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/_rest_exceptions.py @@ -0,0 +1,15 @@ +from ....exception_handling import ( + ExceptionToHttpErrorMap, + exception_handling_decorator, + to_exceptions_handlers_map, +) +from ....login._controller.rest._rest_exceptions import ( + _TO_HTTP_ERROR_MAP as LOGIN_TO_HTTP_ERROR_MAP, +) + +_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {**LOGIN_TO_HTTP_ERROR_MAP} + + +handle_rest_requests_exceptions = exception_handling_decorator( + to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP) +) diff --git a/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py b/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py index d4f18c2ae36..acdd1549e53 100644 --- a/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py +++ b/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py @@ -13,14 +13,13 @@ from servicelib.utils import fire_and_forget_task from ...._meta import API_VTAG -from ....login._controller.rest._rest_exceptions import handle_rest_requests_exceptions -from ....login._login_service import notify_user_logout +from ....login import login_service from ....login.constants import ( CAPTCHA_SESSION_KEY, MSG_LOGGED_OUT, MSG_WRONG_CAPTCHA__INVALID, ) -from ....login.settings import LoginSettingsForProduct, get_plugin_settings +from ....login.settings import get_plugin_settings from ....login.web_utils import flash_response from ....login_auth.decorators import login_required from ....models import AuthenticatedRequestContext @@ -34,13 +33,11 @@ from ....utils import MINUTE from ....utils_rate_limiting import global_rate_limit_route from ... import _preregistration_service +from ._rest_exceptions import handle_rest_requests_exceptions _logger = logging.getLogger(__name__) -routes = web.RouteTableDef() - - def _get_ipinfo(request: web.Request) -> dict[str, Any]: # NOTE: Traefik is also configured to transmit the original IP. x_real_ip = request.headers.get("X-Real-IP", None) @@ -56,6 +53,9 @@ def _get_ipinfo(request: web.Request) -> dict[str, Any]: } +routes = web.RouteTableDef() + + @routes.post( f"/{API_VTAG}/auth/request-account", name="request_product_account", @@ -104,9 +104,7 @@ async def unregister_account(request: web.Request): body = await parse_request_body_as(UnregisterCheck, request) product: Product = products_web.get_current_product(request) - settings: LoginSettingsForProduct = get_plugin_settings( - request.app, product_name=product.name - ) + settings = get_plugin_settings(request.app, product_name=product.name) # checks before deleting credentials = await users_service.get_user_credentials( @@ -130,7 +128,7 @@ async def unregister_account(request: web.Request): await users_service.set_user_as_deleted(request.app, user_id=req_ctx.user_id) # logout - await notify_user_logout( + await login_service.notify_user_logout( request.app, user_id=req_ctx.user_id, client_session_id=None ) response = flash_response(MSG_LOGGED_OUT, "INFO") diff --git a/services/web/server/src/simcore_service_webserver/login_account/_preregistration_service.py b/services/web/server/src/simcore_service_webserver/login_account/_preregistration_service.py index 0c615991daf..d76e7c3b365 100644 --- a/services/web/server/src/simcore_service_webserver/login_account/_preregistration_service.py +++ b/services/web/server/src/simcore_service_webserver/login_account/_preregistration_service.py @@ -128,7 +128,7 @@ async def create_captcha() -> tuple[str, bytes]: async def create_pre_registration( - app: web.Application, profile: PreRegisteredUserGet, product_name: ProductName + app: web.Application, *, profile: PreRegisteredUserGet, product_name: ProductName ): await _users_service.pre_register_user( diff --git a/services/web/server/src/simcore_service_webserver/login_account/plugin.py b/services/web/server/src/simcore_service_webserver/login_account/plugin.py new file mode 100644 index 00000000000..cd0ca8fde2e --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login_account/plugin.py @@ -0,0 +1,13 @@ +import logging + +from aiohttp import web +from servicelib.aiohttp.application_setup import ensure_single_setup + +from ._controller.rest import preregistration as _controller_rest_preregistration + +_logger = logging.getLogger(__name__) + + +@ensure_single_setup(__name__, logger=_logger) +def setup_login_account(app: web.Application): + app.add_routes(_controller_rest_preregistration.routes) From 9d88fc6dd88dfe2b58ede0cee37b94671ddb38a4 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:17:55 +0200 Subject: [PATCH 04/19] cleanup schemas --- .../login/_controller/rest/registration.py | 67 ++----------------- .../_controller/rest/registration_schemas.py | 65 ++++++++++++++++++ 2 files changed, 72 insertions(+), 60 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py index ef9186fa172..39390845b65 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py @@ -1,19 +1,9 @@ import logging from datetime import UTC, datetime, timedelta -from typing import Literal from aiohttp import web from aiohttp.web import RouteTableDef from common_library.error_codes import create_error_code -from models_library.emails import LowerCaseEmailStr -from pydantic import ( - BaseModel, - ConfigDict, - Field, - PositiveInt, - SecretStr, - field_validator, -) from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import parse_request_body_as from servicelib.logging_errors import create_troubleshootting_log_kwargs @@ -30,7 +20,7 @@ session_access_required, ) from ....utils import MINUTE -from ....utils_aiohttp import NextPage, envelope_json_response +from ....utils_aiohttp import envelope_json_response from ....utils_rate_limiting import global_rate_limit_route from ... import ( _auth_service, @@ -54,7 +44,6 @@ from ..._login_service import ( notify_user_confirmation, ) -from ..._models import InputSchema, check_confirm_password_match from ...constants import ( CODE_2FA_SMS_CODE_REQUIRED, MAX_2FA_CODE_RESEND, @@ -71,6 +60,12 @@ get_plugin_settings, ) from ...web_utils import envelope_response, flash_response +from .registration_schemas import ( + InvitationCheck, + InvitationInfo, + RegisterBody, + RegisterPhoneBody, +) _logger = logging.getLogger(__name__) @@ -78,16 +73,6 @@ routes = RouteTableDef() -class InvitationCheck(InputSchema): - invitation: str = Field(..., description="Invitation code") - - -class InvitationInfo(InputSchema): - email: LowerCaseEmailStr | None = Field( - None, description="Email associated to invitation or None" - ) - - @routes.post( f"/{API_VTAG}/auth/register/invitations:check", name="auth_check_registration_invitation", @@ -122,27 +107,6 @@ async def check_registration_invitation(request: web.Request): return envelope_json_response(InvitationInfo(email=email)) -class RegisterBody(InputSchema): - email: LowerCaseEmailStr - password: SecretStr - confirm: SecretStr | None = Field(None, description="Password confirmation") - invitation: str | None = Field(None, description="Invitation code") - - _password_confirm_match = field_validator("confirm")(check_confirm_password_match) - model_config = ConfigDict( - json_schema_extra={ - "examples": [ - { - "email": "foo@mymail.com", - "password": "my secret", # NOSONAR - "confirm": "my secret", # optional - "invitation": "33c451d4-17b7-4e65-9880-694559b8ffc2", # optional only active - } - ] - } - ) - - @routes.post(f"/{API_VTAG}/auth/register", name="auth_register") async def register(request: web.Request): """ @@ -335,23 +299,6 @@ async def register(request: web.Request): return await _security_service.login_granted_response(request=request, user=user) -class RegisterPhoneBody(InputSchema): - email: LowerCaseEmailStr - phone: str = Field( - ..., description="Phone number E.164, needed on the deployments with 2FA" - ) - - -class _PageParams(BaseModel): - expiration_2fa: PositiveInt | None = None - - -class RegisterPhoneNextPage(NextPage[_PageParams]): - logger: str = Field("user", deprecated=True) - level: Literal["INFO", "WARNING", "ERROR"] = "INFO" - message: str - - @routes.post(f"/{API_VTAG}/auth/verify-phone-number", name="auth_register_phone") @session_access_required( name="auth_register_phone", diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py new file mode 100644 index 00000000000..da118246fb7 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py @@ -0,0 +1,65 @@ +import logging +from typing import Literal + +from models_library.emails import LowerCaseEmailStr +from pydantic import ( + BaseModel, + ConfigDict, + Field, + PositiveInt, + SecretStr, + field_validator, +) + +from ....utils_aiohttp import NextPage +from ..._models import InputSchema, check_confirm_password_match + +_logger = logging.getLogger(__name__) + + +class InvitationCheck(InputSchema): + invitation: str = Field(..., description="Invitation code") + + +class InvitationInfo(InputSchema): + email: LowerCaseEmailStr | None = Field( + None, description="Email associated to invitation or None" + ) + + +class RegisterBody(InputSchema): + email: LowerCaseEmailStr + password: SecretStr + confirm: SecretStr | None = Field(None, description="Password confirmation") + invitation: str | None = Field(None, description="Invitation code") + + _password_confirm_match = field_validator("confirm")(check_confirm_password_match) + model_config = ConfigDict( + json_schema_extra={ + "examples": [ + { + "email": "foo@mymail.com", + "password": "my secret", # NOSONAR + "confirm": "my secret", # optional + "invitation": "33c451d4-17b7-4e65-9880-694559b8ffc2", # optional only active + } + ] + } + ) + + +class RegisterPhoneBody(InputSchema): + email: LowerCaseEmailStr + phone: str = Field( + ..., description="Phone number E.164, needed on the deployments with 2FA" + ) + + +class _PageParams(BaseModel): + expiration_2fa: PositiveInt | None = None + + +class RegisterPhoneNextPage(NextPage[_PageParams]): + logger: str = Field("user", deprecated=True) + level: Literal["INFO", "WARNING", "ERROR"] = "INFO" + message: str From 405b81a5103a59acfe54e3b2f64fe73de805c04f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:19:11 +0200 Subject: [PATCH 05/19] updates models --- .../_controller/rest/registration_schemas.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py index da118246fb7..ddb8bba116d 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py @@ -1,5 +1,5 @@ import logging -from typing import Literal +from typing import Annotated, Literal from models_library.emails import LowerCaseEmailStr from pydantic import ( @@ -18,20 +18,23 @@ class InvitationCheck(InputSchema): - invitation: str = Field(..., description="Invitation code") + invitation: Annotated[str, Field(description="Invitation code")] class InvitationInfo(InputSchema): - email: LowerCaseEmailStr | None = Field( - None, description="Email associated to invitation or None" - ) + email: Annotated[ + LowerCaseEmailStr | None, + Field(description="Email associated to invitation or None"), + ] = None class RegisterBody(InputSchema): email: LowerCaseEmailStr password: SecretStr - confirm: SecretStr | None = Field(None, description="Password confirmation") - invitation: str | None = Field(None, description="Invitation code") + confirm: Annotated[SecretStr | None, Field(description="Password confirmation")] = ( + None + ) + invitation: Annotated[str | None, Field(description="Invitation code")] = None _password_confirm_match = field_validator("confirm")(check_confirm_password_match) model_config = ConfigDict( @@ -50,15 +53,21 @@ class RegisterBody(InputSchema): class RegisterPhoneBody(InputSchema): email: LowerCaseEmailStr - phone: str = Field( - ..., description="Phone number E.164, needed on the deployments with 2FA" - ) + phone: Annotated[ + str, Field(description="Phone number E.164, needed on the deployments with 2FA") + ] class _PageParams(BaseModel): expiration_2fa: PositiveInt | None = None +class RegisterPhoneNextPage(NextPage[_PageParams]): + logger: str = "user" + level: Literal["INFO", "WARNING", "ERROR"] = "INFO" + message: str + + class RegisterPhoneNextPage(NextPage[_PageParams]): logger: str = Field("user", deprecated=True) level: Literal["INFO", "WARNING", "ERROR"] = "INFO" From 5e6d29dfb5cc47e233b89a90c0d89bb4349b83b3 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:37:53 +0200 Subject: [PATCH 06/19] fixes --- .../login/_controller/rest/registration_schemas.py | 6 ------ .../simcore_service_webserver/login/_security_service.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py index ddb8bba116d..ac2c4f9fed1 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py @@ -62,12 +62,6 @@ class _PageParams(BaseModel): expiration_2fa: PositiveInt | None = None -class RegisterPhoneNextPage(NextPage[_PageParams]): - logger: str = "user" - level: Literal["INFO", "WARNING", "ERROR"] = "INFO" - message: str - - class RegisterPhoneNextPage(NextPage[_PageParams]): logger: str = Field("user", deprecated=True) level: Literal["INFO", "WARNING", "ERROR"] = "INFO" diff --git a/services/web/server/src/simcore_service_webserver/login/_security_service.py b/services/web/server/src/simcore_service_webserver/login/_security_service.py index ed974c1cec1..6a8fb387b67 100644 --- a/services/web/server/src/simcore_service_webserver/login/_security_service.py +++ b/services/web/server/src/simcore_service_webserver/login/_security_service.py @@ -7,8 +7,8 @@ from servicelib.logging_utils import get_log_record_extra, log_context from ..security import security_web -from ._login_service import flash_response from .constants import MSG_LOGGED_IN +from .web_utils import flash_response _logger = logging.getLogger(__name__) From a1ec02c811df0cc7d3111cc609987752202c18c6 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:42:13 +0200 Subject: [PATCH 07/19] rename --- .../web/server/src/simcore_service_webserver/login/plugin.py | 4 ++-- .../{login_account => login_accounts}/__init__.py | 0 .../{login_account => login_accounts}/_controller/__init__.py | 0 .../_controller/rest/__init__.py | 0 .../_controller/rest/_rest_exceptions.py | 0 .../_controller/rest/preregistration.py | 0 .../_preregistration_service.py | 0 .../{login_account => login_accounts}/plugin.py | 0 services/web/server/tests/unit/with_dbs/03/test_email.py | 4 ++-- 9 files changed, 4 insertions(+), 4 deletions(-) rename services/web/server/src/simcore_service_webserver/{login_account => login_accounts}/__init__.py (100%) rename services/web/server/src/simcore_service_webserver/{login_account => login_accounts}/_controller/__init__.py (100%) rename services/web/server/src/simcore_service_webserver/{login_account => login_accounts}/_controller/rest/__init__.py (100%) rename services/web/server/src/simcore_service_webserver/{login_account => login_accounts}/_controller/rest/_rest_exceptions.py (100%) rename services/web/server/src/simcore_service_webserver/{login_account => login_accounts}/_controller/rest/preregistration.py (100%) rename services/web/server/src/simcore_service_webserver/{login_account => login_accounts}/_preregistration_service.py (100%) rename services/web/server/src/simcore_service_webserver/{login_account => login_accounts}/plugin.py (100%) diff --git a/services/web/server/src/simcore_service_webserver/login/plugin.py b/services/web/server/src/simcore_service_webserver/login/plugin.py index c28da119df6..eed630189bb 100644 --- a/services/web/server/src/simcore_service_webserver/login/plugin.py +++ b/services/web/server/src/simcore_service_webserver/login/plugin.py @@ -11,7 +11,7 @@ ) from settings_library.email import SMTPSettings from settings_library.postgres import PostgresSettings -from simcore_service_webserver.login_account.plugin import setup_login_account +from simcore_service_webserver.login_accounts.plugin import setup_login_account from ..constants import ( APP_PUBLIC_CONFIG_PER_PRODUCT, @@ -23,7 +23,7 @@ from ..email.plugin import setup_email from ..email.settings import get_plugin_settings as get_email_plugin_settings from ..invitations.plugin import setup_invitations -from ..login_account._controller.rest import preregistration +from ..login_accounts._controller.rest import preregistration from ..login_auth.plugin import setup_login_auth from ..products import products_service from ..products.models import ProductName diff --git a/services/web/server/src/simcore_service_webserver/login_account/__init__.py b/services/web/server/src/simcore_service_webserver/login_accounts/__init__.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/login_account/__init__.py rename to services/web/server/src/simcore_service_webserver/login_accounts/__init__.py diff --git a/services/web/server/src/simcore_service_webserver/login_account/_controller/__init__.py b/services/web/server/src/simcore_service_webserver/login_accounts/_controller/__init__.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/login_account/_controller/__init__.py rename to services/web/server/src/simcore_service_webserver/login_accounts/_controller/__init__.py diff --git a/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/__init__.py b/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/__init__.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/login_account/_controller/rest/__init__.py rename to services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/__init__.py diff --git a/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/_rest_exceptions.py b/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/_rest_exceptions.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/login_account/_controller/rest/_rest_exceptions.py rename to services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/_rest_exceptions.py diff --git a/services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py b/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/login_account/_controller/rest/preregistration.py rename to services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py diff --git a/services/web/server/src/simcore_service_webserver/login_account/_preregistration_service.py b/services/web/server/src/simcore_service_webserver/login_accounts/_preregistration_service.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/login_account/_preregistration_service.py rename to services/web/server/src/simcore_service_webserver/login_accounts/_preregistration_service.py diff --git a/services/web/server/src/simcore_service_webserver/login_account/plugin.py b/services/web/server/src/simcore_service_webserver/login_accounts/plugin.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/login_account/plugin.py rename to services/web/server/src/simcore_service_webserver/login_accounts/plugin.py diff --git a/services/web/server/tests/unit/with_dbs/03/test_email.py b/services/web/server/tests/unit/with_dbs/03/test_email.py index 5e65165ce15..2abcf0ec24c 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_email.py +++ b/services/web/server/tests/unit/with_dbs/03/test_email.py @@ -30,10 +30,10 @@ from simcore_service_webserver.email._core import _remove_comments, _render_template from simcore_service_webserver.email._handlers import EmailTestFailed, EmailTestPassed from simcore_service_webserver.email.plugin import setup_email -from simcore_service_webserver.login_account._controller.rest.preregistration import ( +from simcore_service_webserver.login_accounts._controller.rest.preregistration import ( _get_ipinfo, ) -from simcore_service_webserver.login_account._preregistration_service import ( +from simcore_service_webserver.login_accounts._preregistration_service import ( _json_encoder_and_dumps, ) From 17845c7aab919215f5d8ec10c7fbf6d7f47bcb68 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:45:49 +0200 Subject: [PATCH 08/19] rename tests --- ..._preregistration.py => test_login_accounts_preregistration.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename services/web/server/tests/unit/with_dbs/03/login/{test_login_preregistration.py => test_login_accounts_preregistration.py} (100%) diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_preregistration.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_accounts_preregistration.py similarity index 100% rename from services/web/server/tests/unit/with_dbs/03/login/test_login_preregistration.py rename to services/web/server/tests/unit/with_dbs/03/login/test_login_accounts_preregistration.py From 972936045bb5573f1c67f9610ae9ed433b4ec7cb Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:47:51 +0200 Subject: [PATCH 09/19] minor --- .../_controller/rest/preregistration.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py b/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py index acdd1549e53..23f8c7c2488 100644 --- a/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py +++ b/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py @@ -56,6 +56,20 @@ def _get_ipinfo(request: web.Request) -> dict[str, Any]: routes = web.RouteTableDef() +@routes.get(f"/{API_VTAG}/auth/captcha", name="create_captcha") +@global_rate_limit_route(number_of_requests=30, interval_seconds=MINUTE) +@handle_rest_requests_exceptions +async def create_captcha(request: web.Request): + session = await session_service.get_session(request) + + captcha_text, image_data = await _preregistration_service.create_captcha() + + # Store captcha text in session + session[CAPTCHA_SESSION_KEY] = captcha_text + + return web.Response(body=image_data, content_type="image/png") + + @routes.post( f"/{API_VTAG}/auth/request-account", name="request_product_account", @@ -147,17 +161,3 @@ async def unregister_account(request: web.Request): ) return response - - -@routes.get(f"/{API_VTAG}/auth/captcha", name="create_captcha") -@global_rate_limit_route(number_of_requests=30, interval_seconds=MINUTE) -@handle_rest_requests_exceptions -async def create_captcha(request: web.Request): - session = await session_service.get_session(request) - - captcha_text, image_data = await _preregistration_service.create_captcha() - - # Store captcha text in session - session[CAPTCHA_SESSION_KEY] = captcha_text - - return web.Response(body=image_data, content_type="image/png") From 49eeda99a03aff8f497f1a160a012e87828962ad Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:49:52 +0200 Subject: [PATCH 10/19] utils --- .../simcore_service_webserver/login/_controller/rest/auth.py | 2 +- .../simcore_service_webserver/login/_controller/rest/change.py | 2 +- .../login/_controller/rest/confirmation.py | 2 +- .../login/_controller/rest/registration.py | 2 +- .../simcore_service_webserver/login/_controller/rest/twofa.py | 2 +- .../src/simcore_service_webserver/login/_security_service.py | 2 +- .../login_accounts/_controller/rest/preregistration.py | 2 +- .../src/simcore_service_webserver/{login => }/web_utils.py | 0 8 files changed, 7 insertions(+), 7 deletions(-) rename services/web/server/src/simcore_service_webserver/{login => }/web_utils.py (100%) diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py index f404efe30ae..36c12c78517 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py @@ -22,6 +22,7 @@ ) from ....users import preferences_api as user_preferences_api from ....utils_aiohttp import NextPage +from ....web_utils import envelope_response, flash_response from ... import _auth_service, _login_service, _security_service, _twofa_service from ..._models import InputSchema from ...constants import ( @@ -40,7 +41,6 @@ ) from ...decorators import login_required from ...settings import LoginSettingsForProduct, get_plugin_settings -from ...web_utils import envelope_response, flash_response from ._rest_exceptions import handle_rest_requests_exceptions log = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py index 7f6b21f5c2c..4ba56312008 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py @@ -18,6 +18,7 @@ from ....users import api as users_service from ....utils import HOUR from ....utils_rate_limiting import global_rate_limit_route +from ....web_utils import flash_response from ... import _confirmation_service, _confirmation_web from ..._emails_service import get_template_path, send_email_from_template from ..._login_repository_legacy import AsyncpgStorage, get_plugin_storage @@ -37,7 +38,6 @@ ) from ...decorators import login_required from ...settings import LoginOptions, get_plugin_options -from ...web_utils import flash_response _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py index f8f61e05163..fd5c4866d7d 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py @@ -33,6 +33,7 @@ from ....utils import HOUR, MINUTE from ....utils_aiohttp import create_redirect_to_page_response from ....utils_rate_limiting import global_rate_limit_route +from ....web_utils import flash_response from ... import _confirmation_service, _security_service, _twofa_service from ..._invitations_service import ConfirmedInvitationData from ..._login_repository_legacy import ( @@ -59,7 +60,6 @@ get_plugin_options, get_plugin_settings, ) -from ...web_utils import flash_response _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py index 39390845b65..b58635817fe 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py @@ -22,6 +22,7 @@ from ....utils import MINUTE from ....utils_aiohttp import envelope_json_response from ....utils_rate_limiting import global_rate_limit_route +from ....web_utils import envelope_response, flash_response from ... import ( _auth_service, _confirmation_web, @@ -59,7 +60,6 @@ get_plugin_options, get_plugin_settings, ) -from ...web_utils import envelope_response, flash_response from .registration_schemas import ( InvitationCheck, InvitationInfo, diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py index 9d4823d2c21..6ea72434d7a 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py @@ -12,6 +12,7 @@ from ....products import products_web from ....products.models import Product from ....session.access_policies import session_access_required +from ....web_utils import envelope_response from ... import _twofa_service from ..._login_repository_legacy import AsyncpgStorage, get_plugin_storage from ..._models import InputSchema @@ -23,7 +24,6 @@ MSG_UNKNOWN_EMAIL, ) from ...settings import LoginSettingsForProduct, get_plugin_settings -from ...web_utils import envelope_response from ._rest_exceptions import handle_rest_requests_exceptions _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/login/_security_service.py b/services/web/server/src/simcore_service_webserver/login/_security_service.py index 6a8fb387b67..dc5aa049cf9 100644 --- a/services/web/server/src/simcore_service_webserver/login/_security_service.py +++ b/services/web/server/src/simcore_service_webserver/login/_security_service.py @@ -7,8 +7,8 @@ from servicelib.logging_utils import get_log_record_extra, log_context from ..security import security_web +from ..web_utils import flash_response from .constants import MSG_LOGGED_IN -from .web_utils import flash_response _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py b/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py index 23f8c7c2488..aacca38ba5b 100644 --- a/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py +++ b/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py @@ -20,7 +20,6 @@ MSG_WRONG_CAPTCHA__INVALID, ) from ....login.settings import get_plugin_settings -from ....login.web_utils import flash_response from ....login_auth.decorators import login_required from ....models import AuthenticatedRequestContext from ....products import products_web @@ -32,6 +31,7 @@ from ....users._common.schemas import PreRegisteredUserGet from ....utils import MINUTE from ....utils_rate_limiting import global_rate_limit_route +from ....web_utils import flash_response from ... import _preregistration_service from ._rest_exceptions import handle_rest_requests_exceptions diff --git a/services/web/server/src/simcore_service_webserver/login/web_utils.py b/services/web/server/src/simcore_service_webserver/web_utils.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/login/web_utils.py rename to services/web/server/src/simcore_service_webserver/web_utils.py From 35ab1d45faad8cb87f33d0c89ffaaa98db40f68f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:54:33 +0200 Subject: [PATCH 11/19] refactor: clean up auth.py and move schemas to auth_schemas.py --- .../login/_controller/rest/auth.py | 31 ++--------------- .../login/_controller/rest/auth_schemas.py | 33 +++++++++++++++++++ .../login/_controller/rest/change.py | 22 +------------ .../login/_controller/rest/change_schemas.py | 22 +++++++++++++ 4 files changed, 58 insertions(+), 50 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/login/_controller/rest/auth_schemas.py create mode 100644 services/web/server/src/simcore_service_webserver/login/_controller/rest/change_schemas.py diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py index 36c12c78517..1b39b703f04 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py @@ -3,8 +3,7 @@ from aiohttp import web from aiohttp.web import RouteTableDef from models_library.authentification import TwoFactorAuthentificationMethod -from models_library.emails import LowerCaseEmailStr -from pydantic import BaseModel, Field, PositiveInt, SecretStr, TypeAdapter +from pydantic import TypeAdapter from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import parse_request_body_as from servicelib.logging_utils import get_log_record_extra, log_context @@ -21,10 +20,8 @@ session_access_required, ) from ....users import preferences_api as user_preferences_api -from ....utils_aiohttp import NextPage from ....web_utils import envelope_response, flash_response from ... import _auth_service, _login_service, _security_service, _twofa_service -from ..._models import InputSchema from ...constants import ( CODE_2FA_EMAIL_CODE_REQUIRED, CODE_2FA_SMS_CODE_REQUIRED, @@ -42,6 +39,7 @@ from ...decorators import login_required from ...settings import LoginSettingsForProduct, get_plugin_settings from ._rest_exceptions import handle_rest_requests_exceptions +from .auth_schemas import LoginBody, LoginTwoFactorAuthBody, LogoutBody log = logging.getLogger(__name__) @@ -49,20 +47,6 @@ routes = RouteTableDef() -class LoginBody(InputSchema): - email: LowerCaseEmailStr - password: SecretStr - - -class CodePageParams(BaseModel): - message: str - expiration_2fa: PositiveInt | None = None - next_url: str | None = None - - -class LoginNextPage(NextPage[CodePageParams]): ... - - @routes.post(f"/{API_VTAG}/auth/login", name="auth_login") @on_success_grant_session_access_to( name="auth_register_phone", @@ -209,11 +193,6 @@ async def login(request: web.Request): ) -class LoginTwoFactorAuthBody(InputSchema): - email: LowerCaseEmailStr - code: SecretStr - - @routes.post(f"/{API_VTAG}/auth/validate-code-login", name="auth_login_2fa") @session_access_required( "auth_login_2fa", @@ -260,12 +239,6 @@ async def login_2fa(request: web.Request): return await _security_service.login_granted_response(request, user=dict(user)) -class LogoutBody(InputSchema): - client_session_id: str | None = Field( - None, examples=["5ac57685-c40f-448f-8711-70be1936fd63"] - ) - - @routes.post(f"/{API_VTAG}/auth/logout", name="auth_logout") @login_required @handle_rest_requests_exceptions diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth_schemas.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth_schemas.py new file mode 100644 index 00000000000..a06d2259655 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/auth_schemas.py @@ -0,0 +1,33 @@ +from aiohttp.web import RouteTableDef +from models_library.emails import LowerCaseEmailStr +from pydantic import BaseModel, Field, PositiveInt, SecretStr + +from ....utils_aiohttp import NextPage +from ..._models import InputSchema + +routes = RouteTableDef() + + +class LoginBody(InputSchema): + email: LowerCaseEmailStr + password: SecretStr + + +class CodePageParams(BaseModel): + message: str + expiration_2fa: PositiveInt | None = None + next_url: str | None = None + + +class LoginNextPage(NextPage[CodePageParams]): ... + + +class LoginTwoFactorAuthBody(InputSchema): + email: LowerCaseEmailStr + code: SecretStr + + +class LogoutBody(InputSchema): + client_session_id: str | None = Field( + None, examples=["5ac57685-c40f-448f-8711-70be1936fd63"] + ) diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py index 4ba56312008..4506a934582 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py @@ -2,8 +2,6 @@ from aiohttp import web from aiohttp.web import RouteTableDef -from models_library.emails import LowerCaseEmailStr -from pydantic import SecretStr, field_validator from servicelib.aiohttp.requests_validation import parse_request_body_as from servicelib.logging_errors import create_troubleshootting_log_kwargs from servicelib.request_keys import RQT_USERID_KEY @@ -27,7 +25,6 @@ CHANGE_EMAIL, validate_user_status, ) -from ..._models import InputSchema, create_password_match_validator from ...constants import ( MSG_CANT_SEND_MAIL, MSG_CHANGE_EMAIL_REQUESTED, @@ -38,6 +35,7 @@ ) from ...decorators import login_required from ...settings import LoginOptions, get_plugin_options +from .change_schemas import ChangeEmailBody, ChangePasswordBody, ResetPasswordBody _logger = logging.getLogger(__name__) @@ -45,10 +43,6 @@ routes = RouteTableDef() -class ResetPasswordBody(InputSchema): - email: LowerCaseEmailStr - - @routes.post(f"/{API_VTAG}/auth/reset-password", name="initiate_reset_password") @global_rate_limit_route( number_of_requests=10, interval_seconds=HOUR, error_msg=MSG_OFTEN_RESET_PASSWORD @@ -221,10 +215,6 @@ def _get_error_context( return flash_response(MSG_EMAIL_SENT.format(email=request_body.email), "INFO") -class ChangeEmailBody(InputSchema): - email: LowerCaseEmailStr - - async def initiate_change_email(request: web.Request): # NOTE: This code have been intentially disabled in https://github.com/ITISFoundation/osparc-simcore/pull/5472 db: AsyncpgStorage = get_plugin_storage(request.app) @@ -272,16 +262,6 @@ async def initiate_change_email(request: web.Request): return flash_response(MSG_CHANGE_EMAIL_REQUESTED) -class ChangePasswordBody(InputSchema): - current: SecretStr - new: SecretStr - confirm: SecretStr - - _password_confirm_match = field_validator("confirm")( - create_password_match_validator(reference_field="new") - ) - - @routes.post(f"/{API_VTAG}/auth/change-password", name="auth_change_password") @login_required async def change_password(request: web.Request): diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/change_schemas.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/change_schemas.py new file mode 100644 index 00000000000..0af207301fa --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/change_schemas.py @@ -0,0 +1,22 @@ +from models_library.emails import LowerCaseEmailStr +from pydantic import SecretStr, field_validator + +from ..._models import InputSchema, create_password_match_validator + + +class ResetPasswordBody(InputSchema): + email: LowerCaseEmailStr + + +class ChangeEmailBody(InputSchema): + email: LowerCaseEmailStr + + +class ChangePasswordBody(InputSchema): + current: SecretStr + new: SecretStr + confirm: SecretStr + + _password_confirm_match = field_validator("confirm")( + create_password_match_validator(reference_field="new") + ) From 3985f59b4f9e00c857b226631069b2e544d92f1a Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:57:56 +0200 Subject: [PATCH 12/19] split confirmation schemas --- .../login/_controller/rest/confirmation.py | 51 ++++--------------- .../_controller/rest/confirmation_schemas.py | 47 +++++++++++++++++ 2 files changed, 56 insertions(+), 42 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation_schemas.py diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py index fd5c4866d7d..986919365bb 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py @@ -1,6 +1,4 @@ import logging -from contextlib import suppress -from json import JSONDecodeError from aiohttp import web from aiohttp.web import RouteTableDef @@ -8,13 +6,7 @@ from models_library.emails import LowerCaseEmailStr from models_library.products import ProductName from pydantic import ( - BaseModel, - Field, - PositiveInt, - SecretStr, TypeAdapter, - ValidationError, - field_validator, ) from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( @@ -35,7 +27,6 @@ from ....utils_rate_limiting import global_rate_limit_route from ....web_utils import flash_response from ... import _confirmation_service, _security_service, _twofa_service -from ..._invitations_service import ConfirmedInvitationData from ..._login_repository_legacy import ( AsyncpgStorage, ConfirmationTokenDict, @@ -48,7 +39,6 @@ RESET_PASSWORD, notify_user_confirmation, ) -from ..._models import InputSchema, check_confirm_password_match from ...constants import ( MSG_PASSWORD_CHANGE_NOT_ALLOWED, MSG_PASSWORD_CHANGED, @@ -60,6 +50,12 @@ get_plugin_options, get_plugin_settings, ) +from .confirmation_schemas import ( + CodePathParam, + PhoneConfirmationBody, + ResetPasswordConfirmation, + parse_extra_credits_in_usd_or_none, +) _logger = logging.getLogger(__name__) @@ -67,20 +63,6 @@ routes = RouteTableDef() -class _PathParam(BaseModel): - code: SecretStr - - -def _parse_extra_credits_in_usd_or_none( - confirmation: ConfirmationTokenDict, -) -> PositiveInt | None: - with suppress(ValidationError, JSONDecodeError): - confirmation_data = confirmation.get("data", "EMPTY") or "EMPTY" - invitation = ConfirmedInvitationData.model_validate_json(confirmation_data) - return invitation.extra_credits_in_usd - return None - - async def _handle_confirm_registration( app: web.Application, product_name: ProductName, @@ -100,7 +82,7 @@ async def _handle_confirm_registration( app, user_id=user_id, product_name=product_name, - extra_credits_in_usd=_parse_extra_credits_in_usd_or_none(confirmation), + extra_credits_in_usd=parse_extra_credits_in_usd_or_none(confirmation), ) @@ -143,7 +125,7 @@ async def validate_confirmation_and_redirect(request: web.Request): cfg: LoginOptions = get_plugin_options(request.app) product: Product = products_web.get_current_product(request) - path_params = parse_request_path_parameters_as(_PathParam, request) + path_params = parse_request_path_parameters_as(CodePathParam, request) confirmation: ConfirmationTokenDict | None = ( await _confirmation_service.validate_confirmation_code( @@ -213,14 +195,6 @@ async def validate_confirmation_and_redirect(request: web.Request): raise web.HTTPFound(location=redirect_to_login_url) -class PhoneConfirmationBody(InputSchema): - email: LowerCaseEmailStr - phone: str = Field( - ..., description="Phone number E.164, needed on the deployments with 2FA" - ) - code: SecretStr - - @routes.post("/v0/auth/validate-code-register", name="auth_phone_confirmation") @global_rate_limit_route(number_of_requests=5, interval_seconds=MINUTE) @session_access_required( @@ -269,13 +243,6 @@ async def phone_confirmation(request: web.Request): ) -class ResetPasswordConfirmation(InputSchema): - password: SecretStr - confirm: SecretStr - - _password_confirm_match = field_validator("confirm")(check_confirm_password_match) - - @routes.post("/v0/auth/reset-password/{code}", name="complete_reset_password") @global_rate_limit_route(number_of_requests=10, interval_seconds=HOUR) async def complete_reset_password(request: web.Request): @@ -288,7 +255,7 @@ async def complete_reset_password(request: web.Request): cfg: LoginOptions = get_plugin_options(request.app) product: Product = products_web.get_current_product(request) - path_params = parse_request_path_parameters_as(_PathParam, request) + path_params = parse_request_path_parameters_as(CodePathParam, request) request_body = await parse_request_body_as(ResetPasswordConfirmation, request) confirmation = await _confirmation_service.validate_confirmation_code( diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation_schemas.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation_schemas.py new file mode 100644 index 00000000000..f1d732d4c32 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation_schemas.py @@ -0,0 +1,47 @@ +from contextlib import suppress +from json import JSONDecodeError + +from models_library.emails import LowerCaseEmailStr +from pydantic import ( + BaseModel, + Field, + PositiveInt, + SecretStr, + ValidationError, + field_validator, +) + +from ..._invitations_service import ConfirmedInvitationData +from ..._login_repository_legacy import ( + ConfirmationTokenDict, +) +from ..._models import InputSchema, check_confirm_password_match + + +class CodePathParam(BaseModel): + code: SecretStr + + +def parse_extra_credits_in_usd_or_none( + confirmation: ConfirmationTokenDict, +) -> PositiveInt | None: + with suppress(ValidationError, JSONDecodeError): + confirmation_data = confirmation.get("data", "EMPTY") or "EMPTY" + invitation = ConfirmedInvitationData.model_validate_json(confirmation_data) + return invitation.extra_credits_in_usd + return None + + +class PhoneConfirmationBody(InputSchema): + email: LowerCaseEmailStr + phone: str = Field( + ..., description="Phone number E.164, needed on the deployments with 2FA" + ) + code: SecretStr + + +class ResetPasswordConfirmation(InputSchema): + password: SecretStr + confirm: SecretStr + + _password_confirm_match = field_validator("confirm")(check_confirm_password_match) From cee1cdf1923c813659093fd42dac955834578bdf Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:59:33 +0200 Subject: [PATCH 13/19] schema split --- .../login/_controller/rest/twofa.py | 10 +--------- .../login/_controller/rest/twofa_schemas.py | 11 +++++++++++ .../unit/with_dbs/03/login/test_login_twofa_resend.py | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa_schemas.py diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py index 6ea72434d7a..018e23ee1c0 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py @@ -1,10 +1,7 @@ import logging -from typing import Literal from aiohttp import web from aiohttp.web import RouteTableDef -from models_library.emails import LowerCaseEmailStr -from pydantic import Field from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import parse_request_body_as from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON @@ -15,7 +12,6 @@ from ....web_utils import envelope_response from ... import _twofa_service from ..._login_repository_legacy import AsyncpgStorage, get_plugin_storage -from ..._models import InputSchema from ...constants import ( CODE_2FA_EMAIL_CODE_REQUIRED, CODE_2FA_SMS_CODE_REQUIRED, @@ -25,6 +21,7 @@ ) from ...settings import LoginSettingsForProduct, get_plugin_settings from ._rest_exceptions import handle_rest_requests_exceptions +from .twofa_schemas import Resend2faBody _logger = logging.getLogger(__name__) @@ -32,11 +29,6 @@ routes = RouteTableDef() -class Resend2faBody(InputSchema): - email: LowerCaseEmailStr = Field(..., description="User email (identifier)") - via: Literal["SMS", "Email"] = "SMS" - - @routes.post("/v0/auth/two_factor:resend", name="auth_resend_2fa_code") @session_access_required( name="auth_resend_2fa_code", diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa_schemas.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa_schemas.py new file mode 100644 index 00000000000..ec31efc8d44 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa_schemas.py @@ -0,0 +1,11 @@ +from typing import Literal + +from models_library.emails import LowerCaseEmailStr +from pydantic import Field + +from ..._models import InputSchema + + +class Resend2faBody(InputSchema): + email: LowerCaseEmailStr = Field(..., description="User email (identifier)") + via: Literal["SMS", "Email"] = "SMS" diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa_resend.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa_resend.py index 112982a7d68..9ab57d5d639 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa_resend.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa_resend.py @@ -13,7 +13,7 @@ from servicelib.aiohttp import status from simcore_postgres_database.models.products import ProductLoginSettingsDict, products from simcore_service_webserver.application_settings import ApplicationSettings -from simcore_service_webserver.login._controller.rest.auth import ( +from simcore_service_webserver.login._controller.rest.auth_schemas import ( CodePageParams, NextPage, ) From 182f8711338511c639e13e33d106a31ec12537cb Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:14:17 +0200 Subject: [PATCH 14/19] simplified --- .../simcore_service_webserver/login/plugin.py | 4 +- .../login_accounts/__init__.py | 3 -- .../preregistration.py => _controler_rest.py} | 44 +++++++++---------- .../login_accounts/_controller/__init__.py | 0 .../_controller/rest/__init__.py | 0 .../_controller/rest/_rest_exceptions.py | 15 ------- ...preregistration_service.py => _service.py} | 0 .../login_accounts/plugin.py | 4 +- ...login_handlers_registration_invitations.py | 2 +- .../tests/unit/with_dbs/03/test_email.py | 4 +- 10 files changed, 29 insertions(+), 47 deletions(-) delete mode 100644 services/web/server/src/simcore_service_webserver/login_accounts/__init__.py rename services/web/server/src/simcore_service_webserver/login_accounts/{_controller/rest/preregistration.py => _controler_rest.py} (82%) delete mode 100644 services/web/server/src/simcore_service_webserver/login_accounts/_controller/__init__.py delete mode 100644 services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/__init__.py delete mode 100644 services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/_rest_exceptions.py rename services/web/server/src/simcore_service_webserver/login_accounts/{_preregistration_service.py => _service.py} (100%) diff --git a/services/web/server/src/simcore_service_webserver/login/plugin.py b/services/web/server/src/simcore_service_webserver/login/plugin.py index eed630189bb..ad8a74772b4 100644 --- a/services/web/server/src/simcore_service_webserver/login/plugin.py +++ b/services/web/server/src/simcore_service_webserver/login/plugin.py @@ -23,7 +23,7 @@ from ..email.plugin import setup_email from ..email.settings import get_plugin_settings as get_email_plugin_settings from ..invitations.plugin import setup_invitations -from ..login_accounts._controller.rest import preregistration +from ..login_accounts._controller.rest import _controler_rest from ..login_auth.plugin import setup_login_auth from ..products import products_service from ..products.models import ProductName @@ -152,7 +152,7 @@ def setup_login(app: web.Application): app.router.add_routes(confirmation.routes) app.router.add_routes(registration.routes) - app.router.add_routes(preregistration.routes) + app.router.add_routes(_controler_rest.routes) app.router.add_routes(change.routes) app.router.add_routes(twofa.routes) diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/__init__.py b/services/web/server/src/simcore_service_webserver/login_accounts/__init__.py deleted file mode 100644 index aded3db0d87..00000000000 --- a/services/web/server/src/simcore_service_webserver/login_accounts/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Account management -""" diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py b/services/web/server/src/simcore_service_webserver/login_accounts/_controler_rest.py similarity index 82% rename from services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py rename to services/web/server/src/simcore_service_webserver/login_accounts/_controler_rest.py index aacca38ba5b..90d555f5464 100644 --- a/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/preregistration.py +++ b/services/web/server/src/simcore_service_webserver/login_accounts/_controler_rest.py @@ -12,28 +12,28 @@ from servicelib.logging_utils import get_log_record_extra, log_context from servicelib.utils import fire_and_forget_task -from ...._meta import API_VTAG -from ....login import login_service -from ....login.constants import ( +from .._meta import API_VTAG +from ..login import login_service +from ..login.constants import ( CAPTCHA_SESSION_KEY, MSG_LOGGED_OUT, MSG_WRONG_CAPTCHA__INVALID, ) -from ....login.settings import get_plugin_settings -from ....login_auth.decorators import login_required -from ....models import AuthenticatedRequestContext -from ....products import products_web -from ....products.models import Product -from ....security import security_service, security_web -from ....security.decorators import permission_required -from ....session import api as session_service -from ....users import api as users_service -from ....users._common.schemas import PreRegisteredUserGet -from ....utils import MINUTE -from ....utils_rate_limiting import global_rate_limit_route -from ....web_utils import flash_response -from ... import _preregistration_service -from ._rest_exceptions import handle_rest_requests_exceptions +from ..login.settings import get_plugin_settings +from ..login_auth.decorators import login_required +from ..models import AuthenticatedRequestContext +from ..products import products_web +from ..products.models import Product +from ..security import security_service, security_web +from ..security.decorators import permission_required +from ..session import api as session_service +from ..users import api as users_service +from ..users._common.schemas import PreRegisteredUserGet +from ..utils import MINUTE +from ..utils_rate_limiting import global_rate_limit_route +from ..web_utils import flash_response +from . import _service +from ._controller._rest_exceptions import handle_rest_requests_exceptions _logger = logging.getLogger(__name__) @@ -62,7 +62,7 @@ def _get_ipinfo(request: web.Request) -> dict[str, Any]: async def create_captcha(request: web.Request): session = await session_service.get_session(request) - captcha_text, image_data = await _preregistration_service.create_captcha() + captcha_text, image_data = await _service.create_captcha() # Store captcha text in session session[CAPTCHA_SESSION_KEY] = captcha_text @@ -89,7 +89,7 @@ async def request_product_account(request: web.Request): session.pop(CAPTCHA_SESSION_KEY, None) # create pre-regiatration or raise if already exists - await _preregistration_service.create_pre_registration( + await _service.create_pre_registration( request.app, profile=PreRegisteredUserGet.model_validate(body.form), product_name=product.name, @@ -97,7 +97,7 @@ async def request_product_account(request: web.Request): # if created send email to fogbugz or user itself fire_and_forget_task( - _preregistration_service.send_account_request_email_to_support( + _service.send_account_request_email_to_support( request=request, product=product, request_form=body.form, @@ -150,7 +150,7 @@ async def unregister_account(request: web.Request): # send email in the background fire_and_forget_task( - _preregistration_service.send_close_account_email( + _service.send_close_account_email( request, user_email=credentials.email, user_first_name=credentials.display_name, diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/_controller/__init__.py b/services/web/server/src/simcore_service_webserver/login_accounts/_controller/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/__init__.py b/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/_rest_exceptions.py b/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/_rest_exceptions.py deleted file mode 100644 index 529f0a949b0..00000000000 --- a/services/web/server/src/simcore_service_webserver/login_accounts/_controller/rest/_rest_exceptions.py +++ /dev/null @@ -1,15 +0,0 @@ -from ....exception_handling import ( - ExceptionToHttpErrorMap, - exception_handling_decorator, - to_exceptions_handlers_map, -) -from ....login._controller.rest._rest_exceptions import ( - _TO_HTTP_ERROR_MAP as LOGIN_TO_HTTP_ERROR_MAP, -) - -_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {**LOGIN_TO_HTTP_ERROR_MAP} - - -handle_rest_requests_exceptions = exception_handling_decorator( - to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP) -) diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/_preregistration_service.py b/services/web/server/src/simcore_service_webserver/login_accounts/_service.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/login_accounts/_preregistration_service.py rename to services/web/server/src/simcore_service_webserver/login_accounts/_service.py diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/plugin.py b/services/web/server/src/simcore_service_webserver/login_accounts/plugin.py index cd0ca8fde2e..f41d751748b 100644 --- a/services/web/server/src/simcore_service_webserver/login_accounts/plugin.py +++ b/services/web/server/src/simcore_service_webserver/login_accounts/plugin.py @@ -3,11 +3,11 @@ from aiohttp import web from servicelib.aiohttp.application_setup import ensure_single_setup -from ._controller.rest import preregistration as _controller_rest_preregistration +from . import _controler_rest _logger = logging.getLogger(__name__) @ensure_single_setup(__name__, logger=_logger) def setup_login_account(app: web.Application): - app.add_routes(_controller_rest_preregistration.routes) + app.add_routes(_controler_rest.routes) diff --git a/services/web/server/tests/unit/with_dbs/03/invitations/test_login_handlers_registration_invitations.py b/services/web/server/tests/unit/with_dbs/03/invitations/test_login_handlers_registration_invitations.py index 8bb512d5d4c..20f2b0caf92 100644 --- a/services/web/server/tests/unit/with_dbs/03/invitations/test_login_handlers_registration_invitations.py +++ b/services/web/server/tests/unit/with_dbs/03/invitations/test_login_handlers_registration_invitations.py @@ -78,7 +78,7 @@ async def test_check_registration_invitations_with_old_code( assert invitation.email is None -@pytest.mark.acceptance_test() +@pytest.mark.acceptance_test async def test_check_registration_invitation_and_get_email( client: TestClient, mocker: MockerFixture, diff --git a/services/web/server/tests/unit/with_dbs/03/test_email.py b/services/web/server/tests/unit/with_dbs/03/test_email.py index 2abcf0ec24c..2e5012834ed 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_email.py +++ b/services/web/server/tests/unit/with_dbs/03/test_email.py @@ -30,10 +30,10 @@ from simcore_service_webserver.email._core import _remove_comments, _render_template from simcore_service_webserver.email._handlers import EmailTestFailed, EmailTestPassed from simcore_service_webserver.email.plugin import setup_email -from simcore_service_webserver.login_accounts._controller.rest.preregistration import ( +from simcore_service_webserver.login_accounts._controller.rest._controler_rest import ( _get_ipinfo, ) -from simcore_service_webserver.login_accounts._preregistration_service import ( +from simcore_service_webserver.login_accounts._service import ( _json_encoder_and_dumps, ) From c52dfd7353ec73989eed377af429121f64641bdd Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:15:08 +0200 Subject: [PATCH 15/19] refactor --- .../web/server/src/simcore_service_webserver/login/plugin.py | 2 -- .../simcore_service_webserver/login_accounts/_controler_rest.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/login/plugin.py b/services/web/server/src/simcore_service_webserver/login/plugin.py index ad8a74772b4..bd1a466e2ba 100644 --- a/services/web/server/src/simcore_service_webserver/login/plugin.py +++ b/services/web/server/src/simcore_service_webserver/login/plugin.py @@ -23,7 +23,6 @@ from ..email.plugin import setup_email from ..email.settings import get_plugin_settings as get_email_plugin_settings from ..invitations.plugin import setup_invitations -from ..login_accounts._controller.rest import _controler_rest from ..login_auth.plugin import setup_login_auth from ..products import products_service from ..products.models import ProductName @@ -152,7 +151,6 @@ def setup_login(app: web.Application): app.router.add_routes(confirmation.routes) app.router.add_routes(registration.routes) - app.router.add_routes(_controler_rest.routes) app.router.add_routes(change.routes) app.router.add_routes(twofa.routes) diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/_controler_rest.py b/services/web/server/src/simcore_service_webserver/login_accounts/_controler_rest.py index 90d555f5464..99eb486693b 100644 --- a/services/web/server/src/simcore_service_webserver/login_accounts/_controler_rest.py +++ b/services/web/server/src/simcore_service_webserver/login_accounts/_controler_rest.py @@ -14,6 +14,7 @@ from .._meta import API_VTAG from ..login import login_service +from ..login._controller.rest._rest_exceptions import handle_rest_requests_exceptions from ..login.constants import ( CAPTCHA_SESSION_KEY, MSG_LOGGED_OUT, @@ -33,7 +34,6 @@ from ..utils_rate_limiting import global_rate_limit_route from ..web_utils import flash_response from . import _service -from ._controller._rest_exceptions import handle_rest_requests_exceptions _logger = logging.getLogger(__name__) From 1b9c86b9169686ea619b6019ebbbaf02d45c2d60 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:31:37 +0200 Subject: [PATCH 16/19] feat: implement captcha creation and account registration endpoints --- ..._controler_rest.py => _controller_rest.py} | 0 .../login_accounts/_service.py | 25 +++++++++++-------- .../login_accounts/plugin.py | 4 +-- .../test_login_accounts_preregistration.py | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) rename services/web/server/src/simcore_service_webserver/login_accounts/{_controler_rest.py => _controller_rest.py} (100%) diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/_controler_rest.py b/services/web/server/src/simcore_service_webserver/login_accounts/_controller_rest.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/login_accounts/_controler_rest.py rename to services/web/server/src/simcore_service_webserver/login_accounts/_controller_rest.py diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/_service.py b/services/web/server/src/simcore_service_webserver/login_accounts/_service.py index d76e7c3b365..ce5c2ccf7dd 100644 --- a/services/web/server/src/simcore_service_webserver/login_accounts/_service.py +++ b/services/web/server/src/simcore_service_webserver/login_accounts/_service.py @@ -1,3 +1,4 @@ +import asyncio import functools import logging from io import BytesIO @@ -112,19 +113,23 @@ async def send_account_request_email_to_support( async def create_captcha() -> tuple[str, bytes]: - captcha_text = generate_passcode(number_of_digits=6) - image = ImageCaptcha(width=140, height=45) + def _run() -> tuple[str, bytes]: + captcha_text = generate_passcode(number_of_digits=6) + image = ImageCaptcha(width=140, height=45) - # Generate image - data: Image = image.create_captcha_image( - chars=captcha_text, color=(221, 221, 221), background=(0, 20, 46) - ) + # Generate image + data: Image = image.create_captcha_image( + chars=captcha_text, color=(221, 221, 221), background=(0, 20, 46) + ) + + img_byte_arr = BytesIO() + data.save(img_byte_arr, format="PNG") + image_data = img_byte_arr.getvalue() - img_byte_arr = BytesIO() - data.save(img_byte_arr, format="PNG") - image_data = img_byte_arr.getvalue() + return (captcha_text, image_data) - return (captcha_text, image_data) + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _run) async def create_pre_registration( diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/plugin.py b/services/web/server/src/simcore_service_webserver/login_accounts/plugin.py index f41d751748b..08273499596 100644 --- a/services/web/server/src/simcore_service_webserver/login_accounts/plugin.py +++ b/services/web/server/src/simcore_service_webserver/login_accounts/plugin.py @@ -3,11 +3,11 @@ from aiohttp import web from servicelib.aiohttp.application_setup import ensure_single_setup -from . import _controler_rest +from . import _controller_rest _logger = logging.getLogger(__name__) @ensure_single_setup(__name__, logger=_logger) def setup_login_account(app: web.Application): - app.add_routes(_controler_rest.routes) + app.add_routes(_controller_rest.routes) diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_accounts_preregistration.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_accounts_preregistration.py index 85b4343f9fb..bf4e2d894ce 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_accounts_preregistration.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_accounts_preregistration.py @@ -60,7 +60,7 @@ def mocked_send_email(mocker: MockerFixture) -> MagicMock: @pytest.fixture def mocked_captcha_session(mocker: MockerFixture) -> MagicMock: return mocker.patch( - "simcore_service_webserver.login._controller.rest.preregistration.session_service.get_session", + "simcore_service_webserver.login_accounts._controller_rest.session_service.get_session", spec=True, return_value={"captcha": "123456"}, ) From 73f2cb0b93ceb38b54a574f97a82622709fb03a3 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 18:54:28 +0200 Subject: [PATCH 17/19] fixes pylint --- services/web/server/tests/unit/with_dbs/03/test_email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/server/tests/unit/with_dbs/03/test_email.py b/services/web/server/tests/unit/with_dbs/03/test_email.py index 2e5012834ed..5141bd862db 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_email.py +++ b/services/web/server/tests/unit/with_dbs/03/test_email.py @@ -30,7 +30,7 @@ from simcore_service_webserver.email._core import _remove_comments, _render_template from simcore_service_webserver.email._handlers import EmailTestFailed, EmailTestPassed from simcore_service_webserver.email.plugin import setup_email -from simcore_service_webserver.login_accounts._controller.rest._controler_rest import ( +from simcore_service_webserver.login_accounts._controller_rest import ( _get_ipinfo, ) from simcore_service_webserver.login_accounts._service import ( From 265c6190667a9f2fcca8ad48768574651aadf864 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 8 Jul 2025 18:56:18 +0200 Subject: [PATCH 18/19] fixes import --- .../src/simcore_service_webserver/login_accounts/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/login_accounts/__init__.py diff --git a/services/web/server/src/simcore_service_webserver/login_accounts/__init__.py b/services/web/server/src/simcore_service_webserver/login_accounts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 1f658a2114b49765f66753ebfbe2bdfd031a399b Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 9 Jul 2025 14:14:21 +0200 Subject: [PATCH 19/19] fixes imiports --- api/specs/web-server/_auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/specs/web-server/_auth.py b/api/specs/web-server/_auth.py index b17ab6ad238..cc427a189cd 100644 --- a/api/specs/web-server/_auth.py +++ b/api/specs/web-server/_auth.py @@ -15,7 +15,7 @@ from models_library.rest_error import EnvelopedError, Log from pydantic import BaseModel, Field, confloat from simcore_service_webserver._meta import API_VTAG -from simcore_service_webserver.login._controller.rest.auth import ( +from simcore_service_webserver.login._controller.rest.auth_schemas import ( LoginBody, LoginNextPage, LoginTwoFactorAuthBody, @@ -30,7 +30,7 @@ PhoneConfirmationBody, ResetPasswordConfirmation, ) -from simcore_service_webserver.login._controller.rest.registration import ( +from simcore_service_webserver.login._controller.rest.registration_schemas import ( InvitationCheck, InvitationInfo, RegisterBody,