diff --git a/http-api/routes/users/__init__.py b/http-api/routes/users/__init__.py index ac8663d..f1cb749 100644 --- a/http-api/routes/users/__init__.py +++ b/http-api/routes/users/__init__.py @@ -2,11 +2,7 @@ import json from http import HTTPStatus from typing import Annotated -from aws_lambda_powertools.event_handler import content_types -from aws_lambda_powertools.event_handler.api_gateway import ( - Response, - Router, -) +from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.event_handler.exceptions import ( BadRequestError as PowertoolsBadRequestError, ) @@ -24,12 +20,12 @@ from pydantic import UUID4, BaseModel, EmailStr, StringConstraints import cognito import elastic -import middlewares +from api_gateway import JSONResponse from boto3clients import dynamodb_client, idp_client from middlewares import AuditLogMiddleware from models import User from settings import ELASTIC_CONN, USER_POOOL_ID, USER_TABLE -from user import add_email, del_email +from user import add_email, del_email, set_email_as_primary class BadRequestError(MissingError, PowertoolsBadRequestError): ... @@ -63,7 +59,7 @@ def get_users(): middlewares=[AuditLogMiddleware('USER_ADD', user_collect)], ) def post_user(payload: User): - return Response(status_code=HTTPStatus.CREATED) + return JSONResponse(status_code=HTTPStatus.CREATED) class Password(BaseModel): @@ -87,13 +83,11 @@ def password(id: str, payload: Password): user_pool_id=USER_POOOL_ID, idp_client=idp_client, ) - - return Response( + return JSONResponse( body={ 'id': id, 'cognito_sub': payload.cognito_sub, }, - content_type=content_types.APPLICATION_JSON, status_code=HTTPStatus.OK, ) @@ -137,14 +131,46 @@ class Email(BaseModel): middlewares=[AuditLogMiddleware('EMAIL_ADD', user_collect, ('email',))], ) def post_email(id: str, payload: Email): - assert add_email(id, payload.email, persistence_layer=user_layer) - return Response( + add_email(id, payload.email, persistence_layer=user_layer) + return JSONResponse( body=payload, - content_type=content_types.APPLICATION_JSON, status_code=HTTPStatus.CREATED, ) +class EmailAsPrimary(BaseModel): + new_email: EmailStr + old_email: EmailStr + email_verified: bool = False + + +@router.patch( + '//emails', + compress=True, + tags=['User'], + summary='Add user email as primary', + middlewares=[ + AuditLogMiddleware( + 'EMAIL_CHANGE', + user_collect, + ( + 'new_email', + 'old_email', + ), + ) + ], +) +def patch_email(id: str, payload: EmailAsPrimary): + set_email_as_primary( + id, + payload.new_email, + payload.old_email, + email_verified=payload.email_verified, + persistence_layer=user_layer, + ) + return JSONResponse(body=payload, status_code=HTTPStatus.OK) + + @router.delete( '//emails', compress=True, diff --git a/http-api/template.yaml b/http-api/template.yaml index 582eb84..b39d227 100644 --- a/http-api/template.yaml +++ b/http-api/template.yaml @@ -23,7 +23,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:46 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:47 Environment: Variables: TZ: America/Sao_Paulo diff --git a/http-api/tests/routes/test_users.py b/http-api/tests/routes/test_users.py index 9de6a26..c3e5caf 100644 --- a/http-api/tests/routes/test_users.py +++ b/http-api/tests/routes/test_users.py @@ -148,6 +148,36 @@ def test_post_email( } +def test_patch_email( + mock_app, + dynamodb_client, + dynamodb_seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + r = mock_app.lambda_handler( + http_api_proxy( + raw_path='/users/5OxmMjL-ujoR5IMGegQz/emails', + method=HTTPMethod.PATCH, + body={ + 'old_email': 'sergio@somosbeta.com.br', + 'new_email': 'osergiosiqueira@gmail.com', + }, + ), + lambda_context, + ) + + assert r['statusCode'] == HTTPStatus.OK + + collect = DynamoDBCollection(dynamodb_persistence_layer) + user = collect.get_item(KeyPair('5OxmMjL-ujoR5IMGegQz', '0')) + print(user) + # assert user['emails'] == { + # 'sergio@somosbeta.com.br', + # } + + def test_delete_email( mock_app, dynamodb_client, diff --git a/http-api/user.py b/http-api/user.py index d251983..ea1be21 100644 --- a/http-api/user.py +++ b/http-api/user.py @@ -83,3 +83,49 @@ def del_email( return persistence_layer.transact_write_items(transact) except ClientError: raise BadRequestError('Cannot remove the primary email.') + + +def set_email_as_primary( + id: str, + new_email: str, + old_email: str, + /, + *, + email_verified: bool = False, + persistence_layer: DynamoDBPersistenceLayer, +): + now_ = now() + expr = 'SET email_primary = :email_primary, update_date = :update_date' + transact = TransactItems(persistence_layer.table_name) + + transact.update( + key=KeyPair(id, ComposeKey(old_email, 'emails')), + update_expr=expr, + expr_attr_values={ + ':email_primary': False, + ':update_date': now_, + }, + ) + # Set the new email as primary + transact.update( + key=KeyPair(id, ComposeKey(new_email, 'emails')), + update_expr=expr, + expr_attr_values={ + ':email_primary': True, + ':update_date': now_, + }, + ) + transact.update( + key=KeyPair(id, '0'), + update_expr=( + 'SET email = :email, email_verified = :email_verified, ' + 'update_date = :update_date' + ), + expr_attr_values={ + ':email': new_email, + ':email_verified': email_verified, + ':update_date': now_, + }, + ) + + return persistence_layer.transact_write_items(transact) diff --git a/http-api/uv.lock b/http-api/uv.lock index 372bb44..673733c 100644 --- a/http-api/uv.lock +++ b/http-api/uv.lock @@ -521,7 +521,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.2.9" +version = "0.2.10" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, @@ -538,7 +538,6 @@ dependencies = [ { name = "pydantic-extra-types" }, { name = "pytz" }, { name = "requests" }, - { name = "uuid-utils" }, { name = "weasyprint" }, ] @@ -558,12 +557,12 @@ requires-dist = [ { name = "pydantic-extra-types", specifier = ">=2.10.3" }, { name = "pytz", specifier = ">=2025.1" }, { name = "requests", specifier = ">=2.32.3" }, - { name = "uuid-utils", specifier = ">=0.10.0" }, { name = "weasyprint", specifier = ">=65.0" }, ] [package.metadata.requires-dev] dev = [ + { name = "boto3-stubs", extras = ["essential"], specifier = ">=1.37.33" }, { name = "jsonlines", specifier = ">=4.0.0" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.29.0" }, { name = "pytest", specifier = ">=8.3.5" }, @@ -940,26 +939,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, ] -[[package]] -name = "uuid-utils" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/0a/cbdb2eb4845dafeb632d02a18f47b02f87f2ce4f25266f5e3c017976ce89/uuid_utils-0.10.0.tar.gz", hash = "sha256:5db0e1890e8f008657ffe6ded4d9459af724ab114cfe82af1557c87545301539", size = 18828 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/54/9d22fa16b19e5d1676eba510f08a9c458d96e2a62ff2c8ebad64251afb18/uuid_utils-0.10.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d5a4508feefec62456cd6a41bcdde458d56827d908f226803b886d22a3d5e63", size = 573006 }, - { url = "https://files.pythonhosted.org/packages/08/8e/f895c6e52aa603e521fbc13b8626ba5dd99b6e2f5a55aa96ba5b232f4c53/uuid_utils-0.10.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dbefc2b9113f9dfe56bdae58301a2b3c53792221410d422826f3d1e3e6555fe7", size = 292543 }, - { url = "https://files.pythonhosted.org/packages/b6/58/cc4834f377a5e97d6e184408ad96d13042308de56643b6e24afe1f6f34df/uuid_utils-0.10.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffc49c33edf87d1ec8112a9b43e4cf55326877716f929c165a2cc307d31c73d5", size = 323340 }, - { url = "https://files.pythonhosted.org/packages/37/e3/6aeddf148f6a7dd7759621b000e8c85382ec83f52ae79b60842d1dc3ab6b/uuid_utils-0.10.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0636b6208f69d5a4e629707ad2a89a04dfa8d1023e1999181f6830646ca048a1", size = 329653 }, - { url = "https://files.pythonhosted.org/packages/0c/00/dd6c2164ace70b7b1671d9129267df331481d7d1e5f9c5e6a564f07953f6/uuid_utils-0.10.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc06452856b724df9dedfc161c3582199547da54aeb81915ec2ed54f92d19b0", size = 365471 }, - { url = "https://files.pythonhosted.org/packages/b4/e7/0ab8080fcae5462a7b5e555c1cef3d63457baffb97a59b9bc7b005a3ecb1/uuid_utils-0.10.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:263b2589111c61decdd74a762e8f850c9e4386fb78d2cf7cb4dfc537054cda1b", size = 325844 }, - { url = "https://files.pythonhosted.org/packages/73/39/52d94e9ef75b03f44b39ffc6ac3167e93e74ef4d010a93d25589d9f48540/uuid_utils-0.10.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a558db48b7096de6b4d2d2210d82bba8586a6d55f99106b03bb7d01dc5c5bcd6", size = 344389 }, - { url = "https://files.pythonhosted.org/packages/7c/29/4824566f62666238290d99c62a58e4ab2a8b9cf2eccf94cebd9b3359131e/uuid_utils-0.10.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:807465067f3c892514230326ac71a79b28a8dfe2c88ecd2d5675fc844f3c76b5", size = 510078 }, - { url = "https://files.pythonhosted.org/packages/5e/8f/bbcc7130d652462c685f0d3bd26bb214b754215b476340885a4cb50fb89a/uuid_utils-0.10.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:57423d4a2b9d7b916de6dbd75ba85465a28f9578a89a97f7d3e098d9aa4e5d4a", size = 515937 }, - { url = "https://files.pythonhosted.org/packages/23/f8/34e0c00f5f188604d336713e6a020fcf53b10998e8ab24735a39ab076740/uuid_utils-0.10.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:76d8d660f18ff6b767e319b1b5f927350cd92eafa4831d7ef5b57fdd1d91f974", size = 494111 }, - { url = "https://files.pythonhosted.org/packages/1a/52/b7f0066cc90a7a9c28d54061ed195cd617fde822e5d6ac3ccc88509c3c44/uuid_utils-0.10.0-cp39-abi3-win32.whl", hash = "sha256:6c11a71489338837db0b902b75e1ba7618d5d29f05fde4f68b3f909177dbc226", size = 173520 }, - { url = "https://files.pythonhosted.org/packages/8b/15/f04f58094674d333974243fb45d2c740cf4b79186fb707168e57943c84a3/uuid_utils-0.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:11c55ae64f6c0a7a0c741deae8ca2a4eaa11e9c09dbb7bec2099635696034cf7", size = 182965 }, -] - [[package]] name = "wcwidth" version = "0.2.13" diff --git a/layercake/layercake/dynamodb.py b/layercake/layercake/dynamodb.py index 2bd467c..39d87b2 100644 --- a/layercake/layercake/dynamodb.py +++ b/layercake/layercake/dynamodb.py @@ -423,6 +423,8 @@ class TransactItems: if TYPE_CHECKING: from mypy_boto3_dynamodb.client import DynamoDBClient +else: + DynamoDBClient = object class DynamoDBPersistenceLayer: diff --git a/layercake/pyproject.toml b/layercake/pyproject.toml index 9d05ba3..1797fbc 100644 --- a/layercake/pyproject.toml +++ b/layercake/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "layercake" -version = "0.2.9" +version = "0.2.10" description = "Packages shared dependencies to optimize deployment and ensure consistency across functions." readme = "README.md" authors = [