This commit is contained in:
2025-08-15 00:06:05 -03:00
parent 6e0e5f788d
commit a53f37393a
27 changed files with 617 additions and 338 deletions

View File

@@ -23,14 +23,14 @@ from api_gateway import JSONResponse
from boto3clients import dynamodb_client, idp_client
from config import MEILISEARCH_API_KEY, MEILISEARCH_HOST, USER_POOOL_ID, USER_TABLE
from middlewares import AuditLogMiddleware, Tenant, TenantMiddleware
from models import User
from rules.user import update_user
from .add import router as add
from .emails import router as emails
from .logs import router as logs
from .orgs import router as orgs
__all__ = ['logs', 'emails', 'orgs']
__all__ = ['add', 'logs', 'emails', 'orgs']
class BadRequestError(MissingError, PowertoolsBadRequestError):
@@ -82,17 +82,6 @@ def get_users():
)
@router.post(
'/',
compress=True,
tags=['User'],
summary='Create user',
middlewares=[AuditLogMiddleware('USER_ADD', user_collect)],
)
def post_user(payload: User):
return JSONResponse(status_code=HTTPStatus.CREATED)
class UserData(BaseModel):
name: NameStr
cpf: CpfStr
@@ -129,7 +118,7 @@ def put_user(id: str, payload: UserData):
@router.get('/<id>', compress=True, tags=['User'], summary='Get user')
def get_user(id: str):
return user_collect.get_items(
TransactKey(id) + SortKey('0') + SortKey('rate_limit#user_update')
TransactKey(id) + SortKey('0') + SortKey('RATE_LIMIT#USER_UPDATE')
)

View File

