from types import SimpleNamespace from typing import TypedDict from aws_lambda_powertools.event_handler.exceptions import ( BadRequestError, ) from botocore.exceptions import ClientError from botocore.tokens import timedelta from layercake.dateutils import now, ttl from layercake.dynamodb import ( ComposeKey, DynamoDBPersistenceLayer, KeyPair, TransactItems, ) class CPFConflictError(BadRequestError): pass User = TypedDict('User', {'id': str, 'name': str, 'cpf': str}) def update_user( userdata: User, /, *, persistence_layer: DynamoDBPersistenceLayer, ) -> bool: now_ = now() ttl_ = now_ + timedelta(hours=24) user = SimpleNamespace(**userdata) # Get the user's CPF, if it exists. old_cpf = persistence_layer.get_item(KeyPair(user.id, '0')).get('cpf', None) transact = TransactItems(persistence_layer.table_name) transact.update( key=KeyPair(user.id, '0'), update_expr='SET #name = :name, cpf = :cpf, update_date = :update_date', expr_attr_names={ '#name': 'name', }, expr_attr_values={ ':name': user.name, ':cpf': user.cpf, ':update_date': now_, }, cond_expr='attribute_exists(sk)', ) # Prevent the user from updating more than once every 24 hours transact.put( item={ 'id': user.id, 'sk': 'last_profile_edit', 'create_date': now_, 'ttl': ttl(start_dt=ttl_), 'ttl_date': ttl_, }, cond_expr='attribute_not_exists(sk)', ) if user.cpf != old_cpf: transact.put( item={ 'id': 'cpf', 'sk': user.cpf, 'user_id': user.id, 'create_date': now_, }, cond_expr='attribute_not_exists(sk)', ) # Ensures that the old CPF is discarded if old_cpf: transact.delete(key=KeyPair('cpf', old_cpf)) try: persistence_layer.transact_write_items(transact) except ClientError: raise CPFConflictError('CPF is already in use.') else: return True def add_email( id: str, email: str, /, *, persistence_layer: DynamoDBPersistenceLayer, ): now_ = now() transact = TransactItems(persistence_layer.table_name) 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)', ) transact.put( item={ 'id': 'email', 'sk': email, 'user_id': id, 'create_date': now_, }, cond_expr='attribute_not_exists(sk)', ) try: return persistence_layer.transact_write_items(transact) except ClientError: raise BadRequestError('Email already exists.') def del_email( id: str, email: str, /, *, persistence_layer: DynamoDBPersistenceLayer, ) -> bool: """Delete any email except the primary email.""" transact = TransactItems(persistence_layer.table_name) 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}, ) transact.update( key=KeyPair(id, '0'), update_expr='DELETE emails :email', expr_attr_values={ ':email': {email}, }, ) try: 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) # Set the old email as non-primary 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) def del_org_member( id: str, *, org_id: str, persistence_layer: DynamoDBPersistenceLayer, ) -> bool: transact = TransactItems(persistence_layer.table_name) # 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 :org_id', expr_attr_names={'#tenant': 'tenant__org_id'}, expr_attr_values={':org_id': {org_id}}, ) # 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 persistence_layer.transact_write_items(transact)