add user to org

This commit is contained in:
2025-11-10 22:08:26 -03:00
parent 648da66c90
commit c00a42ea39
6 changed files with 139 additions and 19 deletions

View File

@@ -4,14 +4,14 @@ from uuid import uuid4
from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import ( from aws_lambda_powertools.event_handler.exceptions import (
BadRequestError,
NotFoundError, NotFoundError,
ServiceError,
) )
from aws_lambda_powertools.event_handler.openapi.params import Body from aws_lambda_powertools.event_handler.openapi.params import Body
from layercake.dateutils import now from layercake.dateutils import now
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
from layercake.extra_types import CnpjStr, CpfStr, NameStr from layercake.extra_types import CnpjStr, CpfStr, NameStr
from pydantic import UUID4, BaseModel, EmailStr, Field from pydantic import BaseModel, EmailStr
from api_gateway import JSONResponse from api_gateway import JSONResponse
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
@@ -28,7 +28,6 @@ class Org(BaseModel):
class User(BaseModel): class User(BaseModel):
id: UUID4 = Field(default_factory=uuid4)
name: NameStr name: NameStr
cpf: CpfStr cpf: CpfStr
email: EmailStr email: EmailStr
@@ -40,7 +39,9 @@ class CPFConflictError(Exception): ...
class EmailConflictError(Exception): ... class EmailConflictError(Exception): ...
class UserConflictError(BadRequestError): ... class UserConflictError(ServiceError):
def __init__(self, msg: str | dict):
super().__init__(HTTPStatus.CONFLICT, msg)
class UserNotFoundError(NotFoundError): ... class UserNotFoundError(NotFoundError): ...
@@ -94,27 +95,41 @@ def add_user(
exc_cls=OrgMissingError, exc_cls=OrgMissingError,
) )
return JSONResponse(HTTPStatus.CREATED) return JSONResponse(HTTPStatus.NO_CONTENT)
def _create_user(user: User, org: Org) -> bool: def _create_user(user: User, org: Org) -> bool:
now_ = now() now_ = now()
user_id = uuid4()
try: try:
with dyn.transact_writer() as transact: with dyn.transact_writer() as transact:
transact.put( transact.put(
item={ item={
**user.model_dump(), **user.model_dump(),
'id': user_id,
'sk': '0', 'sk': '0',
'email_verified': False,
'org_id': {org.id}, 'org_id': {org.id},
'created_at': now_, 'created_at': now_,
}, },
) )
transact.put(
item={
'id': user_id,
# Post-migration: rename `emails` to `EMAIL`
'sk': f'emails#{user.email}',
'email_verified': False,
'email_primary': True,
'created_at': now_,
}
)
transact.put( transact.put(
item={ item={
# Post-migration: rename `cpf` to `CPF` # Post-migration: rename `cpf` to `CPF`
'id': 'cpf', 'id': 'cpf',
'sk': user.cpf, 'sk': user.cpf,
'created_at': now_,
}, },
cond_expr='attribute_not_exists(sk)', cond_expr='attribute_not_exists(sk)',
exc_cls=CPFConflictError, exc_cls=CPFConflictError,
@@ -124,13 +139,14 @@ def _create_user(user: User, org: Org) -> bool:
# Post-migration: rename `email` to `EMAIL` # Post-migration: rename `email` to `EMAIL`
'id': 'email', 'id': 'email',
'sk': user.email, 'sk': user.email,
'created_at': now_,
}, },
cond_expr='attribute_not_exists(sk)', cond_expr='attribute_not_exists(sk)',
exc_cls=EmailConflictError, exc_cls=EmailConflictError,
) )
transact.put( transact.put(
item={ item={
'id': user.id, 'id': user_id,
# Post-migration: rename `orgs` to `ORG` # Post-migration: rename `orgs` to `ORG`
'sk': f'orgs#{org.id}', 'sk': f'orgs#{org.id}',
'name': org.name, 'name': org.name,
@@ -142,7 +158,7 @@ def _create_user(user: User, org: Org) -> bool:
item={ item={
# Post-migration: rename `orgmembers` to `ORGMEMBER` # Post-migration: rename `orgmembers` to `ORGMEMBER`
'id': f'orgmembers#{org.id}', 'id': f'orgmembers#{org.id}',
'sk': user.id, 'sk': user_id,
'created_at': now_, 'created_at': now_,
} }
) )

View File

@@ -1,6 +1,13 @@
import json import json
from http import HTTPMethod, HTTPStatus from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import (
DynamoDBPersistenceLayer,
PartitionKey,
SortKey,
TransactKey,
)
from ..conftest import HttpApiProxy, LambdaContext from ..conftest import HttpApiProxy, LambdaContext
@@ -40,6 +47,93 @@ def test_get_orgs(
def test_add_user( def test_add_user(
app,
seeds,
http_api_proxy: HttpApiProxy,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/users',
method=HTTPMethod.POST,
body={
'user': {
'name': 'Scott Weiland',
'email': 'scott@stonetemplopilots.com',
'cpf': '40245650016',
},
'org': {
'id': 'f6000f79-6e5c-49a0-952f-3bda330ef278',
'name': 'Branco do Brasil',
'cnpj': '00000000000191',
},
},
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.CREATED
r = dynamodb_persistence_layer.collection.query(
PartitionKey('orgmembers#f6000f79-6e5c-49a0-952f-3bda330ef278')
)
user_id = r['items'][0]['sk']
user = dynamodb_persistence_layer.collection.get_items(
TransactKey(user_id)
+ SortKey('0')
+ SortKey('emails#scott@stonetemplopilots.com')
)
assert user['name'] == 'Scott Weiland'
assert 'email' in user
assert 'email_verified' in user
assert 'created_at' in user
assert 'org_id' in user
assert 'emails#scott@stonetemplopilots.com' in user
def test_user_exists(
app,
seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
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': '2a8963fc-4694-4fe2-953a-316d1b10f1f5',
'name': 'pytest',
'cnpj': '04978826000180',
},
},
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.NO_CONTENT
r = dynamodb_persistence_layer.collection.query(
PartitionKey('orgmembers#2a8963fc-4694-4fe2-953a-316d1b10f1f5')
)
user_id = r['items'][0]['sk']
user = dynamodb_persistence_layer.collection.get_items(
TransactKey(user_id)
+ SortKey('0')
+ SortKey('emails#sergio@somosbeta.com.br', rename_key='email')
+ SortKey('orgs#2a8963fc-4694-4fe2-953a-316d1b10f1f5', rename_key='org')
)
assert 'mx_record_exists' in user['email']
assert 'cnpj' in user['org']
def test_user_conflict(
app, app,
seeds, seeds,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
@@ -57,14 +151,14 @@ def test_add_user(
}, },
'org': { 'org': {
'id': 'f6000f79-6e5c-49a0-952f-3bda330ef278', 'id': 'f6000f79-6e5c-49a0-952f-3bda330ef278',
'name': 'Branco do Brasil', 'name': 'Banco do Brasil',
'cnpj': '00000000000191', 'cnpj': '00000000000191',
}, },
}, },
), ),
lambda_context, lambda_context,
) )
assert r['statusCode'] == HTTPStatus.CREATED assert r['statusCode'] == HTTPStatus.CONFLICT
def test_org_not_found( def test_org_not_found(

View File

@@ -1,11 +1,10 @@
// Users // Users
{"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "0", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br", "cpf": "07879819908"} {"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "0", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br", "cpf": "07879819908"}
{"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "emails#sergio@somosbeta.com.br", "email_primary": true, "mx_record_exists": true}
// User emails {"id": "213a6682-2c59-4404-9189-12eec0a846d4", "sk": "orgs#f6000f79-6e5c-49a0-952f-3bda330ef278", "name": "Banco do Brasil", "cnpj": "00000000000191"}
{"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": "Banco do Brasil", "cnpj": "00000000000191"} {"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "orgs#286f7729-7765-482a-880a-0b153ea799be", "name": "Banco do Brasil", "cnpj": "00000000000191"}
// Enrollments // 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"}}
@@ -14,9 +13,15 @@
{"id": "2a8963fc-4694-4fe2-953a-316d1b10f1f5", "sk": "0", "name": "pytest", "cnpj": "04978826000180"} {"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"} {"id": "f6000f79-6e5c-49a0-952f-3bda330ef278", "sk": "0", "name": "Banco do Brasil", "cnpj": "00000000000191"}
{"id": "orgmembers#f6000f79-6e5c-49a0-952f-3bda330ef278", "sk": "15bacf02-1535-4bee-9022-19d106fd7518"}
// Indicies
// CNPJs // CNPJs
{"id": "cnpj", "sk": "04978826000180", "org_id": "2a8963fc-4694-4fe2-953a-316d1b10f1f5"} {"id": "cnpj", "sk": "04978826000180", "org_id": "2a8963fc-4694-4fe2-953a-316d1b10f1f5"}
{"id": "cnpj", "sk": "00000000000191", "org_id": "6000f79-6e5c-49a0-952f-3bda330ef278"} {"id": "cnpj", "sk": "00000000000191", "org_id": "6000f79-6e5c-49a0-952f-3bda330ef278"}
// CPFs // CPFs
{"id": "cpf", "sk": "07879819908", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518""} {"id": "cpf", "sk": "07879819908", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518"}
// Emails
{"id": "email", "sk": "sergio@somosbeta.com.br", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518"}

View File

@@ -4,6 +4,7 @@ import {
BookCopyIcon, BookCopyIcon,
CalendarClockIcon, CalendarClockIcon,
DollarSign, DollarSign,
FileBadgeIcon,
GraduationCap, GraduationCap,
LayoutDashboard, LayoutDashboard,
ShieldUserIcon, ShieldUserIcon,
@@ -53,6 +54,11 @@ const data = {
url: '/enrollments', url: '/enrollments',
icon: GraduationCap icon: GraduationCap
}, },
{
title: 'Certificações',
url: '/certs',
icon: FileBadgeIcon
},
{ {
title: 'Agendamentos', title: 'Agendamentos',
url: '/scheduled', url: '/scheduled',

View File

@@ -59,8 +59,7 @@ export type User = {
const links = [ const links = [
{ to: '', title: 'Perfil', end: true }, { to: '', title: 'Perfil', end: true },
{ to: 'emails', title: 'Emails' }, { to: 'emails', title: 'Emails' }
{ to: 'orgs', title: 'Empresas' }
] ]
export default function Route({ export default function Route({

View File

@@ -79,16 +79,16 @@ export default function Route({ loaderData: { data } }) {
defaultValue={searchParams.get('q') || ''} defaultValue={searchParams.get('q') || ''}
onChange={(value) => onChange={(value) =>
setSearchParams((searchParams) => { setSearchParams((searchParams) => {
searchParams.set('q', value) searchParams.set('q', String(value))
searchParams.delete('p') searchParams.delete('p')
return searchParams return searchParams
}) })
} }
/> />
</div> </div>
<Button variant="outline" asChild> <Button asChild>
<Link to="add"> <Link to="add">
<PlusIcon /> Adicionar colaborador <PlusIcon /> Adicionar
</Link> </Link>
</Button> </Button>
</div> </div>