add integration

This commit is contained in:
2025-08-06 18:46:21 -03:00
parent e08f16bbaa
commit ff25ade76e
16 changed files with 314 additions and 132 deletions

View File

@@ -34,5 +34,6 @@ def get_vacancies():
# Post-migration: uncomment the following line # Post-migration: uncomment the following line
# ComposeKey(str(tenant.id), prefix='slots#orgs'), # ComposeKey(str(tenant.id), prefix='slots#orgs'),
ComposeKey(str(tenant.id), prefix='vacancies'), ComposeKey(str(tenant.id), prefix='vacancies'),
) ),
limit=150,
) )

View File

@@ -39,7 +39,16 @@ def generate_refresh_token(user_id: str) -> str:
def verify_jwt(token: str) -> dict: def verify_jwt(token: str) -> dict:
try: try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) payload = jwt.decode(
token,
JWT_SECRET,
algorithms=[JWT_ALGORITHM],
issuer=ISSUER,
options={
'require': ['exp', 'sub', 'iss'],
'leeway': 60,
},
)
return payload return payload
except ExpiredSignatureError: except ExpiredSignatureError:
raise ForbiddenError('Token expired') raise ForbiddenError('Token expired')

View File

@@ -7,13 +7,13 @@ from aws_lambda_powertools.event_handler.exceptions import NotFoundError
from layercake.dateutils import now, ttl from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from apigateway_oauth2 import ( from boto3clients import dynamodb_client
from config import DYNAMODB_SORT_KEY, OAUTH2_TABLE
from integrations.apigateway_oauth2 import (
AuthorizationServer, AuthorizationServer,
OAuth2Client, OAuth2Client,
OAuth2Token, OAuth2Token,
) )
from boto3clients import dynamodb_client
from config import DYNAMODB_SORT_KEY, OAUTH2_TABLE
oauth2_layer = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client) oauth2_layer = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
@@ -33,12 +33,11 @@ def create_save_token_func(persistence_layer: DynamoDBPersistenceLayer):
return save_token return save_token
class ClientNotFoundError(NotFoundError): def create_query_client_func(persistence_layer: DynamoDBPersistenceLayer):
class ClientNotFoundError(NotFoundError):
def __init__(self, *_): def __init__(self, *_):
super().__init__('Client not found') super().__init__('Client not found')
def create_query_client_func(persistence_layer: DynamoDBPersistenceLayer):
def query_client(client_id) -> OAuth2Client: def query_client(client_id) -> OAuth2Client:
client = persistence_layer.collection.get_item( client = persistence_layer.collection.get_item(
KeyPair('OAUTH2_CLIENT', f'CLIENT_ID#{client_id}'), KeyPair('OAUTH2_CLIENT', f'CLIENT_ID#{client_id}'),
@@ -89,20 +88,17 @@ def save_authorization_code(code, request):
) )
def exists_nonce(nonce, request): class OpenIDCode(OpenIDCode_):
def exists_nonce(self, nonce, request):
nonce_ = oauth2_layer.get_item( nonce_ = oauth2_layer.get_item(
KeyPair( KeyPair(
f'OAUTH2_CODE#CLIENT_ID#{request.payload.client_id}', f'OAUTH2_CODE#CLIENT_ID#{request.payload.client_id}', # type:ignore
f'NONCE#{nonce}', f'NONCE#{nonce}',
) )
) )
return bool(nonce_) return bool(nonce_)
class OpenIDCode(OpenIDCode_):
def exists_nonce(self, nonce, request):
return exists_nonce(nonce, request)
def get_jwt_config(self, grant): def get_jwt_config(self, grant):
return DUMMY_JWT_CONFIG return DUMMY_JWT_CONFIG

View File

