Skip to content
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

✨ web-api: user's privacy settings #6904

Merged
merged 37 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5750bbd
return username in profile
pcrespov Dec 4, 2024
1233bd1
updates OAS
pcrespov Dec 4, 2024
8b7e0b4
services/webserver api version: 0.47.0 → 0.48.0
pcrespov Dec 4, 2024
17bed97
cleanup
pcrespov Dec 4, 2024
6337748
cleanup tests
pcrespov Dec 4, 2024
2eb0d91
patch
pcrespov Dec 4, 2024
78b93ec
new table
pcrespov Dec 4, 2024
23aa9c4
adding privacy
pcrespov Dec 4, 2024
6d54ef3
patch
pcrespov Dec 5, 2024
be44ee4
move privacy cols to user
pcrespov Dec 5, 2024
ad92c5a
updates users plugin
pcrespov Dec 5, 2024
06f13b8
drafts tests
pcrespov Dec 5, 2024
7aebbbf
migration
pcrespov Dec 5, 2024
e4594d8
test pss
pcrespov Dec 5, 2024
9ffb6fb
warning
pcrespov Dec 5, 2024
f8a019d
fixes migration
pcrespov Dec 5, 2024
fb80984
updates tests
pcrespov Dec 5, 2024
245d841
udpates OAS
pcrespov Dec 5, 2024
51fe8e3
examples
pcrespov Dec 5, 2024
279a1c4
cleanup
pcrespov Dec 5, 2024
baeba07
updates OAS
pcrespov Dec 5, 2024
982fb6f
update username
pcrespov Dec 5, 2024
bfa425d
tests models
pcrespov Dec 5, 2024
f15d8fc
model conversion ready
pcrespov Dec 5, 2024
abb4aa4
cleanup
pcrespov Dec 5, 2024
1ab8b22
handles errors
pcrespov Dec 5, 2024
174e9c3
changes
pcrespov Dec 5, 2024
c66c520
fixes tests
pcrespov Dec 5, 2024
4ed3e04
moves package to models library
pcrespov Dec 5, 2024
ffe4ebf
minor
pcrespov Dec 5, 2024
1fc1195
update OAS
pcrespov Dec 5, 2024
2f54321
updates tests
pcrespov Dec 5, 2024
5e10c29
updates tests
pcrespov Dec 5, 2024
d16e923
cleanup
pcrespov Dec 6, 2024
f6a3ead
api-removed-without-deprecation
pcrespov Dec 6, 2024
0eadc8c
fixes test
pcrespov Dec 6, 2024
5f6c627
Merge branch 'master' into is1779/user-api
odeimaiz Dec 6, 2024
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
6 changes: 3 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ or from https://gitmoji.dev/

## What do these changes do?

<!-- Badge to openapi specs
[![ReDoc](https://img.shields.io/badge/OpenAPI-ReDoc-85ea2d?logo=openapiinitiative)](https://redocly.github.io/redoc/?url=HERE-URL-TO-RAW-FILE)
-->
pcrespov marked this conversation as resolved.
Show resolved Hide resolved


## Related issue/s
Expand All @@ -31,9 +34,6 @@ or from https://gitmoji.dev/
- resolves ITISFoundation/osparc-issues#428
- fixes #26
If openapi changes are provided, optionally point to the swagger editor with new changes
Example [openapi.json specs](https://editor.swagger.io/?url=https://raw.githubusercontent.com/<github-username>/osparc-simcore/is1133/create-api-for-creation-of-pricing-plan/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml)
-->


Expand Down
15 changes: 12 additions & 3 deletions api/specs/web-server/_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Annotated

from fastapi import APIRouter, Depends, status
from models_library.api_schemas_webserver.users import ProfileGet, ProfileUpdate
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
from models_library.api_schemas_webserver.users_preferences import PatchRequestBody
from models_library.generics import Envelope
from models_library.user_preferences import PreferenceIdentifier
Expand All @@ -24,8 +25,6 @@
from simcore_service_webserver.users._tokens_handlers import _TokenPathParams
from simcore_service_webserver.users.schemas import (
PermissionGet,
ProfileGet,
ProfileUpdate,
ThirdPartyToken,
TokenCreate,
)
Expand All @@ -41,14 +40,24 @@ async def get_my_profile():
...


@router.put(
@router.patch(
"/me",
status_code=status.HTTP_204_NO_CONTENT,
)
async def update_my_profile(_profile: ProfileUpdate):
...


@router.put(
"/me",
status_code=status.HTTP_204_NO_CONTENT,
deprecated=True,
description="Use PATCH instead",
)
async def replace_my_profile(_profile: ProfileUpdate):
...


@router.patch(
"/me/preferences/{preference_id}",
status_code=status.HTTP_204_NO_CONTENT,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import re
from datetime import date
from enum import Enum
from typing import Annotated, Literal

from models_library.api_schemas_webserver.groups import MyGroupsGet
from models_library.api_schemas_webserver.users_preferences import AggregatedPreferences
from models_library.basic_types import IDStr
from models_library.emails import LowerCaseEmailStr
from models_library.users import FirstNameStr, LastNameStr, UserID
from pydantic import BaseModel, ConfigDict, Field, field_validator

from ._base import InputSchema, OutputSchema


class ProfilePrivacyGet(OutputSchema):
hide_fullname: bool
hide_email: bool


class ProfilePrivacyUpdate(InputSchema):
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
hide_fullname: bool | None = None
hide_email: bool | None = None


class ProfileGet(BaseModel):
# WARNING: do not use InputSchema until front-end is updated!
id: UserID
user_name: Annotated[
IDStr, Field(description="Unique username identifier", alias="userName")
]
first_name: FirstNameStr | None = None
last_name: LastNameStr | None = None
login: LowerCaseEmailStr

role: Literal["ANONYMOUS", "GUEST", "USER", "TESTER", "PRODUCT_OWNER", "ADMIN"]
groups: MyGroupsGet | None = None
gravatar_id: Annotated[str | None, Field(deprecated=True)] = None

expiration_date: Annotated[
date | None,
Field(
description="If user has a trial account, it sets the expiration date, otherwise None",
alias="expirationDate",
),
] = None

privacy: ProfilePrivacyGet
preferences: AggregatedPreferences

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},
},
]
},
)

