From ea6fb1cbb01515ed872ef038224762982214419a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Sat, 5 Jul 2025 21:13:06 -0300 Subject: [PATCH] update rules --- README.md | 6 +++--- http-api/app/models.py | 9 ++++++++- http-api/app/rules/user.py | 40 ++++++++++++++++++++++++-------------- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index c8461c9..a16ddd4 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,11 @@ Quando o responsável é uma pessoa física (CPF). # Matrículas ```json -{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "0", "course": {"id": "10", "name": "pytest", "access_period": 360}, "tenant": "100"} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "0", "course": {"id": "10", "name": "pytest"}, "tenant": "100"} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "metadata#course", "access_period": 360, "cert": {"exp_interval": 365}} {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "tenant", "org_id": "100", "name": "EDUSEG"} {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "author", "user_id": "202", "name": "Tiago Maciel"} {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "konviva", "user_id": 122, "class_id": 123, "enrollment_id": 1239} -{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "metadata#cert", "exp_interval": 365} {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "started", "started_at": "2025-04-06T11:07:32.762178-03:00"} {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "completed", "completed_at": "2025-04-06T11:07:32.762178-03:00"} {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "failed", "failed_at": "2025-04-06T11:07:32.762178-03:00"} @@ -94,7 +94,7 @@ Se houver `metadata#parent_slot`, deve ser devolvido. ```json {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "0"} {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "cancel_policy"} -{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "metadata#parent_slot", "slot": {"id": "slots#org#123", "sk": "1221#f7120daf-96d2-4639-b8f4-d736fd99e4ee"}} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "metadata#parent_slot", "slot": {"id": "slots#org#123", "sk": "order#1221#f7120daf-96d2-4639-b8f4-d736fd99e4ee"}} ``` # Cursos diff --git a/http-api/app/models.py b/http-api/app/models.py index d925b09..fcde6e9 100644 --- a/http-api/app/models.py +++ b/http-api/app/models.py @@ -52,4 +52,11 @@ class Enrollment(BaseModel): *args, **kwargs, ) -> dict[str, Any]: - return super().model_dump(exclude={'user': {'email_verified'}}, *args, **kwargs) + return super().model_dump( + exclude={ + 'user': {'email_verified'}, + 'course': {'cert', 'access_period'}, + }, + *args, + **kwargs, + ) diff --git a/http-api/app/rules/user.py b/http-api/app/rules/user.py index 56b0080..677d37f 100644 --- a/http-api/app/rules/user.py +++ b/http-api/app/rules/user.py @@ -46,20 +46,26 @@ def update_user( }, cond_expr='attribute_exists(sk)', ) + + class RateLimitError(BadRequestError): + def __init__(self, msg: str): + super().__init__('Update limit reached') + # Prevent the user from updating more than once every 24 hours transact.put( item={ 'id': user.id, - 'sk': 'last_profile_edit', + 'sk': 'rate_limit#user_update', 'create_date': now_, 'ttl': ttl(start_dt=now_ + timedelta(hours=24)), }, + exc_cls=RateLimitError, cond_expr='attribute_not_exists(sk)', ) class CPFConflictError(BadRequestError): def __init__(self, msg: str): - super().__init__('Cpf already exists') + super().__init__('CPF already exists') if user.cpf != old_cpf: transact.put( @@ -90,6 +96,7 @@ def add_email( now_ = now() with persistence_layer.transact_writer() as transact: + # Ensure email is searchable transact.update( key=KeyPair(id, '0'), update_expr='ADD emails :email', @@ -100,7 +107,7 @@ def add_email( transact.put( item={ 'id': id, - 'sk': f'emails#{email}', + 'sk': ComposeKey(email, prefix='emails'), 'email_primary': False, 'email_verified': False, 'create_date': now_, @@ -112,6 +119,7 @@ def add_email( def __init__(self, msg: str): super().__init__('Email already exists') + # Prevent duplicate emails transact.put( item={ 'id': 'email', @@ -138,8 +146,10 @@ def del_email( transact.delete(key=KeyPair('email', email)) transact.delete( key=KeyPair(id, ComposeKey(email, prefix='emails')), - cond_expr='email_primary <> :primary', - expr_attr_values={':primary': True}, + cond_expr='email_primary <> :email_primary', + expr_attr_values={ + ':email_primary': True, + }, exc_cls=BadRequestError, ) transact.update( @@ -162,16 +172,16 @@ def set_email_as_primary( persistence_layer: DynamoDBPersistenceLayer, ): now_ = now() - expr = 'SET email_primary = :email_primary, update_date = :update_date' + expr = 'SET email_primary = :email_primary, updated_at = :updated_at' with persistence_layer.transact_writer() as transact: # Set the old email as non-primary transact.update( - key=KeyPair(id, ComposeKey(old_email, 'emails')), + key=KeyPair(id, ComposeKey(old_email, prefix='emails')), update_expr=expr, expr_attr_values={ ':email_primary': False, - ':update_date': now_, + ':updated_at': now_, }, ) # Set the new email as primary @@ -180,17 +190,17 @@ def set_email_as_primary( update_expr=expr, expr_attr_values={ ':email_primary': True, - ':update_date': now_, + ':updated_at': now_, }, ) transact.update( key=KeyPair(id, '0'), update_expr='SET email = :email, email_verified = :email_verified, \ - update_date = :update_date', + updated_at = :updated_at', expr_attr_values={ ':email': new_email, ':email_verified': email_verified, - ':update_date': now_, + ':updated_at': now_, }, ) @@ -205,8 +215,8 @@ def del_org_member( ) -> bool: with persistence_layer.transact_writer() as transact: # Remove the user's relationship with the organization and their privileges - transact.delete(key=KeyPair(id, f'acls#{org_id}')) - transact.delete(key=KeyPair(id, f'orgs#{org_id}')) + transact.delete(key=KeyPair(id, ComposeKey(org_id, prefix='acls'))) + transact.delete(key=KeyPair(id, ComposeKey(org_id, prefix='orgs'))) transact.update( key=KeyPair(id, '0'), update_expr='DELETE #tenant :org_id', @@ -215,7 +225,7 @@ def del_org_member( ) # Remove the user from the organization's admins and members list - transact.delete(key=KeyPair(org_id, f'admins#{id}')) - transact.delete(key=KeyPair(f'orgmembers#{org_id}', id)) + transact.delete(key=KeyPair(org_id, ComposeKey(id, prefix='admins'))) + transact.delete(key=KeyPair(ComposeKey(org_id, prefix='orgmembers'), id)) return True