add user to org

This commit is contained in:
2025-11-10 21:37:12 -03:00
parent 9ff11c997c
commit 648da66c90
8 changed files with 286 additions and 7 deletions

View File

@@ -40,6 +40,7 @@ app.include_router(enrollments.download_cert, prefix='/enrollments')
app.include_router(enrollments.enroll, prefix='/enrollments') app.include_router(enrollments.enroll, prefix='/enrollments')
app.include_router(users.router, prefix='/users') app.include_router(users.router, prefix='/users')
app.include_router(users.emails, prefix='/users') app.include_router(users.emails, prefix='/users')
app.include_router(users.add, prefix='/users')
app.include_router(users.orgs, prefix='/users') app.include_router(users.orgs, prefix='/users')
app.include_router(orders.router, prefix='/orders') app.include_router(orders.router, prefix='/orders')
app.include_router(orgs.custom_pricing, prefix='/orgs') app.include_router(orgs.custom_pricing, prefix='/orgs')

View File

@@ -1,4 +1,4 @@
from .admins import router as admins
from .custom_pricing import router as custom_pricing from .custom_pricing import router as custom_pricing
__all__ = ['custom_pricing'] __all__ = ['admins', 'custom_pricing']

View File

@@ -0,0 +1,17 @@
from aws_lambda_powertools.event_handler.api_gateway import Router
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from boto3clients import dynamodb_client
from config import COURSE_TABLE, USER_TABLE
router = Router()
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
@router.get('/<org_id>/admins')
def get_admins(org_id: str):
return dyn.collection.query(
KeyPair(org_id, 'admins#'),
table_name=COURSE_TABLE,
limit=100,
)

View File

@@ -0,0 +1,13 @@
from aws_lambda_powertools.event_handler.api_gateway import Router
from layercake.dynamodb import DynamoDBPersistenceLayer
from boto3clients import dynamodb_client
from config import USER_TABLE
router = Router()
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
@router.post('/register')
def register():
return {}

View File

@@ -7,10 +7,11 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import USER_TABLE from config import USER_TABLE
from .add import router as add
from .emails import router as emails from .emails import router as emails
from .orgs import router as orgs from .orgs import router as orgs
__all__ = ['emails', 'orgs'] __all__ = ['add', 'emails', 'orgs']
router = Router() router = Router()
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)

View File

