add cert pages

This commit is contained in:
2025-11-12 08:49:18 -03:00
parent fa87cf3e07
commit 850f620f78
16 changed files with 271 additions and 203 deletions

View File

@@ -40,12 +40,12 @@ 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.admins, prefix='/orgs') app.include_router(orgs.admins, prefix='/orgs')
app.include_router(orgs.custom_pricing, prefix='/orgs') app.include_router(orgs.custom_pricing, prefix='/orgs')
app.include_router(orgs.scheduled, prefix='/orgs') app.include_router(orgs.scheduled, prefix='/orgs')
app.include_router(orgs.users, prefix='/orgs')
@app.get('/health') @app.get('/health')

View File

@@ -10,7 +10,10 @@ from aws_lambda_powertools.utilities.typing import LambdaContext
logger = Logger(__name__) logger = Logger(__name__)
tracer = Tracer() tracer = Tracer()
urls = ['https://bcs7fgb9og.execute-api.sa-east-1.amazonaws.com/health'] urls = [
'https://bcs7fgb9og.execute-api.sa-east-1.amazonaws.com/health',
'https://id.saladeaula.digital/health',
]
@tracer.capture_lambda_handler @tracer.capture_lambda_handler

View File

@@ -1,5 +1,6 @@
from .admins import router as admins from .admins import router as admins
from .custom_pricing import router as custom_pricing from .custom_pricing import router as custom_pricing
from .enrollments.scheduled import router as scheduled from .enrollments.scheduled import router as scheduled
from .users import router as users
__all__ = ['admins', 'custom_pricing', 'scheduled'] __all__ = ['admins', 'custom_pricing', 'scheduled', 'users']

View File

@@ -1,6 +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.api_gateway import Router
from aws_lambda_powertools.event_handler.openapi.params import Body
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from api_gateway import JSONResponse
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import USER_TABLE from config import USER_TABLE
@@ -15,3 +20,14 @@ def get_admins(org_id: str):
KeyPair(org_id, 'admins#'), KeyPair(org_id, 'admins#'),
limit=100, limit=100,
) )
@router.delete('/<org_id>/admins')
def revoke(org_id: str, user_id: Annotated[str, Body(embed=True)]):
with dyn.transact_writer() as transact:
transact.delete(
# Post-migration: rename `admins` to `ADMIN`
key=KeyPair(org_id, f'admins#{user_id}'),
)
return JSONResponse(status_code=HTTPStatus.NO_CONTENT)

View File

