Skip to content

🎨 web-api: Add privacy Field to Profile Endpoints and Retire Legacy Entrypoint #7408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 14 additions & 37 deletions api/specs/web-server/_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -68,25 +56,22 @@ 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(
"/me/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(
Expand All @@ -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(
Expand All @@ -121,8 +103,7 @@ async def list_user_notifications():
)
async def create_user_notification(
_body: UserNotificationCreate,
):
...
): ...


@router.patch(
Expand All @@ -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(): ...


#
Expand All @@ -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): ...


#
Expand All @@ -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
...
Expand All @@ -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): ...
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
ValidationInfo,
field_validator,
)
from pydantic.config import JsonDict

from ..basic_types import IDStr
from ..emails import LowerCaseEmailStr
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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": "[email protected]",
"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": "[email protected]",
"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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion services/web/server/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.61.3
0.61.4
2 changes: 1 addition & 1 deletion services/web/server/setup.cfg
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -11734,6 +11718,9 @@ components:
last_name: Crespo
MyProfilePrivacyGet:
properties:
hideUsername:
type: boolean
title: Hideusername
hideFullname:
type: boolean
title: Hidefullname
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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={},
)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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},
}
)

Expand All @@ -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,
}


Expand Down
1 change: 1 addition & 0 deletions services/web/server/tests/unit/with_dbs/03/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading