diff --git a/api.saladeaula.digital/app/routes/courses/__init__.py b/api.saladeaula.digital/app/routes/courses/__init__.py index 9c6afc4..7b44cf8 100644 --- a/api.saladeaula.digital/app/routes/courses/__init__.py +++ b/api.saladeaula.digital/app/routes/courses/__init__.py @@ -1,4 +1,6 @@ +from http import HTTPStatus from io import BytesIO +from typing import Any from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler.api_gateway import Router @@ -6,12 +8,14 @@ from aws_lambda_powertools.event_handler.exceptions import ( BadRequestError, NotFoundError, ) +from layercake.dateutils import now from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from pydantic import UUID4, BaseModel -from python_multipart import parse_form +from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import COURSE_TABLE +from form_data import parse logger = Logger(__name__) router = Router() @@ -27,45 +31,53 @@ def get_course(course_id: str): class Cert(BaseModel): - exp_interval: int - rawfile: bytes + exp_interval: int | None = None + rawfile: bytes | None = None + + def model_dump(self, **kwargs) -> dict[str, Any]: + return super().model_dump( + exclude={'rawfile'}, + exclude_none=True, + **kwargs, + ) -class FormData(BaseModel): +class Course(BaseModel): id: UUID4 name: str access_period: int cert: Cert | None = None -@router.post('/') +@router.put('/') def edit_course(course_id: str): event = router.current_event if not event.decoded_body: raise BadRequestError('Invalid request body') - ret = {'id': course_id} body = BytesIO(event.decoded_body.encode()) - - def on_field(field): - field_name = field.field_name.decode().split('.') - - if len(field_name) > 1: - field_name, subfield_name = field_name - print(field_name, subfield_name) - else: - field_name, *_ = field_name - ret[field_name] = field.value - - parse_form( - event.headers, # type: ignore - body, - on_field=on_field, - on_file=on_field, + course = Course.model_validate( + {'id': course_id} | parse(event.headers, body), ) + now_ = now() - # print(ret.keys()) - data = FormData.model_validate(ret) - print(data) - return {} + with dyn.transact_writer() as transact: + transact.update( + key=KeyPair(str(course.id), '0'), + update_expr='SET #name = :name, access_period = :access_period, \ + cert = :cert, updated_at = :updated_at', + expr_attr_names={ + '#name': 'name', + }, + expr_attr_values={ + ':name': course.name, + ':cert': course.cert.model_dump() if course.cert else None, + ':access_period': course.access_period, + ':updated_at': now_, + }, + cond_expr='attribute_exists(sk)', + exc_cls=BadRequestError, + ) + + return JSONResponse(HTTPStatus.NO_CONTENT) diff --git a/api.saladeaula.digital/tests/routes/test_courses.py b/api.saladeaula.digital/tests/routes/test_courses.py index eaf1359..9c90ab1 100644 --- a/api.saladeaula.digital/tests/routes/test_courses.py +++ b/api.saladeaula.digital/tests/routes/test_courses.py @@ -1,5 +1,6 @@ from http import HTTPMethod, HTTPStatus +from layercake.dynamodb import DynamoDBPersistenceLayer from requests_toolbelt import MultipartEncoder from ..conftest import HttpApiProxy, LambdaContext @@ -24,14 +25,17 @@ def test_get_course( def test_edit_course( app, seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, http_api_proxy: HttpApiProxy, lambda_context: LambdaContext, ): + course_id = '2a8963fc-4694-4fe2-953a-316d1b10f1f5' + with open('tests/sample.html', 'rb') as f: m = MultipartEncoder( fields={ 'given_cert': 'true', - 'name': 'pytest', + 'name': 'pytest updated from test', 'access_period': '365', 'cert.exp_interval': '360', 'cert.rawfile': f, @@ -39,8 +43,8 @@ def test_edit_course( ) r = app.lambda_handler( http_api_proxy( - raw_path='/courses/2a8963fc-4694-4fe2-953a-316d1b10f1f5', - method=HTTPMethod.POST, + raw_path=f'/courses/{course_id}', + method=HTTPMethod.PUT, headers={ 'Content-Type': m.content_type, }, @@ -49,3 +53,10 @@ def test_edit_course( ), lambda_context, ) + assert r['statusCode'] == HTTPStatus.NO_CONTENT + + r = dynamodb_persistence_layer.get_item( + key={'id': course_id, 'sk': '0'}, + ) + + print(r) diff --git a/id.saladeaula.digital/client/wrangler.toml b/id.saladeaula.digital/client/wrangler.toml index bd2671d..e548d26 100644 --- a/id.saladeaula.digital/client/wrangler.toml +++ b/id.saladeaula.digital/client/wrangler.toml @@ -9,7 +9,7 @@ routes = [ mode = "smart" [vars] -ISSUER_URL = "https://58tkjsb308.execute-api.sa-east-1.amazonaws.com" +ISSUER_URL = "https://duiolq49qn25e.cloudfront.net" [observability.logs] enabled = true diff --git a/id.saladeaula.digital/template.yaml b/id.saladeaula.digital/template.yaml index c6442ac..7ece900 100644 --- a/id.saladeaula.digital/template.yaml +++ b/id.saladeaula.digital/template.yaml @@ -100,3 +100,35 @@ Resources: Path: /userinfo Method: GET ApiId: !Ref HttpApi + + OIDCDistribution: + Type: AWS::CloudFront::Distribution + Properties: + DistributionConfig: + Enabled: true + Origins: + - Id: OidcApiOrigin + DomainName: !Sub "${HttpApi}.execute-api.${AWS::Region}.amazonaws.com" + CustomOriginConfig: + OriginProtocolPolicy: https-only + DefaultCacheBehavior: + TargetOriginId: OidcApiOrigin + ViewerProtocolPolicy: redirect-to-https + AllowedMethods: [GET, HEAD, OPTIONS, PUT, PATCH, POST, DELETE] + CachedMethods: [GET, HEAD, OPTIONS] + ForwardedValues: + QueryString: true + DefaultTTL: 0 + MinTTL: 0 + MaxTTL: 0 + CacheBehaviors: + - PathPattern: "/.well-known/*" + TargetOriginId: OidcApiOrigin + ViewerProtocolPolicy: redirect-to-https + AllowedMethods: [GET, HEAD, OPTIONS] + CachedMethods: [GET, HEAD, OPTIONS] + ForwardedValues: + QueryString: false + DefaultTTL: 3600 # 1 hora + MinTTL: 300 # 5 min + MaxTTL: 86400 # 1 dia