@@ -11,7 +11,7 @@ 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 BaseModel, EmailStr from pydantic import BaseModel, EmailStr, Field
from api_gateway import JSONResponse from api_gateway import JSONResponse
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
@@ -22,7 +22,7 @@ dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
class Org(BaseModel): class Org(BaseModel):
id: str id: str | None = Field(default=None, exclude=True)
name: str name: str
cnpj: CnpjStr cnpj: CnpjStr
@@ -44,56 +44,25 @@ class UserConflictError(ServiceError):
super().__init__(HTTPStatus.CONFLICT, msg) super().__init__(HTTPStatus.CONFLICT, msg)
class UserNotFoundError(NotFoundError): ... class UserMissingError(NotFoundError): ...
class OrgMissingError(NotFoundError): ... class OrgMissingError(NotFoundError): ...
@router.post('/') @router.post('/<org_id>/users')
def add_user( def add_user(
org_id: str,
user: Annotated[User, Body(embed=True)], user: Annotated[User, Body(embed=True)],
org: Annotated[Org, Body(embed=True)], org: Annotated[Org, Body(embed=True)],
): ):
org.id = org_id
if _create_user(user, org): if _create_user(user, org):
return JSONResponse(HTTPStatus.CREATED) return JSONResponse(HTTPStatus.CREATED)
now_ = now()
user_id = _get_user_id(user) user_id = _get_user_id(user)
_add_member(user_id, org)
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.NO_CONTENT) return JSONResponse(HTTPStatus.NO_CONTENT)
@@ -163,7 +132,7 @@ def _create_user(user: User, org: Org) -> bool:
} }
) )
transact.condition( transact.condition(
key=KeyPair(org.id, '0'), key=KeyPair(org.id, '0'), # type: ignore
cond_expr='attribute_exists(sk)', cond_expr='attribute_exists(sk)',
exc_cls=OrgMissingError, exc_cls=OrgMissingError,
) )
@@ -173,6 +142,44 @@ def _create_user(user: User, org: Org) -> bool:
return True return True
def _add_member(user_id: str, org: Org) -> None:
now_ = now()
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'), # type: ignore
cond_expr='attribute_exists(sk)',
exc_cls=OrgMissingError,
)
def _get_user_id(user: User) -> str: def _get_user_id(user: User) -> str:
user_id = dyn.collection.get_items( user_id = dyn.collection.get_items(
KeyPair( KeyPair(
@@ -189,6 +196,6 @@ def _get_user_id(user: User) -> str:
).get('id') ).get('id')
if not user_id: if not user_id:
raise UserNotFoundError() raise UserMissingError()
return user_id return user_id

View File

@@ -7,11 +7,10 @@ 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__ = ['add', 'emails', 'orgs'] __all__ = ['emails', 'orgs']
router = Router() router = Router()
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)

View File

@@ -121,7 +121,7 @@ Resources:
ScheduleEvent: ScheduleEvent:
Type: ScheduleV2 Type: ScheduleV2
Properties: Properties:
ScheduleExpression: 'cron(*/3 5-23 * * ? *)' ScheduleExpression: 'cron(*/5 5-23 * * ? *)'
ScheduleExpressionTimezone: America/Sao_Paulo ScheduleExpressionTimezone: America/Sao_Paulo
Outputs: Outputs:

View File

@@ -0,0 +1,151 @@
import json
from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import (
DynamoDBPersistenceLayer,
PartitionKey,
SortKey,
TransactKey,
)
from ...conftest import HttpApiProxy, LambdaContext
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='/orgs/f6000f79-6e5c-49a0-952f-3bda330ef278/users',
method=HTTPMethod.POST,
body={
'user': {
'name': 'Scott Weiland',
'email': 'scott@stonetemplopilots.com',
'cpf': '40245650016',
},
'org': {
'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='/orgs/2a8963fc-4694-4fe2-953a-316d1b10f1f5/users',
method=HTTPMethod.POST,
body={
'user': {
'name': 'Sérgio R Siqueira',
'email': 'sergio@somosbeta.com.br',
'cpf': '07879819908',
},
'org': {
'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,
seeds,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/orgs/f6000f79-6e5c-49a0-952f-3bda330ef278/users',
method=HTTPMethod.POST,
body={
'user': {
'name': 'Sérgio R Siqueira',
'email': 'sergio@somosbeta.com.br',
'cpf': '07879819908',
},
'org': {
'name': 'Banco do Brasil',
'cnpj': '00000000000191',
},
},
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.CONFLICT
def test_org_not_found(
app,
seeds,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/orgs/123/users',
method=HTTPMethod.POST,
body={
'user': {
'name': 'Sérgio R Siqueira',
'email': 'sergio@somosbeta.com.br',
'cpf': '07879819908',
},
'org': {
'name': 'Banco do Brasil',
'cnpj': '00000000000191',
},
},
),
lambda_context,
)
body = json.loads(r['body'])
assert body['type'] == 'OrgMissingError'

View File

@@ -1,6 +1,8 @@
import json import json
from http import HTTPMethod, HTTPStatus from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import DynamoDBPersistenceLayer
from ..conftest import HttpApiProxy, LambdaContext from ..conftest import HttpApiProxy, LambdaContext
@@ -23,6 +25,24 @@ def test_get_admins(
assert len(r['items']) == 1 assert len(r['items']) == 1
def test_revoke(
app,
seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/orgs/f6000f79-6e5c-49a0-952f-3bda330ef278/admins',
method=HTTPMethod.DELETE,
body={'user_id': '15bacf02-1535-4bee-9022-19d106fd7518'},
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.NO_CONTENT
def test_get_scheduled( def test_get_scheduled(
app, app,
seeds, seeds,

View File

@@ -1,13 +1,6 @@
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
@@ -44,147 +37,3 @@ 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,
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,
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': 'Banco do Brasil',
'cnpj': '00000000000191',
},
},
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.CONFLICT
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

@@ -0,0 +1,7 @@
export function meta({}) {
return [{ title: 'Certificações' }]
}
export default function Route() {
return <>index org</>
}

View File

@@ -61,10 +61,11 @@ export function meta({}: Route.MetaArgs) {
return [{ title: 'Adicionar colaborador' }] return [{ title: 'Adicionar colaborador' }]
} }
export async function action({ request, context }: Route.ActionArgs) { export async function action({ params, request, context }: Route.ActionArgs) {
const { orgid } = params
const body = await request.json() const body = await request.json()
const r = await req({ const r = await req({
url: `users`, url: `orgs/${orgid}/users`,
headers: new Headers({ 'Content-Type': 'application/json' }), headers: new Headers({ 'Content-Type': 'application/json' }),
method: HttpMethod.POST, method: HttpMethod.POST,
body: JSON.stringify(body), body: JSON.stringify(body),

View File

@@ -27,6 +27,11 @@ app.include_router(revoke)
app.include_router(openid_configuration) app.include_router(openid_configuration)
@app.get('/health')
def health():
return {'status': 'available'}
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)
@tracer.capture_lambda_handler @tracer.capture_lambda_handler
def lambda_handler(event: dict[str, Any], context: LambdaContext) -> dict[str, Any]: def lambda_handler(event: dict[str, Any], context: LambdaContext) -> dict[str, Any]:

View File

@@ -98,6 +98,12 @@ Resources:
Path: /userinfo Path: /userinfo
Method: GET Method: GET
ApiId: !Ref HttpApi ApiId: !Ref HttpApi
Health:
Type: HttpApi
Properties:
Path: /health
Method: GET
ApiId: !Ref HttpApi
Outputs: Outputs:
HttpApiUrl: HttpApiUrl:

View File

@@ -17,11 +17,11 @@ export const authMiddleware = async (
const requestId = context.get(requestIdContext) const requestId = context.get(requestIdContext)
let user = session.get('user') as User | null let user = session.get('user') as User | null
session.set('returnTo', new URL(request.url).toString())
if (!user) { if (!user) {
console.log('There is no user logged in') console.log('There is no user logged in')
session.set('returnTo', new URL(request.url).toString())
return redirect('/login', { return redirect('/login', {
headers: new Headers({ headers: new Headers({
'Set-Cookie': await sessionStorage.commitSession(session) 'Set-Cookie': await sessionStorage.commitSession(session)
@@ -43,7 +43,10 @@ export const authMiddleware = async (
refreshToken: tokens.refreshToken() refreshToken: tokens.refreshToken()
} }
console.debug(`[${requestId}] Refresh token retrieved`, user) console.debug(
`[${new Date().toISOString()}] [${requestId}] Refresh token retrieved`,
user
)
// Should replace the user in the session // Should replace the user in the session
session.set('user', user) session.set('user', user)
} }