Skip to content

Commit

Permalink
✨ web-api: request for an account deletion (🗃️) (ITISFoundation#4871)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcrespov authored Oct 23, 2023
1 parent c2067f2 commit 2e5d96c
Show file tree
Hide file tree
Showing 33 changed files with 665 additions and 184 deletions.
35 changes: 23 additions & 12 deletions api/specs/web-server/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from _common import Error, Log
from fastapi import APIRouter, status
from models_library.api_schemas_webserver.auth import UnregisterCheck
from models_library.generics import Envelope
from pydantic import BaseModel, Field, confloat
from simcore_service_webserver._meta import API_VTAG
Expand Down Expand Up @@ -53,16 +54,26 @@ async def check_registration_invitation(check: InvitationCheck):
response_model=Envelope[Log],
operation_id="auth_register",
)
async def register(registration: RegisterBody):
async def register(_body: RegisterBody):
"""User registration"""


@router.post(
"/auth/unregister",
response_model=Envelope[Log],
status_code=status.HTTP_200_OK,
responses={status.HTTP_409_CONFLICT: {"model": Envelope[Error]}},
)
async def unregister_account(_body: UnregisterCheck):
...


@router.post(
"/auth/verify-phone-number",
response_model=Envelope[RegisterPhoneNextPage],
operation_id="auth_register_phone",
)
async def register_phone(registration: RegisterPhoneBody):
async def register_phone(_body: RegisterPhoneBody):
"""user tries to verify phone number for 2 Factor Authentication when registering"""


Expand All @@ -71,7 +82,7 @@ async def register_phone(registration: RegisterPhoneBody):
response_model=Envelope[Log],
operation_id="auth_phone_confirmation",
)
async def phone_confirmation(confirmation: PhoneConfirmationBody):
async def phone_confirmation(_body: PhoneConfirmationBody):
"""user enters 2 Factor Authentication code when registering"""


Expand All @@ -88,7 +99,7 @@ async def phone_confirmation(confirmation: PhoneConfirmationBody):
}
},
)
async def login(authentication: LoginBody):
async def login(_body: LoginBody):
"""user logs in"""


Expand All @@ -103,7 +114,7 @@ async def login(authentication: LoginBody):
}
},
)
async def login_2fa(authentication: LoginTwoFactorAuthBody):
async def login_2fa(_body: LoginTwoFactorAuthBody):
"""user enters 2 Factor Authentication code when login in"""


Expand All @@ -127,7 +138,7 @@ async def resend_2fa_code(resend: Resend2faBody):
response_model=Envelope[Log],
operation_id="auth_logout",
)
async def logout(data: LogoutBody):
async def logout(_body: LogoutBody):
"""user logout"""


Expand All @@ -137,7 +148,7 @@ async def logout(data: LogoutBody):
operation_id="auth_reset_password",
responses={status.HTTP_503_SERVICE_UNAVAILABLE: {"model": Envelope[Error]}},
)
async def reset_password(data: ResetPasswordBody):
async def reset_password(_body: ResetPasswordBody):
"""a non logged-in user requests a password reset"""


Expand All @@ -152,7 +163,7 @@ async def reset_password(data: ResetPasswordBody):
}
},
)
async def reset_password_allowed(code: str, data: ResetPasswordConfirmation):
async def reset_password_allowed(code: str, _body: ResetPasswordConfirmation):
"""changes password using a token code without being logged in"""


Expand All @@ -171,7 +182,7 @@ async def reset_password_allowed(code: str, data: ResetPasswordConfirmation):
},
},
)
async def change_email(data: ChangeEmailBody):
async def change_email(_body: ChangeEmailBody):
"""logged in user changes email"""


Expand Down Expand Up @@ -205,7 +216,7 @@ class PasswordCheckSchema(BaseModel):
},
},
)
async def change_password(data: ChangePasswordBody):
async def change_password(_body: ChangePasswordBody):
"""logged in user changes password"""