@field_validator("role", mode="before")
@classmethod
def _to_upper_string(cls, v):
if isinstance(v, str):
return v.upper()
if isinstance(v, Enum):
return v.name.upper()
return v


class ProfileUpdate(BaseModel):
# WARNING: do not use InputSchema until front-end is updated!
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
first_name: FirstNameStr | None = None
last_name: LastNameStr | None = None
user_name: Annotated[IDStr | None, Field(alias="userName")] = None

privacy: ProfilePrivacyUpdate | None = None

model_config = ConfigDict(
json_schema_extra={
"example": {
"first_name": "Pedro",
"last_name": "Crespo",
}
}
)

@field_validator("user_name")
@classmethod
def _validate_user_name(cls, value: str):
# Ensure valid characters (alphanumeric + . _ -)
if not re.match(r"^[a-zA-Z][a-zA-Z0-9._-]*$", value):
msg = f"Username '{value}' must start with a letter and can only contain letters, numbers and '_', '.' or '-'."
raise ValueError(msg)

# Ensure no consecutive special characters
if re.search(r"[_.-]{2,}", value):
msg = f"Username '{value}' cannot contain consecutive special characters like '__'."
raise ValueError(msg)

# Ensure it doesn't end with a special character
if {value[0], value[-1]}.intersection({"_", "-", "."}):
msg = f"Username '{value}' cannot end or start with a special character."
raise ValueError(msg)

# Check reserved words (example list; extend as needed)
reserved_words = {
"admin",
"root",
"system",
"null",
"undefined",
"support",
"moderator",
# NOTE: add here extra via env vars
}
if any(w in value.lower() for w in reserved_words):
msg = f"Username '{value}' cannot be used."
raise ValueError(msg)

return value
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""new user privacy columns

Revision ID: 38c9ac332c58
Revises: e5555076ef50
Create Date: 2024-12-05 14:29:27.739650+00:00

"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "38c9ac332c58"
down_revision = "e5555076ef50"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"users",
sa.Column(
"privacy_hide_fullname",
sa.Boolean(),
server_default=sa.text("true"),
nullable=False,
),
)
op.add_column(
"users",
sa.Column(
"privacy_hide_email",
sa.Boolean(),
server_default=sa.text("true"),
nullable=False,
),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("users", "privacy_hide_email")
op.drop_column("users", "privacy_hide_fullname")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from functools import total_ordering

import sqlalchemy as sa
from sqlalchemy.sql import expression

from ._common import RefActions
from .base import metadata
Expand Down Expand Up @@ -67,6 +68,9 @@ class UserStatus(str, Enum):
users = sa.Table(
"users",
metadata,
#
# User Identifiers ------------------
#
sa.Column(
"id",
sa.BigInteger(),
Expand All @@ -77,8 +81,23 @@ class UserStatus(str, Enum):
"name",
sa.String(),
nullable=False,
doc="username is a unique short user friendly identifier e.g. pcrespov, sanderegg, GitHK, ...",
doc="username is a unique short user friendly identifier e.g. pcrespov, sanderegg, GitHK, ..."
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
"This identifier **is public**.",
),
sa.Column(
"primary_gid",
sa.BigInteger(),
sa.ForeignKey(
"groups.gid",
name="fk_users_gid_groups",
onupdate=RefActions.CASCADE,
ondelete=RefActions.RESTRICT,
),
doc="User's group ID",
),
#
# User Information ------------------
#
sa.Column(
"first_name",
sa.String(),
Expand All @@ -102,37 +121,52 @@ class UserStatus(str, Enum):
doc="Confirmed user phone used e.g. to send a code for a two-factor-authentication."
"NOTE: new policy (NK) is that the same phone can be reused therefore it does not has to be unique",
),
#
# User Secrets ------------------
#
sa.Column(
"password_hash",
sa.String(),
nullable=False,
doc="Hashed password",
),
sa.Column(
"primary_gid",
sa.BigInteger(),
sa.ForeignKey(
"groups.gid",
name="fk_users_gid_groups",
onupdate=RefActions.CASCADE,
ondelete=RefActions.RESTRICT,
),
doc="User's group ID",
),
#
# User Account ------------------
#
sa.Column(
"status",
sa.Enum(UserStatus),
nullable=False,
default=UserStatus.CONFIRMATION_PENDING,
doc="Status of the user account. SEE UserStatus",
doc="Current status of the user's account",
),
sa.Column(
"role",
sa.Enum(UserRole),
nullable=False,
default=UserRole.USER,
doc="Use for role-base authorization",
doc="Used for role-base authorization",
),
#
# User Privacy Rules ------------------
#
sa.Column(
"privacy_hide_fullname",
sa.Boolean,
nullable=False,
server_default=expression.true(),
doc="If true, it hides users.first_name, users.last_name to others",
),
sa.Column(
"privacy_hide_email",
sa.Boolean,
nullable=False,
server_default=expression.true(),
doc="If true, it hides users.email to others",
),
#
# Timestamps ---------------
#
sa.Column(
"created_at",
sa.DateTime(),
Expand Down
Loading
Loading