from dataclasses import dataclass from http import HTTPStatus from typing import Annotated from uuid import uuid4 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.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 from layercake.extra_types import CpfStr from layercake.funcs import pick from pydantic import EmailStr from boto3clients import dynamodb_client from config import USER_TABLE router = Router() dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) class UserNotFoundError(NotFoundError): ... @router.post('/forgot', compress=True) def forgot(username: Annotated[EmailStr | CpfStr, Body(embed=True)]): now_ = now() user = _get_user(username) reset_ttl = ttl(start_dt=now_, hours=3) code = str(uuid4()) with dyn.transact_writer() as transact: transact.update( key=KeyPair( pk='PASSWORD_RESET', sk=f'USER#{user.id}', ), update_expr='SET #name = :name, \ email = :email, \ #ttl = :ttl, \ updated_at = :now \ ADD code_attempts :code', expr_attr_names={ '#name': 'name', '#ttl': 'ttl', }, expr_attr_values={ ':name': user.name, ':email': user.email, ':ttl': reset_ttl, ':code': {code}, ':now': now_, }, ) dyn.put_item( item={ 'id': 'PASSWORD_RESET', 'sk': f'CODE#{code}', 'name': user.name, 'email': user.email, 'user_id': user.id, 'ttl': reset_ttl, 'created_at': now_, } ) return Response( content_type=content_types.APPLICATION_JSON, status_code=HTTPStatus.CREATED, body={ 'email': mask_email(user.email), }, ) @dataclass(frozen=True) class User: id: str name: str email: str def _get_user(username: str) -> User: user_id = dyn.collection.get_items( KeyPair( pk='email', sk=SortKey(username, path_spec='user_id'), # type: ignore rename_key='user_id', ) + KeyPair( pk='cpf', sk=SortKey(username, path_spec='user_id'), # type: ignore rename_key='user_id', ), flatten_top=False, ).get('user_id') if not user_id: raise UserNotFoundError('User not found') user = dyn.get_item( KeyPair( pk=user_id, sk='0', ) ) return User( **pick(('id', 'name', 'email'), user), ) def mask_email(email): username, domain = email.split('@') username = username[0] + '*' * (len(username) - 3) + username[-2:] return f'{username}@{domain}'