@@ -1,33 +1,84 @@
from uuid import uuid4 from http import HTTPStatus
from http.cookies import SimpleCookie
from urllib.parse import ParseResult, quote, urlencode, urlunparse
from authlib.oauth2 import OAuth2Error from authlib.oauth2 import OAuth2Error
from authlib.oauth2.rfc6749 import errors from authlib.oauth2.rfc6749 import errors
from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler import Response
from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.event_handler.api_gateway import Router
from jose_ import verify_jwt
from oauth2 import authorization from oauth2 import authorization
router = Router() router = Router()
logger = Logger(__name__)
@router.get('/authorize') @router.get('/authorize')
def authorize(): def authorize():
user = { current_event = router.current_event
'id': str(uuid4()), cookies = _parse_cookies(current_event.get('cookies', [])) # type: ignore
'sub': 'sergio@somosbeta.com.br', id_token = cookies.get('id_token')
} continue_url = quote(
urlunparse(
ParseResult(
scheme='',
netloc='',
path=current_event.path,
params='',
query=urlencode(current_event.query_string_parameters),
fragment='',
)
),
safe='',
)
login_url = f'/login?continue={continue_url}'
if not id_token:
return Response(
status_code=HTTPStatus.FOUND,
headers={'Location': login_url},
)
try:
user = verify_jwt(id_token)
except Exception as exc:
logger.exception(exc)
return Response(
status_code=HTTPStatus.FOUND,
headers={'Location': login_url},
)
try: try:
grant = authorization.get_consent_grant( grant = authorization.get_consent_grant(
request=router.current_event, request=router.current_event,
end_user=user, end_user={'id': user['sub']},
) )
except OAuth2Error as err: except OAuth2Error as err:
logger.exception(err)
return dict(err.get_body()) return dict(err.get_body())
try: try:
return authorization.create_authorization_response( return authorization.create_authorization_response(
request=router.current_event, request=router.current_event,
grant_user=user, grant_user={'id': user['sub']},
grant=grant, grant=grant,
) )
except errors.OAuth2Error: except errors.OAuth2Error as err:
logger.exception(err)
return {} return {}
def _parse_cookies(cookies: list[str] | None) -> dict[str, str]:
parsed_cookies = {}
if not cookies:
return parsed_cookies
for s in cookies:
c = SimpleCookie()
c.load(s)
parsed_cookies.update({k: morsel.value for k, morsel in c.items()})
return parsed_cookies

View File

@@ -1,62 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body
class="font-sans antialiased bg-black text-white flex items-center justify-center min-h-screen"
>
<div class="w-full max-w-sm relative">
<div
aria-hidden="true"
class="absolute inset-0 grid grid-cols-2 opacity-20"
>
<div
class="blur-[106px] h-56 bg-gradient-to-br to-lime-400 from-lime-700"
></div>
<div
class="blur-[106px] h-42 bg-gradient-to-r from-lime-400 to-lime-600"
></div>
</div>
<form method="POST" action="/login" class="space-y-6 relative z-1">
<div class="grid gap-2">
<label for="username" class="text-sm leading-none font-medium">
Email ou CPF
</label>
<input
type="text"
id="username"
name="username"
class="border border-white/15 bg-white/8 w-full rounded-lg px-3 py-1.5 shadow-sm outline-none focus-visible:border-white/30 focus-visible:ring-white/20 focus-visible:ring-3 transition"
required
/>
</div>
<div class="grid gap-2">
<div class="flex justify-between items-center">
<label for="password" class="text-sm leading-none font-medium">
Senha
</label>
<a href="#" class="text-sm" tabindex="-1">Esqueceu sua senha?</a>
</div>
<input
type="password"
id="password"
name="password"
class="border border-white/15 bg-white/8 w-full rounded-lg px-3 py-1.5 shadow-sm outline-none focus-visible:border-white/30 focus-visible:ring-white/20 focus-visible:ring-3 transition"
required
/>
</div>
<button
type="submit"
class="w-full text-sm font-medium text-black bg-lime-400 rounded-lg px-4 py-2 h-9"
>
Entrar
</button>
</form>
</div>
</body>
</html>

View File

