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, 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/__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/_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..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,9 +20,9 @@ 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 ..._constants import ( +from ...constants import ( CODE_2FA_EMAIL_CODE_REQUIRED, CODE_2FA_SMS_CODE_REQUIRED, CODE_PHONE_NUMBER_REQUIRED, @@ -37,10 +36,10 @@ 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 +from .auth_schemas import LoginBody, LoginTwoFactorAuthBody, LogoutBody log = logging.getLogger(__name__) @@ -48,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", @@ -134,7 +119,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 +154,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 +181,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": { @@ -208,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", @@ -259,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 @@ -282,7 +256,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/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 6f0a6992338..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 @@ -18,26 +16,26 @@ 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 ..._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 ( ACTIVE, CHANGE_EMAIL, - flash_response, 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 +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") + ) 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..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 ( @@ -33,13 +25,8 @@ 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 ..._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, ConfirmationTokenDict, @@ -50,16 +37,25 @@ CHANGE_EMAIL, REGISTRATION, RESET_PASSWORD, - flash_response, 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, 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) 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..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 @@ -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,17 +20,15 @@ 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, _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 ....web_utils import envelope_response, flash_response +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 ( @@ -55,18 +43,29 @@ 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 +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, get_plugin_options, get_plugin_settings, ) +from .registration_schemas import ( + InvitationCheck, + InvitationInfo, + RegisterBody, + RegisterPhoneBody, +) _logger = logging.getLogger(__name__) @@ -74,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", @@ -118,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): """ @@ -331,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", @@ -398,7 +349,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/registration_schemas.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py new file mode 100644 index 00000000000..ac2c4f9fed1 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration_schemas.py @@ -0,0 +1,68 @@ +import logging +from typing import Annotated, 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: Annotated[str, Field(description="Invitation code")] + + +class InvitationInfo(InputSchema): + email: Annotated[ + LowerCaseEmailStr | None, + Field(description="Email associated to invitation or None"), + ] = None + + +class RegisterBody(InputSchema): + email: LowerCaseEmailStr + password: SecretStr + 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( + 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: 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 = Field("user", deprecated=True) + level: Literal["INFO", "WARNING", "ERROR"] = "INFO" + message: str 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..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 @@ -12,19 +9,19 @@ 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 ..._constants import ( +from ..._login_repository_legacy import AsyncpgStorage, get_plugin_storage +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 +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/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..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,18 +1,14 @@ -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 -from ._constants import ( +from .constants import ( MSG_ACTIVATION_REQUIRED, MSG_USER_BANNED, MSG_USER_DELETED, @@ -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/_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/_registration_service.py b/services/web/server/src/simcore_service_webserver/login/_registration_service.py new file mode 100644 index 00000000000..25a0ad56cfc --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login/_registration_service.py @@ -0,0 +1,2 @@ +def get_user_name_from_email(email: str) -> str: + return email.split("@")[0] 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..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 ._constants import MSG_LOGGED_IN -from ._login_service import flash_response +from ..web_utils 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..bd1a466e2ba 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_accounts.plugin import setup_login_account from ..constants import ( APP_PUBLIC_CONFIG_PER_PRODUCT, @@ -28,16 +29,15 @@ 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, @@ -147,10 +147,10 @@ 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) - app.router.add_routes(preregistration.routes) app.router.add_routes(change.routes) app.router.add_routes(twofa.routes) 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_accounts/__init__.py b/services/web/server/src/simcore_service_webserver/login_accounts/__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_accounts/_controller_rest.py similarity index 74% rename from services/web/server/src/simcore_service_webserver/login/_controller/rest/preregistration.py rename to services/web/server/src/simcore_service_webserver/login_accounts/_controller_rest.py index bad370c31a0..99eb486693b 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/preregistration.py +++ b/services/web/server/src/simcore_service_webserver/login_accounts/_controller_rest.py @@ -6,43 +6,38 @@ 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 ....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 ... import _preregistration_service -from ..._constants import ( +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, 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 +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 _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) @@ -58,6 +53,23 @@ 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 _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", @@ -77,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, @@ -85,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, @@ -97,23 +109,16 @@ 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) - 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( @@ -137,7 +142,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") @@ -145,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, @@ -156,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") diff --git a/services/web/server/src/simcore_service_webserver/login/_preregistration_service.py b/services/web/server/src/simcore_service_webserver/login_accounts/_service.py similarity index 85% rename from services/web/server/src/simcore_service_webserver/login/_preregistration_service.py rename to services/web/server/src/simcore_service_webserver/login_accounts/_service.py index 0c615991daf..ce5c2ccf7dd 100644 --- a/services/web/server/src/simcore_service_webserver/login/_preregistration_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,23 +113,27 @@ 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( - 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_accounts/plugin.py b/services/web/server/src/simcore_service_webserver/login_accounts/plugin.py new file mode 100644 index 00000000000..08273499596 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/login_accounts/plugin.py @@ -0,0 +1,13 @@ +import logging + +from aiohttp import web +from servicelib.aiohttp.application_setup import ensure_single_setup + +from . import _controller_rest + +_logger = logging.getLogger(__name__) + + +@ensure_single_setup(__name__, logger=_logger) +def setup_login_account(app: web.Application): + app.add_routes(_controller_rest.routes) diff --git a/services/web/server/src/simcore_service_webserver/web_utils.py b/services/web/server/src/simcore_service_webserver/web_utils.py new file mode 100644 index 00000000000..31b57e4cbeb --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/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/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/login/test_login_preregistration.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_accounts_preregistration.py similarity index 97% 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 index 9418b324c64..bf4e2d894ce 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_accounts_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 @@ -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"}, ) 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_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..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,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 ( +from simcore_service_webserver.login._controller.rest.auth_schemas 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..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,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_accounts._controller_rest import ( _get_ipinfo, ) -from simcore_service_webserver.login._preregistration_service import ( +from simcore_service_webserver.login_accounts._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,