add forgot endpoint
This commit is contained in:
@@ -1,10 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
ISSUER: str = os.getenv('ISSUER') # type: ignore
|
ISSUER: str = os.getenv('ISSUER') # type: ignore
|
||||||
|
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
|
||||||
|
|
||||||
EMAIL_SENDER = ('EDUSEG®', 'noreply@eduseg.com.br')
|
EMAIL_SENDER = ('EDUSEG®', 'noreply@eduseg.com.br')
|
||||||
|
|
||||||
OAUTH2_TABLE: str = os.getenv('OAUTH2_TABLE') # type: ignore
|
|
||||||
OAUTH2_REFRESH_TOKEN_EXPIRES_IN = 86_400 * 7 # 7 days
|
OAUTH2_REFRESH_TOKEN_EXPIRES_IN = 86_400 * 7 # 7 days
|
||||||
OAUTH2_SCOPES_SUPPORTED: list[str] = [
|
OAUTH2_SCOPES_SUPPORTED: list[str] = [
|
||||||
'openid',
|
'openid',
|
||||||
|
|||||||
@@ -4,17 +4,68 @@ from aws_lambda_powertools.utilities.data_classes import (
|
|||||||
event_source,
|
event_source,
|
||||||
)
|
)
|
||||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
|
from layercake.email_ import Message
|
||||||
|
from layercake.strutils import first_word
|
||||||
|
|
||||||
from boto3clients import sesv2_client
|
from boto3clients import sesv2_client
|
||||||
from config import EMAIL_SENDER
|
from config import EMAIL_SENDER
|
||||||
|
|
||||||
|
SUBJECT = '{first_name}, redefina sua senha na EDUSEG®'
|
||||||
|
MESSAGE = """
|
||||||
|
Oi {first_name}, tudo bem?<br/><br/>
|
||||||
|
|
||||||
|
Recebemos sua solicitação para redefinir sua senha na EDUSEG®.<br/>
|
||||||
|
Para continuar, é só clicar no link abaixo:<br/><br/>
|
||||||
|
|
||||||
|
<a href="https://id.saladeaula.digital/reset/{code}">
|
||||||
|
👉 Clique aqui para redefinir sua senha
|
||||||
|
</a>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
Se você não usar este link em até 3 horas, ele expirará.
|
||||||
|
<a href="https://id.saladeaula.digital/forgot">
|
||||||
|
Obter um novo link de redefinição de senha.
|
||||||
|
</a>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
Se você não pediu essa redefinição, pode ignorar esta mensagem.
|
||||||
|
"""
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@event_source(data_class=EventBridgeEvent)
|
@event_source(data_class=EventBridgeEvent)
|
||||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||||
new_image = event.detail['new_image']
|
new_image = event.detail['new_image']
|
||||||
# Key pattern `PASSWORD_RESET#{code}`
|
first_name = first_word(new_image['name'])
|
||||||
|
# Key pattern `CODE#{code}`
|
||||||
*_, code = new_image['sk'].split('#')
|
*_, code = new_image['sk'].split('#')
|
||||||
|
|
||||||
return True
|
emailmsg = Message(
|
||||||
|
from_=EMAIL_SENDER,
|
||||||
|
to=(new_image['name'], new_image['email']),
|
||||||
|
subject=SUBJECT.format(
|
||||||
|
first_name=first_name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
emailmsg.add_alternative(
|
||||||
|
MESSAGE.format(
|
||||||
|
first_name=first_name,
|
||||||
|
code=code,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sesv2_client.send_email(
|
||||||
|
Content={
|
||||||
|
'Raw': {
|
||||||
|
'Data': emailmsg.as_bytes(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info('Email sent')
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception(exc)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from layercake.dynamodb import (
|
|||||||
from layercake.funcs import omit, pick
|
from layercake.funcs import omit, pick
|
||||||
|
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import ISSUER, OAUTH2_DEFAULT_SCOPES, OAUTH2_SCOPES_SUPPORTED, OAUTH2_TABLE
|
from config import ISSUER, OAUTH2_DEFAULT_SCOPES, OAUTH2_SCOPES_SUPPORTED, USER_TABLE
|
||||||
from integrations.apigateway_oauth2.authorization_server import (
|
from integrations.apigateway_oauth2.authorization_server import (
|
||||||
AuthorizationServer,
|
AuthorizationServer,
|
||||||
)
|
)
|
||||||
@@ -34,7 +34,7 @@ from integrations.apigateway_oauth2.tokens import (
|
|||||||
from util import read_file_path
|
from util import read_file_path
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||||
private_key = read_file_path('private.pem')
|
private_key = read_file_path('private.pem')
|
||||||
private_jwk = JsonWebKey.import_key(private_key)
|
private_jwk = JsonWebKey.import_key(private_key)
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,10 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
|
|||||||
from passlib.hash import pbkdf2_sha256
|
from passlib.hash import pbkdf2_sha256
|
||||||
|
|
||||||
from boto3clients import dynamodb_client, idp_client
|
from boto3clients import dynamodb_client, idp_client
|
||||||
from config import (
|
from config import SESSION_EXPIRES_IN, USER_TABLE
|
||||||
OAUTH2_TABLE,
|
|
||||||
SESSION_EXPIRES_IN,
|
|
||||||
)
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
class InvalidCredentialsError(UnauthorizedError): ...
|
class InvalidCredentialsError(UnauthorizedError): ...
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ from joserfc.errors import JoseError
|
|||||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
|
||||||
|
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import OAUTH2_DEFAULT_SCOPES, OAUTH2_TABLE
|
from config import OAUTH2_DEFAULT_SCOPES, USER_TABLE
|
||||||
from oauth2 import server
|
from oauth2 import server
|
||||||
from util import parse_cookies
|
from util import parse_cookies
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
class SessionNotFoundError(NotFoundError): ...
|
class SessionNotFoundError(NotFoundError): ...
|
||||||
|
|||||||
@@ -1,13 +1,116 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from http import HTTPStatus
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from aws_lambda_powertools.event_handler.api_gateway import Router
|
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 aws_lambda_powertools.event_handler.openapi.params import Body
|
||||||
|
from aws_lambda_powertools.utilities.data_masking import DataMasking
|
||||||
|
from layercake.dateutils import now, ttl
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
|
||||||
from layercake.extra_types import CpfStr
|
from layercake.extra_types import CpfStr
|
||||||
|
from layercake.funcs import pick
|
||||||
from pydantic import EmailStr
|
from pydantic import EmailStr
|
||||||
|
|
||||||
|
from boto3clients import dynamodb_client
|
||||||
|
from config import USER_TABLE
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||||
|
data_masker = DataMasking()
|
||||||
|
|
||||||
|
masking_rules = {
|
||||||
|
'email': {'regex_pattern': '(.)(.*)(..)(@.*)', 'mask_format': r'\1****\3\4'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotFoundError(NotFoundError): ...
|
||||||
|
|
||||||
|
|
||||||
@router.post('/forgot')
|
@router.post('/forgot')
|
||||||
def forgot(email: Annotated[EmailStr | CpfStr, Body(embed=True)]):
|
def forgot(username: Annotated[EmailStr | CpfStr, Body(embed=True)]):
|
||||||
return {}
|
now_ = now()
|
||||||
|
user = _get_user(username)
|
||||||
|
reset_ttl = ttl(start_dt=now_, hours=3)
|
||||||
|
code = 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,
|
||||||
|
'user_id': user.id,
|
||||||
|
'ttl': reset_ttl,
|
||||||
|
'created_at': now_,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status_code=HTTPStatus.CREATED,
|
||||||
|
body=data_masker.erase(
|
||||||
|
{
|
||||||
|
'email': user.email,
|
||||||
|
},
|
||||||
|
masking_rules=masking_rules,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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),
|
||||||
|
)
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ from layercake.funcs import pick
|
|||||||
from pydantic import EmailStr
|
from pydantic import EmailStr
|
||||||
|
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import OAUTH2_TABLE
|
from config import USER_TABLE
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
class UserAlreadyOnboardedError(ServiceError):
|
class UserAlreadyOnboardedError(ServiceError):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from aws_lambda_powertools.event_handler import content_types
|
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.api_gateway import Response, Router
|
||||||
from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError
|
from aws_lambda_powertools.event_handler.exceptions import ServiceError
|
||||||
from aws_lambda_powertools.event_handler.openapi.params import Body
|
from aws_lambda_powertools.event_handler.openapi.params import Body
|
||||||
from aws_lambda_powertools.shared.cookies import Cookie
|
from aws_lambda_powertools.shared.cookies import Cookie
|
||||||
from layercake.dateutils import now, ttl
|
from layercake.dateutils import now, ttl
|
||||||
@@ -16,12 +16,12 @@ from passlib.hash import pbkdf2_sha256
|
|||||||
from pydantic import UUID4, EmailStr
|
from pydantic import UUID4, EmailStr
|
||||||
|
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import OAUTH2_TABLE, SESSION_EXPIRES_IN
|
from config import SESSION_EXPIRES_IN, USER_TABLE
|
||||||
|
|
||||||
from .authentication import new_session
|
from .authentication import new_session
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
class ConflictError(ServiceError):
|
class ConflictError(ServiceError):
|
||||||
@@ -29,9 +29,6 @@ class ConflictError(ServiceError):
|
|||||||
super().__init__(HTTPStatus.CONFLICT, msg)
|
super().__init__(HTTPStatus.CONFLICT, msg)
|
||||||
|
|
||||||
|
|
||||||
class UserNotFound(NotFoundError): ...
|
|
||||||
|
|
||||||
|
|
||||||
class CPFConflictError(ConflictError): ...
|
class CPFConflictError(ConflictError): ...
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ AWSTemplateFormatVersion: 2010-09-09
|
|||||||
Transform: AWS::Serverless-2016-10-31
|
Transform: AWS::Serverless-2016-10-31
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
OAuth2Table:
|
UserTable:
|
||||||
Type: String
|
Type: String
|
||||||
Default: betaeducacao-prod-users_d2o3r5gmm4it7j
|
Default: betaeducacao-prod-users_d2o3r5gmm4it7j
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ Globals:
|
|||||||
POWERTOOLS_LOGGER_LOG_EVENT: true
|
POWERTOOLS_LOGGER_LOG_EVENT: true
|
||||||
DYNAMODB_PARTITION_KEY: id
|
DYNAMODB_PARTITION_KEY: id
|
||||||
DYNAMODB_SORT_KEY: sk
|
DYNAMODB_SORT_KEY: sk
|
||||||
OAUTH2_TABLE: !Ref OAuth2Table
|
USER_TABLE: !Ref UserTable
|
||||||
ISSUER: https://id.saladeaula.digital
|
ISSUER: https://id.saladeaula.digital
|
||||||
|
|
||||||
Resources:
|
Resources:
|
||||||
@@ -32,6 +32,11 @@ Resources:
|
|||||||
Properties:
|
Properties:
|
||||||
RetentionInDays: 90
|
RetentionInDays: 90
|
||||||
|
|
||||||
|
EventLog:
|
||||||
|
Type: AWS::Logs::LogGroup
|
||||||
|
Properties:
|
||||||
|
RetentionInDays: 90
|
||||||
|
|
||||||
HttpApi:
|
HttpApi:
|
||||||
Type: AWS::Serverless::HttpApi
|
Type: AWS::Serverless::HttpApi
|
||||||
Properties:
|
Properties:
|
||||||
@@ -129,6 +134,34 @@ Resources:
|
|||||||
Method: GET
|
Method: GET
|
||||||
ApiId: !Ref HttpApi
|
ApiId: !Ref HttpApi
|
||||||
|
|
||||||
|
EventSendForgotEmailFunction:
|
||||||
|
Type: AWS::Serverless::Function
|
||||||
|
Properties:
|
||||||
|
Handler: events.send_forgot_email.lambda_handler
|
||||||
|
LoggingConfig:
|
||||||
|
LogGroup: !Ref EventLog
|
||||||
|
Policies:
|
||||||
|
- Version: 2012-10-17
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Action:
|
||||||
|
- ses:SendRawEmail
|
||||||
|
Resource:
|
||||||
|
- !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:identity/eduseg.com.br
|
||||||
|
- !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:configuration-set/tracking
|
||||||
|
Events:
|
||||||
|
DynamoDBEvent:
|
||||||
|
Type: EventBridgeRule
|
||||||
|
Properties:
|
||||||
|
Pattern:
|
||||||
|
resources: [!Ref UserTable]
|
||||||
|
detail-type: [INSERT]
|
||||||
|
detail:
|
||||||
|
new_image:
|
||||||
|
id: [PASSWORD_RESET]
|
||||||
|
sk:
|
||||||
|
- prefix: CODE#
|
||||||
|
|
||||||
Outputs:
|
Outputs:
|
||||||
HttpApiUrl:
|
HttpApiUrl:
|
||||||
Description: URL of your API endpoint
|
Description: URL of your API endpoint
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ SK = 'sk'
|
|||||||
# https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure
|
# https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure
|
||||||
def pytest_configure():
|
def pytest_configure():
|
||||||
os.environ['TZ'] = 'America/Sao_Paulo'
|
os.environ['TZ'] = 'America/Sao_Paulo'
|
||||||
os.environ['OAUTH2_TABLE'] = PYTEST_TABLE_NAME
|
os.environ['USER_TABLE'] = PYTEST_TABLE_NAME
|
||||||
os.environ['SESSION_SECRET'] = 'secret'
|
os.environ['SESSION_SECRET'] = 'secret'
|
||||||
os.environ['DYNAMODB_PARTITION_KEY'] = PK
|
os.environ['DYNAMODB_PARTITION_KEY'] = PK
|
||||||
os.environ['DYNAMODB_SORT_KEY'] = SK
|
os.environ['DYNAMODB_SORT_KEY'] = SK
|
||||||
|
|||||||
0
id.saladeaula.digital/tests/events/__init__.py
Normal file
0
id.saladeaula.digital/tests/events/__init__.py
Normal file
19
id.saladeaula.digital/tests/events/test_send_forgot_email.py
Normal file
19
id.saladeaula.digital/tests/events/test_send_forgot_email.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from aws_lambda_powertools.utilities.typing.lambda_context import LambdaContext
|
||||||
|
|
||||||
|
import events.send_forgot_email as app
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_forgot_email(monkeypatch, lambda_context: LambdaContext):
|
||||||
|
monkeypatch.setattr(app.sesv2_client, 'send_email', lambda *args, **kwargs: ...)
|
||||||
|
|
||||||
|
event = {
|
||||||
|
'detail': {
|
||||||
|
'new_image': {
|
||||||
|
'id': 'PASSWORD_RESET',
|
||||||
|
'sk': 'CODE#123',
|
||||||
|
'name': 'Sérgio R Siqueira',
|
||||||
|
'email': 'sergio@somosbeta.com.br',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||||
37
id.saladeaula.digital/tests/routes/test_forgot.py
Normal file
37
id.saladeaula.digital/tests/routes/test_forgot.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from http import HTTPMethod
|
||||||
|
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
|
||||||
|
|
||||||
|
from ..conftest import HttpApiProxy, LambdaContext
|
||||||
|
|
||||||
|
|
||||||
|
def test_forgot(
|
||||||
|
app,
|
||||||
|
seeds,
|
||||||
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
|
http_api_proxy: HttpApiProxy,
|
||||||
|
lambda_context: LambdaContext,
|
||||||
|
):
|
||||||
|
r = app.lambda_handler(
|
||||||
|
http_api_proxy(
|
||||||
|
raw_path='/forgot',
|
||||||
|
method=HTTPMethod.POST,
|
||||||
|
body={'username': '07879819908'},
|
||||||
|
),
|
||||||
|
lambda_context,
|
||||||
|
)
|
||||||
|
assert 's****io@somosbeta.com.br' == r['body']['email']
|
||||||
|
|
||||||
|
app.lambda_handler(
|
||||||
|
http_api_proxy(
|
||||||
|
raw_path='/forgot',
|
||||||
|
method=HTTPMethod.POST,
|
||||||
|
body={'username': '07879819908'},
|
||||||
|
),
|
||||||
|
lambda_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
forgot = dynamodb_persistence_layer.collection.query(
|
||||||
|
PartitionKey('PASSWORD_RESET'),
|
||||||
|
)
|
||||||
|
assert len(forgot['items']) == 3
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
from aws_lambda_powertools import Logger
|
from aws_lambda_powertools import Logger
|
||||||
from aws_lambda_powertools.event_handler.exceptions import (
|
from aws_lambda_powertools.event_handler.exceptions import (
|
||||||
BadRequestError,
|
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
|
ServiceError,
|
||||||
)
|
)
|
||||||
from layercake.dateutils import now, ttl
|
from layercake.dateutils import now, ttl
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey
|
||||||
@@ -351,6 +352,6 @@ class EnrollmentNotFoundError(NotFoundError):
|
|||||||
super().__init__('Enrollment not found')
|
super().__init__('Enrollment not found')
|
||||||
|
|
||||||
|
|
||||||
class EnrollmentConflictError(BadRequestError):
|
class EnrollmentConflictError(ServiceError):
|
||||||
def __init__(self, *_):
|
def __init__(self, *_):
|
||||||
super().__init__('Enrollment status conflict')
|
super().__init__(HTTPStatus.CONFLICT, 'Enrollment status conflict')
|
||||||
|
|||||||
Reference in New Issue
Block a user