@@ -0,0 +1,163 @@
from http import HTTPStatus
from uuid import uuid4
from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import (
BadRequestError,
NotFoundError,
)
from layercake.dateutils import now
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
from layercake.extra_types import CnpjStr, CpfStr, NameStr
from pydantic import (
UUID4,
BaseModel,
ConfigDict,
EmailStr,
Field,
)
from api_gateway import JSONResponse
from boto3clients import dynamodb_client
from config import USER_TABLE
router = Router()
logger = Logger(__name__)
layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
class Org(BaseModel):
id: str
name: str
cnpj: CnpjStr
class User(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
id: UUID4 = Field(default_factory=uuid4)
name: NameStr
email: EmailStr
email_verified: bool = False
cpf: CpfStr
org: Org = Field(..., exclude=True)
class UserNotFoundError(NotFoundError):
def __init__(self, *_):
super().__init__('User not found')
class CPFConflictError(BadRequestError):
def __init__(self, *_):
super().__init__('CPF already exists')
class EmailConflictError(BadRequestError):
def __init__(self, *_):
super().__init__('Email already exists')
@router.post('/', compress=True)
def add(user: User):
now_ = now()
user_id = user.id
org = user.org
try:
with layer.transact_writer() as transact:
transact.put(
item={
'id': 'cpf',
'sk': user.cpf,
'user_id': user.id,
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=CPFConflictError,
)
transact.put(
item={
'id': 'email',
'sk': user.email,
'user_id': user.id,
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=EmailConflictError,
)
transact.put(
item={
'sk': '0',
'tenant_id': {org.id},
# Post-migration: uncomment the following line
# 'createDate': now_,
'createDate': now_,
}
| user.model_dump()
)
transact.put(
item={
'id': user.id,
'sk': f'orgs#{user.org.id}',
'name': org.name,
'cnpj': org.cnpj,
'created_at': now_,
}
)
transact.put(
item={
'id': f'orgmembers#{org.id}',
'sk': user.id,
'created_at': now_,
}
)
except (CPFConflictError, EmailConflictError):
user_id = layer.collection.get_items(
KeyPair(
pk='cpf',
sk=SortKey(user.cpf, path_spec='user_id'),
rename_key='id',
)
+ KeyPair(
pk='email',
sk=SortKey(user.email, path_spec='user_id'),
rename_key='id',
),
flatten_top=False,
).get('id')
if not user_id:
raise UserNotFoundError()
with layer.transact_writer() as transact:
transact.update(
key=KeyPair(user_id, '0'),
update_expr='ADD tenant_id :org_id',
expr_attr_values={
':org_id': {org.id},
},
)
transact.put(
item={
'id': user_id,
'sk': f'orgs#{user.org.id}',
'name': org.name,
'cnpj': org.cnpj,
'created_at': now_,
}
)
transact.put(
item={
'id': f'orgmembers#{org.id}',
'sk': user_id,
'created_at': now_,
}
)
return JSONResponse(
status_code=HTTPStatus.CREATED,
body={'id': user_id},
)

View File

@@ -1,13 +1,11 @@
from http import HTTPStatus
from typing import Annotated
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import (
BadRequestError as PowertoolsBadRequestError,
)
from aws_lambda_powertools.event_handler.openapi.params import Body
from layercake.dynamodb import (
DynamoDBPersistenceLayer,
KeyPair,
MissingError,
PrefixKey,
)
from pydantic import BaseModel, EmailStr
@@ -15,23 +13,13 @@ from pydantic import BaseModel, EmailStr
from api_gateway import JSONResponse
from boto3clients import dynamodb_client
from config import USER_TABLE
from middlewares import AuditLogMiddleware
from rules.user import add_email, del_email, set_email_as_primary
class BadRequestError(MissingError, PowertoolsBadRequestError): ...
from rules.user import add_email, remove_email, set_email_as_primary
router = Router()
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
@router.get(
'/<id>/emails',
compress=True,
tags=['User'],
summary='Get user emails',
)
@router.get('/<id>/emails', compress=True)
def get_emails(id: str):
start_key = router.current_event.get_query_string_value('start_key', None)
@@ -45,18 +33,12 @@ class Email(BaseModel):
email: EmailStr
@router.post(
'/<id>/emails',
compress=True,
tags=['User'],
summary='Add user email',
middlewares=[AuditLogMiddleware('EMAIL_ADD', user_layer.collection, ('email',))],
)
def post_email(id: str, payload: Email):
add_email(id, payload.email, persistence_layer=user_layer)
@router.post('/<id>/emails', compress=True)
def add_email_(id: str, email: Annotated[str, Body(embed=True)]):
add_email(id, email, persistence_layer=user_layer)
return JSONResponse(
body=payload,
body={'email': email},
status_code=HTTPStatus.CREATED,
)
@@ -67,22 +49,7 @@ class EmailAsPrimary(BaseModel):
email_verified: bool = False
@router.patch(
'/<id>/emails',
compress=True,
tags=['User'],
summary='Add user email as primary',
middlewares=[
AuditLogMiddleware(
'EMAIL_CHANGE',
user_layer.collection,
(
'new_email',
'old_email',
),
)
],
)
@router.patch('/<id>/emails', compress=True)
def patch_email(id: str, payload: EmailAsPrimary):
set_email_as_primary(
id,
@@ -98,15 +65,9 @@ def patch_email(id: str, payload: EmailAsPrimary):
)
@router.delete(
'/<id>/emails',
compress=True,
tags=['User'],
summary='Delete user email',
middlewares=[AuditLogMiddleware('EMAIL_DEL', user_layer.collection, ('email',))],
)
@router.delete('/<id>/emails', compress=True)
def delete_email(id: str, payload: Email):
del_email(
remove_email(
id,
payload.email,
persistence_layer=user_layer,

View File

@@ -4,7 +4,6 @@ from aws_lambda_powertools.event_handler.exceptions import (
)
from layercake.dynamodb import (
ComposeKey,
DynamoDBCollection,
DynamoDBPersistenceLayer,
MissingError,
PartitionKey,
@@ -13,29 +12,19 @@ from layercake.dynamodb import (
from boto3clients import dynamodb_client
from config import USER_TABLE
from .orgs import router as orgs
__all__ = ['orgs']
class BadRequestError(MissingError, PowertoolsBadRequestError): ...
router = Router()
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
user_collect = DynamoDBCollection(user_layer, exc_cls=BadRequestError)
@router.get(
'/<id>/logs',
compress=True,
tags=['User'],
summary='Get user logs',
)
@router.get('/<id>/logs', compress=True, tags=['User'])
def get_logs(id: str):
return user_collect.query(
# Post-migration: uncomment to enable PartitionKey with a composite key (id with `logs` prefix).
# PartitionKey(ComposeKey(id, 'logs')),
return user_layer.collection.query(
# Post-migration: uncomment the following line
# PartitionKey(ComposeKey(id, 'LOGS')),
PartitionKey(ComposeKey(id, 'log', delimiter=':')),
start_key=router.current_event.get_query_string_value('start_key', None),
)

View File

@@ -16,8 +16,7 @@ from pydantic import BaseModel
from api_gateway import JSONResponse
from boto3clients import dynamodb_client
from config import USER_TABLE
from middlewares.audit_log_middleware import AuditLogMiddleware
from rules.user import del_org_member
from rules.user import remove_org_member
class BadRequestError(MissingError, PowertoolsBadRequestError): ...
@@ -30,13 +29,13 @@ user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
@router.get(
'/<id>/orgs',
compress=True,
tags=['User'],
summary='Get user orgs',
)
def get_orgs(id: str):
start_key = router.current_event.get_query_string_value('start_key', None)
return user_layer.collection.query(
KeyPair(id, PrefixKey('orgs')),
start_key=router.current_event.get_query_string_value('start_key', None),
start_key=start_key,
)
@@ -46,17 +45,7 @@ class Unassign(BaseModel):
cnpj: CnpjStr
@router.delete(
'/<id>/orgs',
compress=True,
tags=['User'],
summary='Delete user org',
middlewares=[
AuditLogMiddleware(
'UNASSIGN_ORG', user_layer.collection, ('id', 'name', 'cnpj')
)
],
)
@router.delete('/<id>/orgs', compress=True)
def delete_org(id: str, payload: Unassign):
del_org_member(id, org_id=payload.id, persistence_layer=user_layer)
remove_org_member(id, org_id=payload.id, persistence_layer=user_layer)
return JSONResponse(status_code=HTTPStatus.OK, body=payload)

View File