add register

This commit is contained in:
2025-12-02 22:54:27 -03:00
parent e5ac436c5a
commit d461a507f9
6 changed files with 279 additions and 44 deletions

View File

@@ -25,6 +25,12 @@ dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
idp = boto3.client('cognito-idp')
class InvalidCredentialsError(ForbiddenError): ...
class UserNotFoundError(NotFoundError): ...
@router.post('/authentication')
def authentication(
username: Annotated[str, Body()],
@@ -36,7 +42,7 @@ def authentication(
_get_idp_user(user_id, username, password)
else:
if not pbkdf2_sha256.verify(password, password_hash):
raise ForbiddenError('Invalid credentials')
raise InvalidCredentialsError('Invalid credentials')
return Response(
status_code=HTTPStatus.OK,
@@ -61,7 +67,7 @@ def _get_user(username: str) -> tuple[str, str | None]:
)
if not user:
raise UserNotFoundError()
raise UserNotFoundError('User not found')
password = dyn.collection.get_item(
KeyPair(
@@ -121,13 +127,13 @@ def _get_idp_user(
}
)
except Exception:
raise ForbiddenError('Invalid credentials')
raise InvalidCredentialsError('Invalid credentials')
return True
def new_session(sub: str) -> str:
sid = str(uuid4())
def new_session(user_id: str) -> str:
session_id = str(uuid4())
now_ = now()
exp = ttl(start_dt=now_, seconds=SESSION_EXPIRES_IN)
@@ -135,24 +141,19 @@ def new_session(sub: str) -> str:
transact.put(
item={
'id': 'SESSION',
'sk': sid,
'user_id': sub,
'sk': session_id,
'user_id': user_id,
'ttl': exp,
'created_at': now_,
}
)
transact.put(
item={
'id': sub,
'sk': f'SESSION#{sid}',
'id': user_id,
'sk': f'SESSION#{session_id}',
'ttl': exp,
'created_at': now_,
}
)
return f'{sid}:{sub}'
class UserNotFoundError(NotFoundError):
def __init__(self, *_):
super().__init__('User not found')
return f'{session_id}:{user_id}'

View File

@@ -1,12 +1,17 @@
from dataclasses import asdict, dataclass
from http import HTTPStatus
from typing import Annotated
from uuid import uuid4
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import ServiceError
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.openapi.params import Body
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
from layercake.extra_types import CnpjStr, CpfStr, NameStr
from pydantic import BaseModel, EmailStr
from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from layercake.extra_types import CpfStr, NameStr
from layercake.funcs import pick
from passlib.hash import pbkdf2_sha256
from pydantic import UUID4, EmailStr
from boto3clients import dynamodb_client
from config import OAUTH2_TABLE
@@ -15,24 +20,181 @@ router = Router()
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
class UserConflictError(ServiceError):
class ConflictError(ServiceError):
def __init__(self, msg: str | dict):
super().__init__(HTTPStatus.CONFLICT, msg)
class Org(BaseModel):
id: str | None
class UserNotFound(NotFoundError): ...
class CPFConflictError(ConflictError): ...
class EmailConflictError(ConflictError): ...
class NeverLoggedConflictError(ConflictError): ...
@dataclass(frozen=True)
class User:
id: str
name: str
cnpj: CnpjStr
email: str
cpf: str
@router.get('/register')
@router.post('/register')
def register(
id: Annotated[UUID4, Body(embed=True, alias='id', default_factory=uuid4)],
name: Annotated[NameStr, Body(embed=True)],
email: Annotated[EmailStr, Body(embed=True)],
password: Annotated[str, Body(min_length=6, embed=True)],
cpf: Annotated[CpfStr, Body(embed=True)],
id: Annotated[str | None, Body(embed=True)] = None,
org: Annotated[Org | None, Body(embed=True)] = None,
):
return {}
new_user = User(id=str(id), name=name, email=email, cpf=cpf)
existing = dyn.collection.get_item(
KeyPair(str(id), '0'),
default=False,
raise_on_error=False,
)
if existing:
_update_user(
old_user=User(**pick(('id', 'name', 'email', 'cpf'), existing)),
new_user=new_user,
password=password,
)
return Response(
status_code=HTTPStatus.OK,
body=asdict(new_user),
)
_create_user(user=new_user, password=password)
return Response(
status_code=HTTPStatus.CREATED,
body=asdict(new_user),
)
def _create_user(*, user: User, password: str):
now_ = now()
with dyn.transact_writer() as transact:
transact.put(
item={
'sk': '0',
'email_verified': False,
'created_at': now_,
}
| asdict(user),
)
transact.put(
item={
'id': user.id,
# Post-migration (users): rename `emails` to `EMAIL`
'sk': f'emails#{user.email}',
'email_verified': False,
'email_primary': True,
'mx_record_exists': False,
'created_at': now_,
}
)
transact.put(
item={
'id': user.id,
'sk': 'PASSWORD',
'hash': pbkdf2_sha256.hash(password),
'created_at': now_,
}
)
transact.put(
item={
# Post-migration (users): rename `cpf` to `CPF`
'id': 'cpf',
'sk': user.cpf,
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=CPFConflictError,
)
transact.put(
item={
# Post-migration (users): rename `email` to `EMAIL`
'id': 'email',
'sk': user.email,
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=EmailConflictError,
)
def _update_user(*, old_user: User, new_user: User, password: str):
now_ = now()
with dyn.transact_writer() as transact:
transact.update(
key=KeyPair(new_user.id, '0'),
update_expr='SET #name = :name, \
email = :email, \
updated_at = :now',
expr_attr_names={
'#name': 'name',
},
expr_attr_values={
':name': new_user.name,
':email': new_user.email,
':now': now_,
},
cond_expr='attribute_exists(sk)',
)
transact.put(
item={
'id': new_user.id,
'sk': 'PASSWORD',
'hash': pbkdf2_sha256.hash(password),
'created_at': now_,
}
)
transact.delete(
key=KeyPair(new_user.id, 'NEVER_LOGGED'),
cond_expr='attribute_exists(sk)',
exc_cls=NeverLoggedConflictError,
)
if new_user.email != old_user.email:
transact.put(
item={
'id': new_user.id,
# Post-migration (users): rename `emails` to `EMAIL`
'sk': f'emails#{new_user.email}',
'email_verified': False,
'email_primary': True,
'mx_record_exists': False,
'created_at': now_,
}
)
transact.put(
item={
'id': new_user.id,
'sk': f'EMAIL_VERIFICATION#{uuid4()}',
'name': new_user.name,
'email': new_user.email,
'ttl': ttl(start_dt=now_, days=30),
'created_at': now_,
}
)
transact.put(
item={
# Post-migration (users): rename `email` to `EMAIL`
'id': 'email',
'sk': new_user.email,
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=EmailConflictError,
)