from datetime import timedelta from types import SimpleNamespace from typing import TypedDict from aws_lambda_powertools.event_handler.exceptions import ( BadRequestError, ) from layercake.dateutils import now, ttl from layercake.dynamodb import ( DynamoDBPersistenceLayer, KeyPair, SortKey, ) User = TypedDict('User', {'id': str, 'name': str, 'cpf': str}) class CPFConflictError(BadRequestError): def __init__(self, *_): super().__init__('CPF already exists') class RateLimitError(BadRequestError): def __init__(self, *_): super().__init__('Update limit reached') class EmailConflictError(BadRequestError): def __init__(self, *_): super().__init__('Email already exists') def update_user( obj: User, /, *, persistence_layer: DynamoDBPersistenceLayer, ) -> bool: now_ = now() user = SimpleNamespace(**obj) # Get the user's CPF, if it exists. old_cpf = persistence_layer.collection.get_item( KeyPair( pk=user.id, sk=SortKey('0', path_spec='cpf'), ) ) with persistence_layer.transact_writer() as transact: transact.update( key=KeyPair(user.id, '0'), update_expr='SET #name = :name, cpf = :cpf, updated_at = :updated_at', expr_attr_names={ '#name': 'name', }, expr_attr_values={ ':name': user.name, ':cpf': user.cpf, ':updated_at': now_, }, cond_expr='attribute_exists(sk)', ) # Prevent the user from updating more than once every 24 hours transact.put( item={ 'id': user.id, 'sk': 'RATE_LIMIT#USER_UPDATE', 'created_at': now_, 'ttl': ttl(start_dt=now_ + timedelta(hours=24)), }, exc_cls=RateLimitError, cond_expr='attribute_not_exists(sk)', ) if user.cpf != old_cpf: transact.put( item={ 'id': 'cpf', 'sk': user.cpf, 'user_id': user.id, 'created_at': now_, }, cond_expr='attribute_not_exists(sk)', exc_cls=CPFConflictError, ) # Ensures that the old CPF is discarded if old_cpf: transact.delete(key=KeyPair('cpf', old_cpf)) return True def add_email( id: str, email: str, /, *, persistence_layer: DynamoDBPersistenceLayer, ): now_ = now() with persistence_layer.transact_writer() as transact: # Ensure email is searchable transact.update( key=KeyPair(id, '0'), update_expr='ADD emails :email', expr_attr_values={ ':email': {email}, }, ) transact.put( item={ 'id': id, 'sk': f'emails#{email}', 'email_primary': False, 'email_verified': False, 'create_date': now_, }, cond_expr='attribute_not_exists(sk)', ) # Prevent duplicate emails transact.put( item={ 'id': 'email', 'sk': email, 'user_id': id, 'create_date': now_, }, cond_expr='attribute_not_exists(sk)', exc_cls=EmailConflictError, ) return True def remove_email( id: str, email: str, /, *, persistence_layer: DynamoDBPersistenceLayer, ) -> bool: """Delete any email except the primary email.""" with persistence_layer.transact_writer() as transact: transact.delete(key=KeyPair('email', email)) transact.delete( key=KeyPair(id, f'emails#{email}'), cond_expr='email_primary <> :email_primary', expr_attr_values={ ':email_primary': True, }, exc_cls=BadRequestError, ) transact.update( key=KeyPair(id, '0'), update_expr='DELETE emails :email', expr_attr_values={ ':email': {email}, }, ) return True 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, updated_at = :updated_at' with persistence_layer.transact_writer() as transact: # Set the old email as non-primary transact.update( key=KeyPair(id, f'emails#{old_email}'), update_expr=expr, expr_attr_values={ ':email_primary': False, ':updated_at': now_, }, ) # Set the new email as primary transact.update( key=KeyPair(id, f'emails#{new_email}'), update_expr=expr, expr_attr_values={ ':email_primary': True, ':updated_at': now_, }, ) transact.update( key=KeyPair(id, '0'), update_expr='DELETE emails :email_set SET email = :email, \ email_verified = :email_verified, updated_at = :updated_at', expr_attr_values={ ':email': new_email, ':email_set': {new_email}, ':email_verified': email_verified, ':updated_at': now_, }, ) return True def remove_org_member( id: str, *, org_id: str, persistence_layer: DynamoDBPersistenceLayer, ) -> bool: now_ = now() 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.update( key=KeyPair(id, '0'), update_expr='DELETE tenant_id :tenant_id SET updated_at = :updated_at', expr_attr_values={ ':tenant_id': {org_id}, ':updated_at': now_, }, ) # 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)) return True