diff --git a/api/specs/web-server/_users.py b/api/specs/web-server/_users.py index 89d5eaaba2f..d0d733a01e3 100644 --- a/api/specs/web-server/_users.py +++ b/api/specs/web-server/_users.py @@ -39,26 +39,14 @@ "/me", response_model=Envelope[MyProfileGet], ) -async def get_my_profile(): - ... +async def get_my_profile(): ... @router.patch( "/me", status_code=status.HTTP_204_NO_CONTENT, ) -async def update_my_profile(_body: MyProfilePatch): - ... - - -@router.put( - "/me", - status_code=status.HTTP_204_NO_CONTENT, - deprecated=True, - description="Use PATCH instead", -) -async def replace_my_profile(_body: MyProfilePatch): - ... +async def update_my_profile(_body: MyProfilePatch): ... @router.patch( @@ -68,16 +56,14 @@ async def replace_my_profile(_body: MyProfilePatch): async def set_frontend_preference( preference_id: PreferenceIdentifier, _body: PatchRequestBody, -): - ... +): ... @router.get( "/me/tokens", response_model=Envelope[list[MyTokenGet]], ) -async def list_tokens(): - ... +async def list_tokens(): ... @router.post( @@ -85,8 +71,7 @@ async def list_tokens(): response_model=Envelope[MyTokenGet], status_code=status.HTTP_201_CREATED, ) -async def create_token(_body: MyTokenCreate): - ... +async def create_token(_body: MyTokenCreate): ... @router.get( @@ -95,24 +80,21 @@ async def create_token(_body: MyTokenCreate): ) async def get_token( _path: Annotated[_TokenPathParams, Depends()], -): - ... +): ... @router.delete( "/me/tokens/{service}", status_code=status.HTTP_204_NO_CONTENT, ) -async def delete_token(_path: Annotated[_TokenPathParams, Depends()]): - ... +async def delete_token(_path: Annotated[_TokenPathParams, Depends()]): ... @router.get( "/me/notifications", response_model=Envelope[list[UserNotification]], ) -async def list_user_notifications(): - ... +async def list_user_notifications(): ... @router.post( @@ -121,8 +103,7 @@ async def list_user_notifications(): ) async def create_user_notification( _body: UserNotificationCreate, -): - ... +): ... @router.patch( @@ -132,16 +113,14 @@ async def create_user_notification( async def mark_notification_as_read( _path: Annotated[_NotificationPathParams, Depends()], _body: UserNotificationPatch, -): - ... +): ... @router.get( "/me/permissions", response_model=Envelope[list[MyPermissionGet]], ) -async def list_user_permissions(): - ... +async def list_user_permissions(): ... # @@ -154,8 +133,7 @@ async def list_user_permissions(): response_model=Envelope[list[UserGet]], description="Search among users who are publicly visible to the caller (i.e., me) based on their privacy settings.", ) -async def search_users(_body: UsersSearch): - ... +async def search_users(_body: UsersSearch): ... # @@ -171,7 +149,7 @@ async def search_users(_body: UsersSearch): tags=_extra_tags, ) async def search_users_for_admin( - _query: Annotated[UsersForAdminSearchQueryParams, Depends()] + _query: Annotated[UsersForAdminSearchQueryParams, Depends()], ): # NOTE: see `Search` in `Common Custom Methods` in https://cloud.google.com/apis/design/custom_methods ... @@ -182,5 +160,4 @@ async def search_users_for_admin( response_model=Envelope[UserForAdminGet], tags=_extra_tags, ) -async def pre_register_user_for_admin(_body: PreRegisteredUserGet): - ... +async def pre_register_user_for_admin(_body: PreRegisteredUserGet): ... diff --git a/packages/models-library/src/models_library/api_schemas_webserver/users.py b/packages/models-library/src/models_library/api_schemas_webserver/users.py index 947623fc956..1facf8bb1e9 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/users.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/users.py @@ -16,6 +16,7 @@ ValidationInfo, field_validator, ) +from pydantic.config import JsonDict from ..basic_types import IDStr from ..emails import LowerCaseEmailStr @@ -46,11 +47,13 @@ class MyProfilePrivacyGet(OutputSchema): + hide_username: bool hide_fullname: bool hide_email: bool class MyProfilePrivacyPatch(InputSchema): + hide_username: bool | None = None hide_fullname: bool | None = None hide_email: bool | None = None @@ -79,23 +82,33 @@ class MyProfileGet(OutputSchemaWithoutCamelCase): privacy: MyProfilePrivacyGet preferences: AggregatedPreferences + @staticmethod + def _update_json_schema_extra(schema: JsonDict) -> None: + schema.update( + { + "examples": [ + { + "id": 42, + "login": "bla@foo.com", + "userName": "bla42", + "role": "admin", # pre + "expirationDate": "2022-09-14", # optional + "preferences": {}, + "privacy": { + "hide_username": 0, + "hide_fullname": 0, + "hide_email": 1, + }, + }, + ] + } + ) + model_config = ConfigDict( # NOTE: old models have an hybrid between snake and camel cases! # Should be unified at some point populate_by_name=True, - json_schema_extra={ - "examples": [ - { - "id": 42, - "login": "bla@foo.com", - "userName": "bla42", - "role": "admin", # pre - "expirationDate": "2022-09-14", # optional - "preferences": {}, - "privacy": {"hide_fullname": 0, "hide_email": 1}, - }, - ] - }, + json_schema_extra=_update_json_schema_extra, ) @field_validator("role", mode="before") diff --git a/packages/postgres-database/src/simcore_postgres_database/utils_users.py b/packages/postgres-database/src/simcore_postgres_database/utils_users.py index 7aa5c442d37..c35123c9545 100644 --- a/packages/postgres-database/src/simcore_postgres_database/utils_users.py +++ b/packages/postgres-database/src/simcore_postgres_database/utils_users.py @@ -27,10 +27,10 @@ class UserNotFoundInRepoError(BaseUserRepoError): # NOTE: see MyProfilePatch.user_name -_MIN_USERNAME_LEN: Final[int] = 4 +MIN_USERNAME_LEN: Final[int] = 4 -def _generate_random_chars(length: int = _MIN_USERNAME_LEN) -> str: +def _generate_random_chars(length: int = MIN_USERNAME_LEN) -> str: """returns `length` random digit character""" return "".join(secrets.choice(string.digits) for _ in range(length)) @@ -42,8 +42,8 @@ def _generate_username_from_email(email: str) -> str: username = re.sub(r"[^a-zA-Z0-9]", "", username).lower() # Ensure the username is at least 4 characters long - if len(username) < _MIN_USERNAME_LEN: - username += _generate_random_chars(length=_MIN_USERNAME_LEN - len(username)) + if len(username) < MIN_USERNAME_LEN: + username += _generate_random_chars(length=MIN_USERNAME_LEN - len(username)) return username diff --git a/services/web/server/VERSION b/services/web/server/VERSION index d1952dc561e..bf54d53ec26 100644 --- a/services/web/server/VERSION +++ b/services/web/server/VERSION @@ -1 +1 @@ -0.61.3 +0.61.4 diff --git a/services/web/server/setup.cfg b/services/web/server/setup.cfg index 9d731c41639..ccbfa6b24c9 100644 --- a/services/web/server/setup.cfg +++ b/services/web/server/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.61.3 +current_version = 0.61.4 commit = True message = services/webserver api version: {current_version} → {new_version} tag = False diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index b6e931af533..6facdd9ddf1 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: simcore-service-webserver description: Main service with an interface (http-API & websockets) to the web front-end - version: 0.61.3 + version: 0.61.4 servers: - url: '' description: webserver @@ -1180,22 +1180,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_MyProfileGet_' - put: - tags: - - users - summary: Replace My Profile - description: Use PATCH instead - operationId: replace_my_profile - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/MyProfilePatch' - required: true - responses: - '204': - description: Successful Response - deprecated: true patch: tags: - users @@ -11734,6 +11718,9 @@ components: last_name: Crespo MyProfilePrivacyGet: properties: + hideUsername: + type: boolean + title: Hideusername hideFullname: type: boolean title: Hidefullname @@ -11742,11 +11729,17 @@ components: title: Hideemail type: object required: + - hideUsername - hideFullname - hideEmail title: MyProfilePrivacyGet MyProfilePrivacyPatch: properties: + hideUsername: + anyOf: + - type: boolean + - type: 'null' + title: Hideusername hideFullname: anyOf: - type: boolean diff --git a/services/web/server/src/simcore_service_webserver/users/_common/models.py b/services/web/server/src/simcore_service_webserver/users/_common/models.py index 513d8bed102..967f010d0b0 100644 --- a/services/web/server/src/simcore_service_webserver/users/_common/models.py +++ b/services/web/server/src/simcore_service_webserver/users/_common/models.py @@ -55,6 +55,7 @@ class ToUserUpdateDB(BaseModel): first_name: str | None = None last_name: str | None = None + privacy_hide_username: bool | None = None privacy_hide_fullname: bool | None = None privacy_hide_email: bool | None = None diff --git a/services/web/server/src/simcore_service_webserver/users/_users_rest.py b/services/web/server/src/simcore_service_webserver/users/_users_rest.py index e3c143cd7cf..e89814e5e2d 100644 --- a/services/web/server/src/simcore_service_webserver/users/_users_rest.py +++ b/services/web/server/src/simcore_service_webserver/users/_users_rest.py @@ -113,9 +113,6 @@ async def get_my_profile(request: web.Request) -> web.Response: @routes.patch(f"/{API_VTAG}/me", name="update_my_profile") -@routes.put( - f"/{API_VTAG}/me", name="replace_my_profile" # deprecated. Use patch instead -) @login_required @permission_required("user.profile.update") @_handle_users_exceptions diff --git a/services/web/server/tests/unit/isolated/test_users_models.py b/services/web/server/tests/unit/isolated/test_users_models.py index 99fdc7f4feb..e61f543e211 100644 --- a/services/web/server/tests/unit/isolated/test_users_models.py +++ b/services/web/server/tests/unit/isolated/test_users_models.py @@ -33,7 +33,9 @@ def fake_profile_get(faker: Faker) -> MyProfileGet: user_name=fake_profile["username"], login=fake_profile["mail"], role="USER", - privacy=MyProfilePrivacyGet(hide_fullname=True, hide_email=True), + privacy=MyProfilePrivacyGet( + hide_fullname=True, hide_email=True, hide_username=False + ), preferences={}, ) @@ -78,7 +80,7 @@ def test_parsing_output_of_get_user_profile(): "last_name": "", "role": "Guest", "gravatar_id": "9d5e02c75fcd4bce1c8861f219f7f8a5", - "privacy": {"hide_email": True, "hide_fullname": False}, + "privacy": {"hide_email": True, "hide_fullname": False, "hide_username": False}, "groups": { "me": { "gid": 2, @@ -125,7 +127,7 @@ def test_mapping_update_models_from_rest_to_db(): { "first_name": "foo", "userName": "foo1234", - "privacy": {"hideFullname": False}, + "privacy": {"hideFullname": False, "hideUsername": True}, } ) @@ -137,6 +139,7 @@ def test_mapping_update_models_from_rest_to_db(): "first_name": "foo", "name": "foo1234", "privacy_hide_fullname": False, + "privacy_hide_username": True, } diff --git a/services/web/server/tests/unit/with_dbs/03/test_users.py b/services/web/server/tests/unit/with_dbs/03/test_users.py index 2d816aa67b1..c4008f75235 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users.py @@ -503,6 +503,7 @@ async def test_profile_workflow( assert updated_profile.user_name == "odei123" assert updated_profile.privacy != my_profile.privacy + assert updated_profile.privacy.hide_username == my_profile.privacy.hide_username assert updated_profile.privacy.hide_email == my_profile.privacy.hide_email assert updated_profile.privacy.hide_fullname != my_profile.privacy.hide_fullname