Expand Down Expand Up @@ -265,7 +276,7 @@ async def list_api_keys(code: str):
},
},
)
async def create_api_key(data: ApiKeyCreate):
async def create_api_key(_body: ApiKeyCreate):
"""creates API keys to access public API"""


Expand All @@ -285,5 +296,5 @@ async def create_api_key(data: ApiKeyCreate):
},
},
)
async def delete_api_key(data: ApiKeyCreate):
async def delete_api_key(_body: ApiKeyCreate):
"""deletes API key by name"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from models_library.emails import LowerCaseEmailStr
from pydantic import SecretStr

from ._base import InputSchema


class UnregisterCheck(InputSchema):
email: LowerCaseEmailStr
password: SecretStr
2 changes: 1 addition & 1 deletion packages/postgres-database/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.8.0
0.10.0
2 changes: 1 addition & 1 deletion packages/postgres-database/setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.8.0
current_version = 0.10.0
commit = True
message = packages/postgres-database version: {current_version} → {new_version}
tag = False
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""rm created_ip and add DELETED user status
Revision ID: be0dece4e67c
Revises: 76d106b243c3
Create Date: 2023-10-23 17:36:46.349925+00:00
"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "be0dece4e67c"
down_revision = "76d106b243c3"
branch_labels = None
depends_on = None

column_name = "status"
enum_typename = "userstatus"
new_value = "DELETED"


def upgrade():
# SEE https://medium.com/makimo-tech-blog/upgrading-postgresqls-enum-type-with-sqlalchemy-using-alembic-migration-881af1e30abe

with op.get_context().autocommit_block():
op.execute(f"ALTER TYPE {enum_typename} ADD VALUE '{new_value}'")

# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("users", "created_ip")
# ### end Alembic commands ###


def downgrade():

# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"users",
sa.Column("created_ip", sa.VARCHAR(), autoincrement=False, nullable=True),
)
# ### end Alembic commands ###

# NOTE: Downgrade new updates requires re-building the entire enum!
op.execute(f"ALTER TYPE {enum_typename} RENAME TO {enum_typename}_old")
op.execute(
f"CREATE TYPE {enum_typename} AS ENUM('CONFIRMATION_PENDING', 'ACTIVE', 'BANNED')"
)
op.execute(
f"ALTER TABLE users ALTER COLUMN {column_name} TYPE {enum_typename} USING "
f"{column_name}::text::{enum_typename}"
)
op.execute(f"DROP TYPE {enum_typename}_old")
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,14 @@ class UserStatus(Enum):
active: user is confirmed and can use the platform
expired: user is not authorized because it expired after a trial period
banned: user is not authorized
deleted: this account is marked for deletion
"""

CONFIRMATION_PENDING = "PENDING"
ACTIVE = "ACTIVE"
EXPIRED = "EXPIRED"
BANNED = "BANNED"
DELETED = "DELETED"


users = sa.Table(
Expand Down Expand Up @@ -146,12 +148,6 @@ class UserStatus(Enum):
doc="Sets the expiration date for trial accounts."
"If set to NULL then the account does not expire.",
),
sa.Column(
"created_ip",
sa.String(),
nullable=True,
doc="User IP from which use was created",
),
# ---------------------------
sa.PrimaryKeyConstraint("id", name="user_pkey"),
sa.UniqueConstraint("email", name="user_login_key"),
Expand Down
25 changes: 12 additions & 13 deletions packages/postgres-database/tests/projects/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# pylint: disable=unused-variable


from typing import Optional
from typing import AsyncIterable

import pytest
from aiopg.sa.connection import SAConnection
Expand All @@ -14,43 +14,42 @@
from simcore_postgres_database.models.projects import projects
from simcore_postgres_database.models.users import users

USERNAME = f"{__name__}.me"
PARENT_PROJECT_NAME = f"{__name__}.parent"


