From a8bb1799bca2e5caa5c2e50c6edc0de3751bf11b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Mon, 8 Dec 2025 12:40:35 -0300 Subject: [PATCH] add created by --- api.saladeaula.digital/app/app.py | 6 +- .../app/routes/enrollments/cancel.py | 7 +- .../app/routes/enrollments/enroll.py | 78 ++++++++- .../app/routes/orgs/__init__.py | 154 +----------------- .../app/routes/orgs/users/__init__.py | 21 ++- api.saladeaula.digital/tests/conftest.py | 19 ++- .../tests/routes/test_enrollments.py | 72 -------- .../tests/routes/test_orgs.py | 2 +- api.saladeaula.digital/tests/seeds.jsonl | 1 + 9 files changed, 127 insertions(+), 233 deletions(-) diff --git a/api.saladeaula.digital/app/app.py b/api.saladeaula.digital/app/app.py index 3bebd66..815fe17 100644 --- a/api.saladeaula.digital/app/app.py +++ b/api.saladeaula.digital/app/app.py @@ -14,6 +14,7 @@ from aws_lambda_powertools.utilities.typing import LambdaContext from api_gateway import JSONResponse from json_encoder import JSONEncoder +from middlewares.authentication_middleware import AuthenticationMiddleware from routes import courses, enrollments, orders, orgs, users logger = Logger(__name__) @@ -32,6 +33,7 @@ app = APIGatewayHttpResolver( debug=debug, serializer=serializer, ) +app.use(middlewares=[AuthenticationMiddleware()]) app.enable_swagger(path='/swagger') app.include_router(courses.router, prefix='/courses') app.include_router(enrollments.router, prefix='/enrollments') @@ -45,7 +47,7 @@ app.include_router(users.emails, prefix='/users') app.include_router(users.orgs, prefix='/users') app.include_router(users.password, prefix='/users') app.include_router(orders.router, prefix='/orders') -app.include_router(orgs.router, prefix='/orgs') +app.include_router(orgs.add, prefix='/orgs') app.include_router(orgs.admins, prefix='/orgs') app.include_router(orgs.custom_pricing, prefix='/orgs') app.include_router(orgs.scheduled, prefix='/orgs') @@ -59,7 +61,7 @@ def health(): @app.exception_handler(ServiceError) def exc_error(exc: ServiceError): - logger.exception(exc) + # logger.exception(exc) return JSONResponse( body={ diff --git a/api.saladeaula.digital/app/routes/enrollments/cancel.py b/api.saladeaula.digital/app/routes/enrollments/cancel.py index 58d0cc5..21a401f 100644 --- a/api.saladeaula.digital/app/routes/enrollments/cancel.py +++ b/api.saladeaula.digital/app/routes/enrollments/cancel.py @@ -11,6 +11,7 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from boto3clients import dynamodb_client from config import ENROLLMENT_TABLE +from middlewares.authentication_middleware import User as Authenticated logger = Logger(__name__) router = Router() @@ -33,6 +34,7 @@ def cancel( lock_hash: Annotated[str | None, Body(embed=True)] = None, ): now_ = now() + canceled_by: Authenticated = router.context['user'] with dyn.transact_writer() as transact: transact.update( @@ -55,7 +57,10 @@ def cancel( item={ 'id': enrollment_id, 'sk': 'CANCELED_BY', - 'canceled_by': {}, + 'canceled_by': { + 'id': canceled_by.id, + 'name': canceled_by.name, + }, 'created_at': now_, } ) diff --git a/api.saladeaula.digital/app/routes/enrollments/enroll.py b/api.saladeaula.digital/app/routes/enrollments/enroll.py index 7390ac5..1fedf9b 100644 --- a/api.saladeaula.digital/app/routes/enrollments/enroll.py +++ b/api.saladeaula.digital/app/routes/enrollments/enroll.py @@ -1,14 +1,88 @@ +from decimal import Decimal +from typing import Annotated + from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler.api_gateway import Router -from layercake.dynamodb import DynamoDBPersistenceLayer +from aws_lambda_powertools.event_handler.exceptions import ( + NotFoundError, +) +from aws_lambda_powertools.event_handler.openapi.params import Body +from layercake.batch import BatchProcessor +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair +from layercake.extra_types import CnpjStr, CpfStr, NameStr +from pydantic import UUID4, BaseModel, EmailStr, FutureDate from boto3clients import dynamodb_client from config import ENROLLMENT_TABLE +from middlewares.authentication_middleware import User as Authenticated logger = Logger(__name__) router = Router() dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) +processor = BatchProcessor() + + +class SubscriptionNotFoundError(NotFoundError): ... + + +class User(BaseModel): + id: str | UUID4 + name: NameStr + cpf: CpfStr + email: EmailStr + + +class Course(BaseModel): + id: UUID4 + name: str + access_period: int + unit_price: Decimal + + +class Enrollment(BaseModel): + user: User + course: Course + scheduled_for: FutureDate | None = None + + +class Org(BaseModel): + id: str | UUID4 + name: str + cnpj: CnpjStr @router.post('/') -def enroll(): ... +def enroll( + org_id: Annotated[UUID4 | str, Body(embed=True)], + enrollments: Annotated[tuple[Enrollment, ...], Body(embed=True)], +): + created_by: Authenticated = router.context['user'] + org = dyn.collection.get_items( + KeyPair( + pk=str(org_id), + sk='0', + ) + + KeyPair( + pk='SUBSCRIPTION', + sk=f'ORG#{org_id}', + rename_key='subscription', + ) + ) + + subscribed = 'subscription' in org + if not subscribed: + return checkout(Org.model_validate(org), enrollments, created_by=created_by) + + scheduled, unscheduled = [], [] + for x in enrollments: + (scheduled if x.scheduled_for else unscheduled).append(x) + + print(scheduled, created_by) + + +def checkout( + org: Org, + enrollments: tuple[Enrollment, ...], + created_by: Authenticated, +): + print(org, enrollments, created_by) diff --git a/api.saladeaula.digital/app/routes/orgs/__init__.py b/api.saladeaula.digital/app/routes/orgs/__init__.py index 9a5afa8..f2ff422 100644 --- a/api.saladeaula.digital/app/routes/orgs/__init__.py +++ b/api.saladeaula.digital/app/routes/orgs/__init__.py @@ -1,157 +1,7 @@ -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 NotFoundError -from aws_lambda_powertools.event_handler.openapi.params import Body -from layercake.dateutils import now -from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair -from layercake.extra_types import CnpjStr, NameStr -from pydantic import UUID4, BaseModel, EmailStr - -from api_gateway import JSONResponse -from boto3clients import dynamodb_client -from config import INTERNAL_EMAIL_DOMAIN, USER_TABLE -from exceptions import ConflictError - +from .add import router as add from .admins import router as admins from .custom_pricing import router as custom_pricing from .enrollments.scheduled import router as scheduled from .users import router as users -router = Router() -dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) - - -__all__ = ['admins', 'custom_pricing', 'scheduled', 'users'] - - -class CNPJConflictError(ConflictError): ... - - -class EmailConflictError(ConflictError): ... - - -class UserNotFoundError(NotFoundError): ... - - -class EmailNotFoundError(NotFoundError): ... - - -class User(BaseModel): - id: str | UUID4 - name: NameStr - email: EmailStr - - -@router.post('/') -def add_org( - name: Annotated[str, Body(embed=True)], - cnpj: Annotated[CnpjStr, Body(embed=True)], - user: Annotated[User, Body(embed=True)], -): - now_ = now() - org_id = str(uuid4()) - email = f'org+{cnpj}@{INTERNAL_EMAIL_DOMAIN}' - - with dyn.transact_writer() as transact: - transact.put( - item={ - # Post-migration (users): rename `cnpj` to `CNPJ` - 'id': 'cnpj', - 'sk': cnpj, - 'org_id': org_id, - 'created_at': now_, - }, - cond_expr='attribute_not_exists(sk)', - exc_cls=CNPJConflictError, - ) - transact.put( - item={ - # Post-migration (users): rename `email` to `EMAIL` - 'id': 'email', - 'sk': email, - 'user_id': org_id, - 'created_at': now_, - }, - cond_expr='attribute_not_exists(sk)', - exc_cls=EmailConflictError, - ) - transact.put( - item={ - 'id': org_id, - 'sk': '0', - 'name': name, - 'email': email, - 'cnpj': cnpj, - 'created_at': now_, - } - ) - transact.put( - item={ - 'id': org_id, - # Post-migration: rename `emails` to `EMAIL` - 'sk': f'emails#{email}', - 'email_primary': True, - 'email_verified': True, - 'mx_record_exists': True, - 'created_at': now_, - } - ) - transact.put( - item={ - 'id': org_id, - # Post-migration (users): rename `admins#` to `ADMIN#` - 'sk': f'admins#{user.id}', - 'name': user.name, - 'email': user.email, - 'created_at': now_, - } - ) - transact.put( - item={ - 'id': user.id, - # Post-migration (users): rename `orgs#` to `ORG#` - 'sk': f'orgs#{org_id}', - 'name': name, - 'cnpj': cnpj, - 'created_at': now_, - } - ) - transact.put( - item={ - 'id': user.id, - 'sk': f'SCOPE#{org_id}', - 'scope': {'apps:admin'}, - 'created_at': now_, - } - ) - transact.put( - item={ - # Post-migration (users): rename `orgmembers#` to `MEMBER#ORG#` - 'id': f'orgmembers#{org_id}', - 'sk': user.id, - 'created_at': now_, - } - ) - transact.condition( - key=KeyPair(str(user.id), '0'), - cond_expr='attribute_exists(sk)', - exc_cls=UserNotFoundError, - ) - transact.condition( - # Post-migration (users): rename `email` to `EMAIL` - key=KeyPair('email', user.email), - cond_expr='attribute_exists(sk)', - exc_cls=EmailNotFoundError, - ) - - return JSONResponse( - status_code=HTTPStatus.CREATED, - body={ - 'id': org_id, - 'name': name, - 'email': email, - }, - ) +__all__ = ['add', 'admins', 'custom_pricing', 'scheduled', 'users'] diff --git a/api.saladeaula.digital/app/routes/orgs/users/__init__.py b/api.saladeaula.digital/app/routes/orgs/users/__init__.py index 30f4662..80b7d81 100644 --- a/api.saladeaula.digital/app/routes/orgs/users/__init__.py +++ b/api.saladeaula.digital/app/routes/orgs/users/__init__.py @@ -14,6 +14,7 @@ from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import INTERNAL_EMAIL_DOMAIN, USER_TABLE from exceptions import ConflictError +from middlewares.authentication_middleware import User as Authenticated router = Router() dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) @@ -53,8 +54,9 @@ def add( org: Annotated[Org, Body(embed=True)], ): org.id = org_id + created_by: Authenticated = router.context['user'] - if _create_user(user, org): + if _create_user(user, org, created_by): return JSONResponse(HTTPStatus.CREATED) user_id = _get_user_id(user) @@ -101,7 +103,11 @@ def unlink(org_id: str, user_id: str): return JSONResponse(HTTPStatus.NO_CONTENT) -def _create_user(user: User, org: Org) -> bool: +def _create_user( + user: User, + org: Org, + created_by: Authenticated, +) -> bool: now_ = now() user_id = uuid4() email_verified = INTERNAL_EMAIL_DOMAIN in user.email @@ -130,6 +136,17 @@ def _create_user(user: User, org: Org) -> bool: 'created_at': now_, } ) + transact.put( + item={ + 'id': user_id, + 'sk': 'CREATED_BY', + 'created_by': { + 'id': created_by.id, + 'name': created_by.name, + }, + 'created_at': now_, + } + ) transact.put( item={ 'id': user_id, diff --git a/api.saladeaula.digital/tests/conftest.py b/api.saladeaula.digital/tests/conftest.py index a81fb46..4306adb 100644 --- a/api.saladeaula.digital/tests/conftest.py +++ b/api.saladeaula.digital/tests/conftest.py @@ -62,7 +62,24 @@ class HttpApiProxy: 'requestContext': { 'accountId': '123456789012', 'apiId': 'api-id', - 'authorizer': {}, + 'authorizer': { + 'jwt': { + 'claims': { + 'aud': '1db63660-063d-4280-b2ea-388aca4a9459', + 'client_id': '1db63660-063d-4280-b2ea-388aca4a9459', + 'email': 'sergio@somosbeta.com.br', + 'email_verified': 'true', + 'exp': '1765205975', + 'iat': '1765202375', + 'iss': 'https://id.saladeaula.digital', + 'jti': 'Fbbyvwwze3npdEgs', + 'name': 'Sérgio R Siqueira', + 'scope': 'openid profile email offline_access apps:admin', + 'sub': '5OxmMjL-ujoR5IMGegQz', + }, + 'scopes': None, + } + }, 'domainName': 'id.execute-api.us-east-1.amazonaws.com', 'domainPrefix': 'id', 'http': { diff --git a/api.saladeaula.digital/tests/routes/test_enrollments.py b/api.saladeaula.digital/tests/routes/test_enrollments.py index 7a2b50a..d231747 100644 --- a/api.saladeaula.digital/tests/routes/test_enrollments.py +++ b/api.saladeaula.digital/tests/routes/test_enrollments.py @@ -1,8 +1,6 @@ import json from http import HTTPMethod, HTTPStatus -from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair - from ..conftest import HttpApiProxy, LambdaContext @@ -24,73 +22,3 @@ def test_get_enrollment( body = json.loads(r['body']) assert 'user' in body assert 'course' in body - - -def test_get_scormset( - app, - seeds, - http_api_proxy: HttpApiProxy, - lambda_context: LambdaContext, -): - r = app.lambda_handler( - http_api_proxy( - raw_path='/enrollments/9c166c5e-890f-4e77-9855-769c29aaeb2e/scorm', - method=HTTPMethod.GET, - ), - lambda_context, - ) - assert r['statusCode'] == HTTPStatus.OK - - body = json.loads(r['body']) - print(body) - - -def test_post_scormset( - app, - seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, - http_api_proxy: HttpApiProxy, - lambda_context: LambdaContext, -): - scormbody = { - 'suspend_data': '{"v":2,"d":[123,34,112,114,111,103,114,101,115,115,34,58,256,108,263,115,111,110,265,267,34,48,266,256,112,266,49,53,44,34,105,278,276,287,99,281,284,286,275,277,275,290,58,49,125,300,284,49,289,291,285,287,295,256,297,299,302,304,298,125,284,50,313,299,301,34,317,275,293,123,320,51,287,324,320,52,328,278,320,53,332,267,320,54,336,325,315,34,55,340,320,56,345,342,57,348,302,308,306,337,342,49,303,323,333,356,322,256,329,300,365,125],"cpv":"_lnxccXW"}', - 'launch_data': '', - 'comments': '', - 'comments_from_lms': '', - 'core': { - 'student_id': '', - 'student_name': '', - 'lesson_location': '', - 'credit': '', - 'lesson_status': 'incomplete', - 'entry': '', - 'lesson_mode': 'normal', - 'exit': 'suspend', - 'session_time': '00:00:00', - 'score': {'raw': '', 'min': '', 'max': '100'}, - 'total_time': '00:00:00', - }, - 'objectives': {}, - 'student_data': { - 'mastery_score': '', - 'max_time_allowed': '', - 'time_limit_action': '', - }, - 'student_preference': {'audio': '', 'language': '', 'speed': '', 'text': ''}, - 'interactions': {}, - } - - r = app.lambda_handler( - http_api_proxy( - raw_path='/enrollments/578ec87f-94c7-4840-8780-bb4839cc7e64/scorm', - method=HTTPMethod.POST, - body=scormbody, - ), - lambda_context, - ) - assert r['statusCode'] == HTTPStatus.NO_CONTENT - - r = dynamodb_persistence_layer.collection.get_item( - KeyPair('578ec87f-94c7-4840-8780-bb4839cc7e64', 'SCORM_COMMIT#LAST') - ) - assert r['cmi']['suspend_data'] == scormbody['suspend_data'] diff --git a/api.saladeaula.digital/tests/routes/test_orgs.py b/api.saladeaula.digital/tests/routes/test_orgs.py index 17b7916..9872eb3 100644 --- a/api.saladeaula.digital/tests/routes/test_orgs.py +++ b/api.saladeaula.digital/tests/routes/test_orgs.py @@ -13,7 +13,7 @@ def test_add_org( http_api_proxy: HttpApiProxy, lambda_context: LambdaContext, ): - user_id = '213a6682-2c59-4404-9189-12eec0a846d4' + user_id = '15bacf02-1535-4bee-9022-19d106fd7518' r = app.lambda_handler( http_api_proxy( raw_path='/orgs', diff --git a/api.saladeaula.digital/tests/seeds.jsonl b/api.saladeaula.digital/tests/seeds.jsonl index af30188..c29ff61 100644 --- a/api.saladeaula.digital/tests/seeds.jsonl +++ b/api.saladeaula.digital/tests/seeds.jsonl @@ -25,6 +25,7 @@ // CNPJs {"id": "cnpj", "sk": "04978826000180", "org_id": "2a8963fc-4694-4fe2-953a-316d1b10f1f5"} {"id": "cnpj", "sk": "00000000000191", "org_id": "6000f79-6e5c-49a0-952f-3bda330ef278"} +{"id": "SUBSCRIPTION", "sk": "ORG#2a8963fc-4694-4fe2-953a-316d1b10f1f5"} // CPFs {"id": "cpf", "sk": "07879819908", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518"}