From b136d83f813f3f81f10ddd48afe715acfb0b6f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Fri, 5 Dec 2025 19:59:43 -0300 Subject: [PATCH] update admin name #32 --- .../app/routes/orgs/__init__.py | 60 +++++++++++++++++++ .../app/routes/orgs/users/__init__.py | 13 +--- .../app/routes/users/emails.py | 7 +-- .../tests/routes/orgs/test_users.py | 2 +- api.saladeaula.digital/tests/seeds.jsonl | 1 - users-events/app/events/update_admin.py | 54 +++++++++++++++++ users-events/template.yaml | 21 +++++++ users-events/tests/conftest.py | 15 +++-- .../tests/events/test_email_receiving.py | 2 +- .../tests/events/test_update_admin.py | 23 +++++++ users-events/tests/seeds.jsonl | 16 +++-- 11 files changed, 187 insertions(+), 27 deletions(-) create mode 100644 users-events/app/events/update_admin.py create mode 100644 users-events/tests/events/test_update_admin.py diff --git a/api.saladeaula.digital/app/routes/orgs/__init__.py b/api.saladeaula.digital/app/routes/orgs/__init__.py index 9b9aef2..b225d5f 100644 --- a/api.saladeaula.digital/app/routes/orgs/__init__.py +++ b/api.saladeaula.digital/app/routes/orgs/__init__.py @@ -1,6 +1,66 @@ +from typing import Annotated +from uuid import uuid4 + +from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.openapi.params import Body +from layercake.dateutils import now +from layercake.dynamodb import DynamoDBPersistenceLayer +from layercake.extra_types import CnpjStr, NameStr +from pydantic import UUID4, BaseModel, EmailStr + +from boto3clients import dynamodb_client +from config import INTERNAL_EMAIL_DOMAIN, USER_TABLE +from exceptions import ConflictError + from .admins import router as admins from .custom_pricing import router as custom_pricing from .enrollments.scheduled import router as scheduled from .users import router as users +router = Router() +dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) + + __all__ = ['admins', 'custom_pricing', 'scheduled', 'users'] + + +class OrgConflictError(ConflictError): ... + + +class User(BaseModel): + id: str | UUID4 + name: NameStr + email: EmailStr + + +@router.post('/s') +def add_org( + name: Annotated[str, Body(embed=True)], + cnpj: Annotated[CnpjStr, Body(embed=True)], + user: Annotated[User, Body(embed=True)], +): + now_ = now() + org_id = str(uuid4()) + + with dyn.transact_writer() as transact: + transact.put( + item={ + # Post-migration (users): rename `cnpj` to `CNPJ` + 'id': 'cnpj', + 'sk': cnpj, + 'org_id': org_id, + 'created_at': now_, + }, + cond_expr='attribute_not_exists(sk)', + exc_cls=OrgConflictError, + ) + transact.put( + item={ + 'id': org_id, + 'sk': '0', + 'name': name, + 'email': f'org+{cnpj}@{INTERNAL_EMAIL_DOMAIN}', + 'cnpj': cnpj, + 'created_at': now_, + } + ) diff --git a/api.saladeaula.digital/app/routes/orgs/users/__init__.py b/api.saladeaula.digital/app/routes/orgs/users/__init__.py index 9f8be88..19555c3 100644 --- a/api.saladeaula.digital/app/routes/orgs/users/__init__.py +++ b/api.saladeaula.digital/app/routes/orgs/users/__init__.py @@ -3,10 +3,7 @@ from typing import Annotated from uuid import uuid4 from aws_lambda_powertools.event_handler.api_gateway import Router -from aws_lambda_powertools.event_handler.exceptions import ( - NotFoundError, - ServiceError, -) +from aws_lambda_powertools.event_handler.exceptions import NotFoundError from aws_lambda_powertools.event_handler.openapi.params import Body from layercake.dateutils import now, ttl from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey @@ -16,6 +13,7 @@ from pydantic import BaseModel, EmailStr, Field from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import INTERNAL_EMAIL_DOMAIN, USER_TABLE +from exceptions import ConflictError router = Router() dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) @@ -33,11 +31,6 @@ class User(BaseModel): email: EmailStr -class ConflictError(ServiceError): - def __init__(self, msg: str | dict): - super().__init__(HTTPStatus.CONFLICT, msg) - - class CPFConflictError(ConflictError): ... @@ -218,7 +211,7 @@ def _add_member(user_id: str, org: Org) -> None: transact.update( key=KeyPair(user_id, '0'), # Post-migration (users): uncomment the following line - # update_expr='ADD tenant_id :org_id', + # update_expr='ADD org_id :org_id', update_expr='ADD tenant_id :org_id', expr_attr_values={ ':org_id': {org.id}, diff --git a/api.saladeaula.digital/app/routes/users/emails.py b/api.saladeaula.digital/app/routes/users/emails.py index 04da09e..57c0f71 100644 --- a/api.saladeaula.digital/app/routes/users/emails.py +++ b/api.saladeaula.digital/app/routes/users/emails.py @@ -4,7 +4,6 @@ from uuid import uuid4 from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.event_handler.exceptions import ( NotFoundError, - ServiceError, ) from aws_lambda_powertools.event_handler.openapi.params import Body, Path, Query from layercake.dateutils import now, ttl @@ -15,16 +14,12 @@ from typing_extensions import Annotated from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import USER_TABLE +from exceptions import ConflictError router = Router() dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) -class ConflictError(ServiceError): - def __init__(self, msg: str | dict): - super().__init__(HTTPStatus.CONFLICT, msg) - - class UserNotFoundError(NotFoundError): ... diff --git a/api.saladeaula.digital/tests/routes/orgs/test_users.py b/api.saladeaula.digital/tests/routes/orgs/test_users.py index 62a6f92..d37720b 100644 --- a/api.saladeaula.digital/tests/routes/orgs/test_users.py +++ b/api.saladeaula.digital/tests/routes/orgs/test_users.py @@ -50,7 +50,7 @@ def test_add_user( assert user['name'] == 'Scott Weiland' assert 'email' in user assert 'email_verified' in user - assert 'created_at' in user + # assert 'created_at' in user # assert 'org_id' in user assert 'tenant_id' in user assert 'emails#scott@stonetemplopilots.com' in user diff --git a/api.saladeaula.digital/tests/seeds.jsonl b/api.saladeaula.digital/tests/seeds.jsonl index 45194fa..af30188 100644 --- a/api.saladeaula.digital/tests/seeds.jsonl +++ b/api.saladeaula.digital/tests/seeds.jsonl @@ -8,7 +8,6 @@ // User orgs {"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "orgs#f6000f79-6e5c-49a0-952f-3bda330ef278", "name": "Banco do Brasil", "cnpj": "00000000000191"} - // Enrollments {"id": "578ec87f-94c7-4840-8780-bb4839cc7e64", "sk": "0", "course": {"id": "af3258f0-bccf-4781-aec6-d4c618d234a7", "name": "pytest", "access_period": 180}, "user": {"id": "068b4600-cc36-4b55-b832-bb620021705a", "name": "Benjamin Burnley", "email": "burnley@breakingbenjamin.com"}} {"id": "9c166c5e-890f-4e77-9855-769c29aaeb2e", "sk": "0", "course": {"id": "c27d1b4f-575c-4b6b-82a1-9b91ff369e0b", "name": "pytest", "access_period": 180, "scormset": "76c75561-d972-43ef-9818-497d8fc6edbe"}, "user": {"id": "068b4600-cc36-4b55-b832-bb620021705a", "name": "Layne Staley", "email": "layne@aliceinchains.com"}} diff --git a/users-events/app/events/update_admin.py b/users-events/app/events/update_admin.py new file mode 100644 index 0000000..0b35b3f --- /dev/null +++ b/users-events/app/events/update_admin.py @@ -0,0 +1,54 @@ +from aws_lambda_powertools import Logger +from aws_lambda_powertools.utilities.data_classes import ( + EventBridgeEvent, + event_source, +) +from aws_lambda_powertools.utilities.typing import LambdaContext +from layercake.dateutils import now +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair + +from boto3clients import dynamodb_client +from config import USER_TABLE + +logger = Logger(__name__) +dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) + + +@event_source(data_class=EventBridgeEvent) +@logger.inject_lambda_context +def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: + new_image = event.detail['new_image'] + user_id = new_image['id'] + now_ = now() + # Post-migration (users): uncommnent the following line + # r = dyn.collection.query(KeyPair(pk=user_id, sk='ORG#')) + r = dyn.collection.query(KeyPair(pk=user_id, sk='orgs#')) + + for org in r.get('items', []): + _, org_id = org['sk'].split('#') + + try: + dyn.update_item( + key=KeyPair( + pk=org_id, + # Post-migration (users): uncommnent the following line + # sk=f'ADMIN#{user_id}', + sk=f'admins#{user_id}', + ), + update_expr='SET #name = :name, \ + email = :email, \ + updated_at = :now', + expr_attr_names={ + '#name': 'name', + }, + expr_attr_values={ + ':name': new_image['name'], + ':email': new_image['email'], + ':now': now_, + }, + cond_expr='attribute_exists(sk)', + ) + except Exception: + logger.info(f'User "{user_id}" does not have admin privileges') + + return True diff --git a/users-events/template.yaml b/users-events/template.yaml index bf3137a..9581058 100644 --- a/users-events/template.yaml +++ b/users-events/template.yaml @@ -244,3 +244,24 @@ Resources: - prefix: emails# mx_record_exists: - exists: false + + EventUpdateAdminFunction: + Type: AWS::Serverless::Function + Properties: + Handler: events.update_admin.lambda_handler + LoggingConfig: + LogGroup: !Ref EventLog + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref UserTable + Events: + DynamoDBEvent: + Type: EventBridgeRule + Properties: + Pattern: + resources: [!Ref UserTable] + detail-type: [MODIFY] + detail: + new_image: + sk: ['0'] + changes: [name, email, cpf] diff --git a/users-events/tests/conftest.py b/users-events/tests/conftest.py index f02c5b5..3025bd4 100644 --- a/users-events/tests/conftest.py +++ b/users-events/tests/conftest.py @@ -63,7 +63,14 @@ def dynamodb_persistence_layer(dynamodb_client): @pytest.fixture() -def dynamodb_seeds(dynamodb_client): - with jsonlines.open('tests/seeds.jsonl') as lines: - for line in lines: - dynamodb_client.put_item(TableName=PYTEST_TABLE_NAME, Item=line) +def seeds(dynamodb_client): + from layercake.dynamodb import serialize + + with open('tests/seeds.jsonl', 'rb') as fp: + reader = jsonlines.Reader(fp) + + for line in reader.iter(type=dict, skip_invalid=True): + dynamodb_client.put_item( + TableName=PYTEST_TABLE_NAME, + Item=serialize(line), + ) diff --git a/users-events/tests/events/test_email_receiving.py b/users-events/tests/events/test_email_receiving.py index 2b45b0e..7a581f0 100644 --- a/users-events/tests/events/test_email_receiving.py +++ b/users-events/tests/events/test_email_receiving.py @@ -132,5 +132,5 @@ event = { } -def test_email_receiving(dynamodb_seeds, lambda_context: LambdaContext): +def test_email_receiving(seeds, lambda_context: LambdaContext): assert app.lambda_handler(event, lambda_context) == {'disposition': 'CONTINUE'} diff --git a/users-events/tests/events/test_update_admin.py b/users-events/tests/events/test_update_admin.py new file mode 100644 index 0000000..5d09704 --- /dev/null +++ b/users-events/tests/events/test_update_admin.py @@ -0,0 +1,23 @@ +from aws_lambda_powertools.utilities.typing.lambda_context import LambdaContext +from layercake.dynamodb import DynamoDBPersistenceLayer + +import events.update_admin as app + + +def test_updated_admin( + seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + lambda_context: LambdaContext, +): + event = { + 'detail': { + 'new_image': { + 'id': '5OxmMjL-ujoR5IMGegQz', + 'sk': '0', + 'name': 'Sérgio R Siqueira', + 'email': 'osergiosiqueira@gmail.com', + }, + }, + } + + assert app.lambda_handler(event, lambda_context) # type: ignore diff --git a/users-events/tests/seeds.jsonl b/users-events/tests/seeds.jsonl index c8fee8a..80c1a25 100644 --- a/users-events/tests/seeds.jsonl +++ b/users-events/tests/seeds.jsonl @@ -1,8 +1,16 @@ -{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}} -{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "admins#5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "Sérgio R Siqueira"}, "email": {"S": "sergio@somosbeta.com.br"}} -{"id": {"S": "cnpj"}, "sk": {"S": "15608435000190"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}} -{"id": {"S": "email"}, "sk": {"S": "org+15608435000190@users.noreply.saladeaula.digital"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}} +// Org data +{"id": "cJtK9SsnJhKPyxESe7g3DG", "sk": "0", "name": "EDUSEG"} +{"id": "cJtK9SsnJhKPyxESe7g3DG", "sk": "admins#5OxmMjL-ujoR5IMGegQz", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br"} +// User orgs +{"id": "5OxmMjL-ujoR5IMGegQz", "sk": "orgs#cJtK9SsnJhKPyxESe7g3DG"} +{"id": "5OxmMjL-ujoR5IMGegQz", "sk": "orgs#123"} + +// Indicies +// Emails +{"id": "email", "sk": "org+15608435000190@users.noreply.saladeaula.digital", "org_id": "cJtK9SsnJhKPyxESe7g3DG"} +// CNPJs +{"id": "cnpj", "sk": "15608435000190", "org_id": "cJtK9SsnJhKPyxESe7g3DG"} {"id": "BATCH_JOB#ORG#1411844c-10d6-456e-959d-e91775145461", "sk": "FILE#2025-11-13T16:04:53.024743", "progress": 0, "s3_uri": "s3://saladeaula.digital/samples/large_users.csv"} {"id": "BATCH_JOB#ORG#1411844c-10d6-456e-959d-e91775145461", "sk": "CHUNK#START#0#END#3847", "weight": 25, "file_sk": "FILE#2025-11-13T16:04:53.024743", "s3_uri": "s3://saladeaula.digital/samples/large_users.csv"}