@pytest.fixture
async def user(pg_engine: Engine) -> RowProxy:
_USERNAME = f"{__name__}.me"
# some user
async with pg_engine.acquire() as conn:
result: Optional[ResultProxy] = await conn.execute(
users.insert().values(**random_user(name=USERNAME)).returning(users)
result: ResultProxy | None = await conn.execute(
users.insert().values(**random_user(name=_USERNAME)).returning(users)
)
assert result.rowcount == 1

_user: Optional[RowProxy] = await result.first()
_user: RowProxy | None = await result.first()
assert _user
assert _user.name == USERNAME
assert _user.name == _USERNAME
return _user


@pytest.fixture
async def project(pg_engine: Engine, user: RowProxy) -> RowProxy:
_PARENT_PROJECT_NAME = f"{__name__}.parent"
# a user's project
async with pg_engine.acquire() as conn:
result: Optional[ResultProxy] = await conn.execute(
result: ResultProxy | None = await conn.execute(
projects.insert()
.values(**random_project(prj_owner=user.id, name=PARENT_PROJECT_NAME))
.values(**random_project(prj_owner=user.id, name=_PARENT_PROJECT_NAME))
.returning(projects)
)
assert result.rowcount == 1

_project: Optional[RowProxy] = await result.first()
_project: RowProxy | None = await result.first()
assert _project
assert _project.name == PARENT_PROJECT_NAME
assert _project.name == _PARENT_PROJECT_NAME
return _project


@pytest.fixture
async def conn(pg_engine: Engine) -> SAConnection:
async def conn(pg_engine: Engine) -> AsyncIterable[SAConnection]:
async with pg_engine.acquire() as conn:
yield conn
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ def random_user(**overrides) -> dict[str, Any]:
"email": FAKE.email().lower(),
"password_hash": _DEFAULT_HASH,
"status": UserStatus.ACTIVE,
"created_ip": FAKE.ipv4(),
}
assert set(data.keys()).issubset({c.name for c in users.columns}) # nosec

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class _UserInfoDictRequired(TypedDict, total=True):

class UserInfoDict(_UserInfoDictRequired, total=False):
created_at: datetime
created_ip: int
password_hash: str


Expand Down Expand Up @@ -107,7 +106,7 @@ async def __aexit__(self, *args):


class LoggedUser(NewUser):
def __init__(self, client, params=None, *, check_if_succeeds=True):
def __init__(self, client: TestClient, params=None, *, check_if_succeeds=True):
super().__init__(params, client.app)
self.client = client
self.enable_check = check_if_succeeds
Expand Down
1 change: 1 addition & 0 deletions packages/service-library/src/servicelib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def fire_and_forget_task(
task_suffix_name: str,
fire_and_forget_tasks_collection: set[asyncio.Task],
) -> asyncio.Task:
# NOTE: details on rationale in https://github.com/ITISFoundation/osparc-simcore/pull/3120
task = asyncio.create_task(obj, name=f"fire_and_forget_task_{task_suffix_name}")
fire_and_forget_tasks_collection.add(task)

Expand Down
4 changes: 2 additions & 2 deletions services/storage/tests/data/users.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
id,name,email,password_hash,status,role,created_at,created_ip
21,devops,[email protected],$5$rounds=1000$jjUWjHSG5F2dMKw.$9VRlE4YLl4bPfIrWkDz/8GtEx1XkzTpuZzyc/uiBFE4,ACTIVE,USER,2019-06-27 11:35:44.828696,172.16.8.64
id,name,email,password_hash,status,role,created_at
21,devops,[email protected],$5$rounds=1000$jjUWjHSG5F2dMKw.$9VRlE4YLl4bPfIrWkDz/8GtEx1XkzTpuZzyc/uiBFE4,ACTIVE,USER,2019-06-27 11:35:44.828696
2 changes: 1 addition & 1 deletion services/web/server/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.34.0
0.35.0
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.34.0
current_version = 0.35.0
commit = True
message = services/webserver api version: {current_version} → {new_version}
tag = False
Expand Down
Loading

0 comments on commit 2e5d96c

Please sign in to comment.