@@ -1,5 +1,4 @@
from http import HTTPStatus from http import HTTPStatus
from pathlib import Path
from typing import Annotated from typing import Annotated
from aws_lambda_powertools.event_handler import ( from aws_lambda_powertools.event_handler import (
@@ -7,8 +6,9 @@ from aws_lambda_powertools.event_handler import (
) )
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 ForbiddenError, NotFoundError from aws_lambda_powertools.event_handler.exceptions import ForbiddenError, NotFoundError
from aws_lambda_powertools.event_handler.openapi.params import Form from aws_lambda_powertools.event_handler.openapi.params import Form, Param
from aws_lambda_powertools.shared.cookies import Cookie from aws_lambda_powertools.shared.cookies import Cookie
from jinja2 import Environment, PackageLoader, select_autoescape
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from passlib.hash import pbkdf2_sha256 from passlib.hash import pbkdf2_sha256
@@ -18,15 +18,20 @@ from jose_ import generate_jwt
router = Router() router = Router()
oauth2_layer = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client) oauth2_layer = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
templates = Environment(
loader=PackageLoader('app'),
autoescape=select_autoescape(['html']),
)
@router.get('/login') @router.get('/login', compress=True)
def login_form(): def login_form(continue_: Annotated[str, Param(alias='continue')]):
html = Path(__file__).with_name('login.html').read_text(encoding='utf-8') template = templates.get_template('login.html')
html = template.render(**{'continue': continue_})
return Response( return Response(
body=html, body=html,
status_code=HTTPStatus.OK.value, status_code=HTTPStatus.OK,
content_type='text/html', content_type='text/html',
) )
@@ -35,6 +40,7 @@ def login_form():
def login( def login(
username: Annotated[str, Form()], username: Annotated[str, Form()],
password: Annotated[str, Form()], password: Annotated[str, Form()],
continue_: Annotated[str, Form(alias='continue')],
): ):
user_id, password_hash = _get_user(username) user_id, password_hash = _get_user(username)
@@ -44,7 +50,10 @@ def login(
jwt_token = generate_jwt(user_id, username) jwt_token = generate_jwt(user_id, username)
return Response( return Response(
status_code=HTTPStatus.OK, status_code=HTTPStatus.FOUND,
headers={
'Location': continue_,
},
cookies=[ cookies=[
Cookie( Cookie(
name='id_token', name='id_token',

View File

@@ -0,0 +1,115 @@
<!doctype html>
<html>
<head>
<title>EDUSEG®</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body
class="font-sans antialiased bg-black text-white flex items-center justify-center min-h-screen px-3"
>
<div class="w-full max-w-sm relative">
<div
aria-hidden="true"
class="absolute inset-0 grid grid-cols-2 opacity-20"
>
<div
class="blur-[106px] h-56 bg-gradient-to-br to-lime-400 from-lime-700"
></div>
<div
class="blur-[106px] h-42 bg-gradient-to-r from-lime-400 to-lime-600"
></div>
</div>
<div class="grid gap-5 relative z-1">
<div class="text-center space-y-1">
<span
class="border border-white/15 bg-white/5 px-2.5 py-3 rounded-xl inline-block"
><svg
width="18"
height="24"
viewBox="0 0 18 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="size-12"
>
<path
d="M16.2756 23.4353L8.93847 20.1298C8.7383 20.0015 8.48167 20.0015 8.27893 20.1298L0.941837 23.4353C0.533793 23.6945 0 23.4019 0 22.9194V1.12629C0.00256631 0.787535 0.277162 0.512939 0.615915 0.512939H16.6066C16.9454 0.512939 17.22 0.787535 17.22 1.12629V22.9194C17.22 23.4019 16.6862 23.6945 16.2781 23.4353H16.2756Z"
fill="#8CD366"
></path>
<path
d="M10.7274 3.71313H3.34668V6.41803H10.7274V3.71313Z"
fill="#2E3524"
></path>
<path
d="M9.42115 8.4939H3.34668V10.6496H9.42115V8.4939Z"
fill="#2E3524"
></path>
<path
d="M10.7274 12.7263H3.34668V15.4312H10.7274V12.7263Z"
fill="#2E3524"
></path>
<path
d="M12.9984 13.6731H12.9958C12.5111 13.6731 12.1182 14.066 12.1182 14.5508V14.5533C12.1182 15.0381 12.5111 15.431 12.9958 15.431H12.9984C13.4831 15.431 13.8761 15.0381 13.8761 14.5533V14.5508C13.8761 14.066 13.4831 13.6731 12.9984 13.6731Z"
fill="#2E3524"
></path></svg
></span>
<h1 class="text-3xl mt-6 font-semibold font-display text-balance">
Faça login
</h1>
<p class="text-white/50 text-sm">
Não tem uma conta?
<a href="" class="font-medium text-white">Cadastre-se</a>.
</p>
</div>
<form method="POST" action="/login" class="space-y-6">
<input name="continue" type="hidden" value="{{ continue }}" />
<div class="grid gap-2">
<label for="username" class="text-sm leading-none font-medium">
Email ou CPF
</label>
<input
type="text"
id="username"
name="username"
class="border border-white/15 bg-white/8 w-full rounded-lg px-3 py-2.5 shadow-sm outline-none focus-visible:border-white/30 focus-visible:ring-white/20 focus-visible:ring-3 transition"
required
/>
</div>
<div class="grid gap-2">
<div class="flex justify-between items-center">
<label for="password" class="text-sm leading-none font-medium">
Senha
</label>
<a href="#" class="text-sm" tabindex="-1">Esqueceu sua senha?</a>
</div>
<input
type="password"
id="password"
name="password"
class="border border-white/15 bg-white/8 w-full rounded-lg px-3 py-2.5 shadow-sm outline-none focus-visible:border-white/30 focus-visible:ring-white/20 focus-visible:ring-3 transition"
required
/>
</div>
<button
type="submit"
class="w-full text-sm font-medium text-black bg-lime-400 rounded-lg px-4 py-2 h-9"
>
Entrar
</button>
</form>
<p class="text-xs text-white/50 text-center">
Ao fazer login, você concorda com nossa
<a href="#" class="underline hover:no-underline" target="_blank">
política de privacidade </a
>.
</p>
</div>
</div>
</body>
</html>

View File

@@ -14,7 +14,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:90 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:91
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo
@@ -51,24 +51,24 @@ Resources:
- DynamoDBCrudPolicy: - DynamoDBCrudPolicy:
TableName: !Ref OAuth2Table TableName: !Ref OAuth2Table
Events: Events:
Login:
Type: HttpApi
Properties:
Path: /login
Method: GET
ApiId: !Ref HttpApi
LoginPost:
Type: HttpApi
Properties:
Path: /login
Method: POST
ApiId: !Ref HttpApi
Authorize: Authorize:
Type: HttpApi Type: HttpApi
Properties: Properties:
Path: /authorize Path: /authorize
Method: GET Method: GET
ApiId: !Ref HttpApi ApiId: !Ref HttpApi
LoginForm:
Type: HttpApi
Properties:
Path: /login
Method: GET
ApiId: !Ref HttpApi
Login:
Type: HttpApi
Properties:
Path: /login
Method: POST
ApiId: !Ref HttpApi
OpenidConfiguration: OpenidConfiguration:
Type: HttpApi Type: HttpApi
Properties: Properties:

View File

@@ -3,6 +3,7 @@ import json
import os import os
from dataclasses import dataclass from dataclasses import dataclass
from http import HTTPMethod from http import HTTPMethod
from urllib.parse import urlencode
import jsonlines import jsonlines
import pytest import pytest
@@ -37,16 +38,22 @@ class HttpApiProxy:
body: dict | str | None = None, body: dict | str | None = None,
*, *,
headers: dict = {}, headers: dict = {},
cookies: dict = {}, cookies: list[str] = [],
query_string_parameters: dict = {}, query_string_parameters: dict = {},
is_base64_encoded: bool = True, is_base64_encoded: bool = True,
**kwargs, **kwargs,
) -> dict: ) -> dict:
if isinstance(body, dict):
body = json.dumps(body)
if is_base64_encoded and body:
body = _base64_encode(body)
return { return {
'version': '2.0', 'version': '2.0',
'routeKey': '$default', 'routeKey': '$default',
'rawPath': raw_path, 'rawPath': raw_path,
'rawQueryString': 'parameter1=value1&parameter1=value2&parameter2=value', 'rawQueryString': urlencode(query_string_parameters),
'cookies': cookies, 'cookies': cookies,
'headers': headers, 'headers': headers,
'queryStringParameters': query_string_parameters, 'queryStringParameters': query_string_parameters,
@@ -69,17 +76,17 @@ class HttpApiProxy:
'time': '12/Mar/2020:19:03:58 +0000', 'time': '12/Mar/2020:19:03:58 +0000',
'timeEpoch': 1583348638390, 'timeEpoch': 1583348638390,
}, },
'body': _base64_dict(body) if isinstance(body, dict) else body, 'body': body,
'pathParameters': {'parameter1': 'value1'}, 'pathParameters': {'parameter1': 'value1'},
'isBase64Encoded': is_base64_encoded, 'isBase64Encoded': is_base64_encoded,
'stageVariables': {'stageVariable1': 'value1', 'stageVariable2': 'value2'}, 'stageVariables': {'stageVariable1': 'value1', 'stageVariable2': 'value2'},
} }
def _base64_dict(obj: dict = {}) -> str | None: def _base64_encode(s: str) -> str | None:
if not obj: if not s:
return None return None
return base64.b64encode(json.dumps(obj).encode()).decode() return base64.b64encode(s.encode()).decode()
@pytest.fixture @pytest.fixture
@@ -128,7 +135,7 @@ def dynamodb_persistence_layer(dynamodb_client):
@pytest.fixture() @pytest.fixture()
def dynamodb_seeds(dynamodb_client): def seeds(dynamodb_client):
from layercake.dynamodb import serialize from layercake.dynamodb import serialize
with open('tests/seeds.jsonl', 'rb') as fp: with open('tests/seeds.jsonl', 'rb') as fp:
@@ -142,7 +149,7 @@ def dynamodb_seeds(dynamodb_client):
@pytest.fixture @pytest.fixture
def mock_app(): def app():
import app import app
return app return app

View File

@@ -2,19 +2,26 @@ from http import HTTPMethod
from layercake.dynamodb import DynamoDBPersistenceLayer from layercake.dynamodb import DynamoDBPersistenceLayer
from jose_ import generate_jwt
from ..conftest import HttpApiProxy, LambdaContext from ..conftest import HttpApiProxy, LambdaContext
def test_authorize( def test_authorize(
mock_app, app,
dynamodb_seeds, seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer, dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
client_id = 'd72d4005-1fa7-4430-9754-80d5e2487bb6' client_id = 'd72d4005-1fa7-4430-9754-80d5e2487bb6'
r = mock_app.lambda_handler( id_token = generate_jwt(
user_id='357db1c5-7442-4075-98a3-fbe5c938a419',
email='sergio@somosbeta.com.br',
)
r = app.lambda_handler(
http_api_proxy( http_api_proxy(
raw_path='/authorize', raw_path='/authorize',
method=HTTPMethod.GET, method=HTTPMethod.GET,
@@ -25,6 +32,9 @@ def test_authorize(
'scope': 'openid', 'scope': 'openid',
'nonce': '123', 'nonce': '123',
}, },
cookies=[
f'id_token={id_token}; HttpOnly; Secure',
],
), ),
lambda_context, lambda_context,
) )

View File

@@ -3,16 +3,17 @@ from http import HTTPMethod
from ..conftest import HttpApiProxy, LambdaContext from ..conftest import HttpApiProxy, LambdaContext
def test_html_page( def test_html(
mock_app, app,
dynamodb_seeds, seeds,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
r = mock_app.lambda_handler( r = app.lambda_handler(
http_api_proxy( http_api_proxy(
raw_path='/login', raw_path='/login',
method=HTTPMethod.GET, method=HTTPMethod.GET,
query_string_parameters={'continue': 'http://localhost'},
), ),
lambda_context, lambda_context,
) )
@@ -21,20 +22,19 @@ def test_html_page(
def test_login( def test_login(
mock_app, app,
dynamodb_seeds, seeds,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
r = mock_app.lambda_handler( r = app.lambda_handler(
http_api_proxy( http_api_proxy(
raw_path='/login', raw_path='/login',
method=HTTPMethod.POST, method=HTTPMethod.POST,
headers={ headers={
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
body='username=sergio@somosbeta.com.br&password=pytest@123', body='username=sergio@somosbeta.com.br&password=pytest@123&continue=https://localhost',
is_base64_encoded=False,
), ),
lambda_context, lambda_context,
) )

View File

@@ -1,7 +1,11 @@
// OAuth2
{"id": "OAUTH2_CLIENT", "sk": "CLIENT_ID#d72d4005-1fa7-4430-9754-80d5e2487bb6", "secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code"]} {"id": "OAUTH2_CLIENT", "sk": "CLIENT_ID#d72d4005-1fa7-4430-9754-80d5e2487bb6", "secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code"]}
{"id": "OAUTH2_CODE#CLIENT_ID#d72d4005-1fa7-4430-9754-80d5e2487bb6", "sk": "CODE#kyqp3oSuRFTfuBaCmq3XOgGWg67l42Kt3D6xPEj7Yd3MLdi9", "redirect_uri": "https://localhost/callback", "user_id": "0cb0ce87-9df6-40c1-9fa7-7dfdafd7910e", "nonce": "123", "scope": "openid profile email"} {"id": "OAUTH2_CODE#CLIENT_ID#d72d4005-1fa7-4430-9754-80d5e2487bb6", "sk": "CODE#kyqp3oSuRFTfuBaCmq3XOgGWg67l42Kt3D6xPEj7Yd3MLdi9", "redirect_uri": "https://localhost/callback", "user_id": "0cb0ce87-9df6-40c1-9fa7-7dfdafd7910e", "nonce": "123", "scope": "openid profile email"}
// Post-migration: uncomment the following line // Post-migration: uncomment the following line
// {"id": "EMAIL", "sk": "sergio@somosbeta.com.br", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419"} // {"id": "EMAIL", "sk": "sergio@somosbeta.com.br", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419"}
{"id": "email", "sk": "sergio@somosbeta.com.br", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419"} {"id": "email", "sk": "sergio@somosbeta.com.br", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419"}
// User data
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "0", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br"} {"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "0", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br"}
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "PASSWORD", "hash": "$pbkdf2-sha256$29000$IuTcm7M2BiAEgPB.b.3dGw$d8xVCbx8zxg7MeQBrOvCOgniiilsIHEMHzoH/OXftLQ"} {"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "PASSWORD", "hash": "$pbkdf2-sha256$29000$IuTcm7M2BiAEgPB.b.3dGw$d8xVCbx8zxg7MeQBrOvCOgniiilsIHEMHzoH/OXftLQ"}

View File

@@ -434,6 +434,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
] ]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]] [[package]]
name = "jmespath" name = "jmespath"
version = "1.0.1" version = "1.0.1"
@@ -469,7 +481,7 @@ wheels = [
[[package]] [[package]]
name = "layercake" name = "layercake"
version = "0.9.6" version = "0.9.7"
source = { directory = "../layercake" } source = { directory = "../layercake" }
dependencies = [ dependencies = [
{ name = "arnparse" }, { name = "arnparse" },
@@ -478,6 +490,7 @@ dependencies = [
{ name = "dictdiffer" }, { name = "dictdiffer" },
{ name = "ftfy" }, { name = "ftfy" },
{ name = "glom" }, { name = "glom" },
{ name = "jinja2" },
{ name = "meilisearch" }, { name = "meilisearch" },
{ name = "orjson" }, { name = "orjson" },
{ name = "passlib" }, { name = "passlib" },
@@ -500,6 +513,7 @@ requires-dist = [
{ name = "dictdiffer", specifier = ">=0.9.0" }, { name = "dictdiffer", specifier = ">=0.9.0" },
{ name = "ftfy", specifier = ">=6.3.1" }, { name = "ftfy", specifier = ">=6.3.1" },
{ name = "glom", specifier = ">=24.11.0" }, { name = "glom", specifier = ">=24.11.0" },
{ name = "jinja2", specifier = ">=3.1.6" },
{ name = "meilisearch", specifier = ">=0.34.0" }, { name = "meilisearch", specifier = ">=0.34.0" },
{ name = "orjson", specifier = ">=3.10.15" }, { name = "orjson", specifier = ">=3.10.15" },
{ name = "passlib", specifier = ">=1.7.4" }, { name = "passlib", specifier = ">=1.7.4" },
@@ -525,6 +539,34 @@ dev = [
{ name = "ruff", specifier = ">=0.11.1" }, { name = "ruff", specifier = ">=0.11.1" },
] ]
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
]
[[package]] [[package]]
name = "meilisearch" name = "meilisearch"
version = "0.36.0" version = "0.36.0"