@@ -0,0 +1,178 @@
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 (
BadRequestError,
NotFoundError,
)
from aws_lambda_powertools.event_handler.openapi.params import Body
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, EmailStr, Field
from api_gateway import JSONResponse
from boto3clients import dynamodb_client
from config import USER_TABLE
router = Router()
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
class Org(BaseModel):
id: str
name: str
cnpj: CnpjStr
class User(BaseModel):
id: UUID4 = Field(default_factory=uuid4)
name: NameStr
cpf: CpfStr
email: EmailStr
class CPFConflictError(Exception): ...
class EmailConflictError(Exception): ...
class UserConflictError(BadRequestError): ...
class UserNotFoundError(NotFoundError): ...
class OrgMissingError(NotFoundError): ...
@router.post('/')
def add_user(
user: Annotated[User, Body(embed=True)],
org: Annotated[Org, Body(embed=True)],
):
if _create_user(user, org):
return JSONResponse(HTTPStatus.CREATED)
now_ = now()
user_id = _get_user_id(user)
with dyn.transact_writer() as transact:
transact.update(
key=KeyPair(user_id, '0'),
update_expr='ADD org_id :org_id',
expr_attr_values={
':org_id': {org.id},
},
)
transact.put(
item={
'id': user_id,
# Post-migration: rename `orgs` to `ORG`
'sk': f'orgs#{org.id}',
'name': org.name,
'cnpj': org.cnpj,
'created_at': now_,
}
)
transact.put(
item={
# Post-migration: rename `orgmembers` to `ORGMEMBER`
'id': f'orgmembers#{org.id}',
'sk': user_id,
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=UserConflictError,
)
transact.condition(
key=KeyPair(org.id, '0'),
cond_expr='attribute_exists(sk)',
exc_cls=OrgMissingError,
)
return JSONResponse(HTTPStatus.CREATED)
def _create_user(user: User, org: Org) -> bool:
now_ = now()
try:
with dyn.transact_writer() as transact:
transact.put(
item={
**user.model_dump(),
'sk': '0',
'org_id': {org.id},
'created_at': now_,
},
)
transact.put(
item={
# Post-migration: rename `cpf` to `CPF`
'id': 'cpf',
'sk': user.cpf,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=CPFConflictError,
)
transact.put(
item={
# Post-migration: rename `email` to `EMAIL`
'id': 'email',
'sk': user.email,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=EmailConflictError,
)
transact.put(
item={
'id': user.id,
# Post-migration: rename `orgs` to `ORG`
'sk': f'orgs#{org.id}',
'name': org.name,
'cnpj': org.cnpj,
'created_at': now_,
}
)
transact.put(
item={
# Post-migration: rename `orgmembers` to `ORGMEMBER`
'id': f'orgmembers#{org.id}',
'sk': user.id,
'created_at': now_,
}
)
transact.condition(
key=KeyPair(org.id, '0'),
cond_expr='attribute_exists(sk)',
exc_cls=OrgMissingError,
)
except (CPFConflictError, EmailConflictError):
return False
else:
return True
def _get_user_id(user: User) -> str:
user_id = dyn.collection.get_items(
KeyPair(
pk='email',
sk=SortKey(user.email, path_spec='user_id'),
rename_key='id',
)
+ KeyPair(
pk='cpf',
sk=SortKey(user.cpf, path_spec='user_id'),
rename_key='id',
),
flatten_top=False,
).get('id')
if not user_id:
raise UserNotFoundError()
return user_id

View File

@@ -37,3 +37,60 @@ def test_get_orgs(
lambda_context, lambda_context,
) )
assert r['statusCode'] == HTTPStatus.OK assert r['statusCode'] == HTTPStatus.OK
def test_add_user(
app,
seeds,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/users',
method=HTTPMethod.POST,
body={
'user': {
'name': 'Sérgio R Siqueira',
'email': 'sergio@somosbeta.com.br',
'cpf': '07879819908',
},
'org': {
'id': 'f6000f79-6e5c-49a0-952f-3bda330ef278',
'name': 'Branco do Brasil',
'cnpj': '00000000000191',
},
},
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.CREATED
def test_org_not_found(
app,
seeds,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/users',
method=HTTPMethod.POST,
body={
'user': {
'name': 'Sérgio R Siqueira',
'email': 'sergio@somosbeta.com.br',
'cpf': '07879819908',
},
'org': {
'id': '123',
'name': 'Banco do Brasil',
'cnpj': '00000000000191',
},
},
),
lambda_context,
)
body = json.loads(r['body'])
assert body['type'] == 'OrgMissingError'

View File

@@ -1,10 +1,22 @@
{"id": "2a8963fc-4694-4fe2-953a-316d1b10f1f5", "sk": "0", "name": "pytest" } // Users
{"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "0", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br", "cpf": "07879819908"}
// User emails // User emails
{"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "emails#sergio@somosbeta.com.br"} {"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "emails#sergio@somosbeta.com.br"}
// User orgs // User orgs
{"id": "213a6682-2c59-4404-9189-12eec0a846d4", "sk": "orgs#286f7729-7765-482a-880a-0b153ea799be", "name": "ACME", "cnpj": "00000000000191"} {"id": "213a6682-2c59-4404-9189-12eec0a846d4", "sk": "orgs#286f7729-7765-482a-880a-0b153ea799be", "name": "Banco do Brasil", "cnpj": "00000000000191"}
// Enrollment // Enrollments
{"id": "578ec87f-94c7-4840-8780-bb4839cc7e64", "sk": "0", "course": {"id": "af3258f0-bccf-4781-aec6-d4c618d234a7", "name": "pytest", "access_period": 180}, "user": {"id": "068b4600-cc36-4b55-b832-bb620021705a", "name": "Benjamin Burnley", "email": "burnley@breakingbenjamin.com"}} {"id": "578ec87f-94c7-4840-8780-bb4839cc7e64", "sk": "0", "course": {"id": "af3258f0-bccf-4781-aec6-d4c618d234a7", "name": "pytest", "access_period": 180}, "user": {"id": "068b4600-cc36-4b55-b832-bb620021705a", "name": "Benjamin Burnley", "email": "burnley@breakingbenjamin.com"}}
// Orgs
{"id": "2a8963fc-4694-4fe2-953a-316d1b10f1f5", "sk": "0", "name": "pytest", "cnpj": "04978826000180"}
{"id": "f6000f79-6e5c-49a0-952f-3bda330ef278", "sk": "0", "name": "Banco do Brasil", "cnpj": "00000000000191"}
// CNPJs
{"id": "cnpj", "sk": "04978826000180", "org_id": "2a8963fc-4694-4fe2-953a-316d1b10f1f5"}
{"id": "cnpj", "sk": "00000000000191", "org_id": "6000f79-6e5c-49a0-952f-3bda330ef278"}
// CPFs
{"id": "cpf", "sk": "07879819908", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518""}