diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index c59b314..fca481b 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -55,7 +55,44 @@ jobs: else echo "Invalid branch name!" && exit 1 fi - echo "STACK_NAME=ReservationStackScheduleANDCourts${{env.STAGE}}" >> $GITHUB_ENV + echo "STACK_NAME=ReservationStackApi" >> $GITHUB_ENV + + - name: Fetch config from SSM + run: | + STAGE="${{ github.ref_name }}" + + USER_API_BASE=$(aws ssm get-parameter \ + --name "/reservationmssuser/$STAGE/api/url" \ + --query "Parameter.Value" --output text) || true + + if [ -n "$USER_API_BASE" ]; then + echo "::add-mask::$USER_API_BASE" + fi + + echo "USER_API_BASE=$USER_API_BASE" >> $GITHUB_ENV + + - name: Validate variables from ssm + shell: /usr/bin/bash -e {0} + run: | + missing=() + + check_var() { + local key=$1 + local value=$2 + if [ -z "$value" ]; then + missing+=("$key") + fi + } + + check_var "USER_API_BASE" "$USER_API_BASE" + + if [ ${#missing[@]} -gt 0 ]; then + echo "❌ Missing environment variables:" + printf ' - %s\n' "${missing[@]}" + exit 1 + fi + + echo "✅ All environment variables from ssm are set" - uses: actions/checkout@v3 - uses: actions/setup-python@v4 @@ -66,7 +103,7 @@ jobs: run: | npm install -g aws-cdk cd iac - pip install -r requirements.txt + pip install -r requirements-infra.txt - name: DeployWithCDK run: | @@ -85,4 +122,4 @@ jobs: FROM_EMAIL: ${{ vars.FROM_EMAIL }} HIDDEN_COPY: ${{ vars.HIDDEN_COPY }} REPLY_TO_EMAIL: ${{ vars.REPLY_TO_EMAIL }} - USER_API_URL: ${{ secrets.USER_API_URL }} + USER_API_URL: ${{ env.USER_API_BASE }} diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4d12145..615e003 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -7,4 +7,4 @@ on: jobs: testApp: - uses: maua-dev/ci_workflows_reusable/.github/workflows/pytest_ci.yml@V1.0 + uses: maua-dev/ci_workflows_reusable/.github/workflows/pytest_ci_311.yml@V1.2 diff --git a/iac/adjust_layer_directory.py b/iac/adjust_layer_directory.py index 61845b2..ef1ad0a 100644 --- a/iac/adjust_layer_directory.py +++ b/iac/adjust_layer_directory.py @@ -6,7 +6,7 @@ # --- Configurações --- BUILD_DIRECTORY = "build" PYTHON_TOP_LEVEL_DIR = os.path.join(BUILD_DIRECTORY, "python") -REQUIREMENTS_FILE = "requirements-layer.txt" +REQUIREMENTS_FILE = "requirements-app.txt" # --- CONSTRUÇÃO CORRETA DO CAMINHO --- # Pega o diretório do projeto (a raiz 'reservation_api') subindo um nível a partir do script atual. diff --git a/iac/app.py b/iac/app.py index 6e19da3..063aef7 100644 --- a/iac/app.py +++ b/iac/app.py @@ -3,7 +3,7 @@ import aws_cdk as cdk from adjust_layer_directory import adjust_layer_directory -from stacks.iac_stack import IacStack +from stack.iac_stack import IacStack @@ -19,26 +19,29 @@ aws_region = os.environ.get("AWS_REGION") aws_account_id = os.environ.get("AWS_ACCOUNT_ID") stack_name = os.environ.get("STACK_NAME") -github_ref = os.environ.get("GITHUB_REF_NAME") - -stage = '' -if 'prod' in github_ref: - stage = 'PROD' -elif 'homolog' in github_ref: - stage = 'HOMOLOG' -elif 'dev' in github_ref: - stage = 'DEV' -else: - stage = 'TEST' + +# CD ja checa se esta dentro do branch name dev, hmlg, prod +# É ideal manter o CD como capitalize para seguir no mesmo esquema do stackname +stage = os.environ.get("GITHUB_REF_NAME").capitalize() tags = { - 'project': 'Reservation Courts and Schedule MSS', + 'project': 'Reservation Api MSS', 'stage': stage, - 'stack': 'BACK', + 'stack': stack_name, 'owner': 'DevCommunity' } -IacStack(app, stack_name, env=cdk.Environment(account=aws_account_id, region=aws_region), tags=tags) +IacStack( + app, + stack_id=stack_name, + stack_name=stack_name, + stage=stage, + env=cdk.Environment( + account=aws_account_id, + region=aws_region + ), + tags=tags +) app.synth() diff --git a/iac/components/apigw_construct.py b/iac/components/apigw_construct.py new file mode 100644 index 0000000..4a48446 --- /dev/null +++ b/iac/components/apigw_construct.py @@ -0,0 +1,84 @@ +from aws_cdk import Resource, aws_apigateway as apigateway +from constructs import Construct +from aws_cdk.aws_apigateway import RestApi, Cors, CorsOptions, GatewayResponse, ResponseType + + +class ApigwConstruct(Construct): + rest_api: RestApi + api_gateway_resource: Resource + + def __init__( + self, + scope: Construct, + construct_id: str, + stage: str, + stack_name: str, + **kwargs + ): + + stage = stage.capitalize() + + super().__init__(scope, construct_id, **kwargs) + + cors_options = CorsOptions( + allow_origins = + [ + "https://reservation.maua.br", + "https://reservation.devmaua.com" + ] + if stage == 'Prod' + else + [ + "https://reservation.hml.devmaua.com", + "https://reservation.dev.devmaua.com", + "https://localhost:3000", + "http://localhost:3000" + ], + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allow_headers=Cors.DEFAULT_HEADERS + ) + + self.rest_api = RestApi( + self, + id=f"{stack_name}_RestApi_{stage}", + rest_api_name=f"{stack_name}_RestApi_{stage}", + description="This is the Maua Reservation RestApi", + deploy_options=apigateway.StageOptions( + stage_name=stage.lower(), + logging_level=apigateway.MethodLoggingLevel.INFO, + data_trace_enabled=False, + metrics_enabled=True, + ), + default_cors_preflight_options=cors_options + ) + + GatewayResponse( + self, + "AuthorizerDenyResponse", + rest_api=self.rest_api, + type=ResponseType.ACCESS_DENIED, + response_headers={ + "Access-Control-Allow-Origin": "'*'", + "Access-Control-Allow-Headers": "'*'", + "Access-Control-Allow-Methods": "'*'", + }, + status_code="403" + ) + + GatewayResponse( + self, + "AuthorizerUnauthorizedResponse", + rest_api=self.rest_api, + type=ResponseType.UNAUTHORIZED, + response_headers={ + "Access-Control-Allow-Origin": "'*'", + "Access-Control-Allow-Headers": "'*'", + "Access-Control-Allow-Methods": "'*'", + }, + status_code="401" + ) + + self.api_gateway_resource = self.rest_api.root.add_resource( + "reservation-api", + default_cors_preflight_options=cors_options + ) \ No newline at end of file diff --git a/iac/components/dynamo_construct.py b/iac/components/dynamo_construct.py new file mode 100644 index 0000000..1ad288e --- /dev/null +++ b/iac/components/dynamo_construct.py @@ -0,0 +1,39 @@ +from aws_cdk import ( + aws_dynamodb as dynamodb, RemovalPolicy, +) +from constructs import Construct + +class DynamoConstruct(Construct): + table: dynamodb.Table + + def __init__( + self, + scope: Construct, + construct_id: str, + stage: str, + stack_name: str, + **kwargs + ) -> None: + super().__init__(scope, construct_id, **kwargs) + + stage = stage.capitalize() + + self.table = dynamodb.Table( + self, + id=f"{stack_name}_DynamoTable_{stage}", + table_name=f"{stack_name}_DynamoTable_{stage}", + partition_key=dynamodb.Attribute( + name="PK", + type=dynamodb.AttributeType.STRING + ), + sort_key=dynamodb.Attribute( + name="SK", + type=dynamodb.AttributeType.STRING + ), + billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, + removal_policy=RemovalPolicy.RETAIN if stage == "Prod" else RemovalPolicy.DESTROY + ) + + + + diff --git a/iac/stacks/lambda_stack.py b/iac/components/lambda_construct.py similarity index 73% rename from iac/stacks/lambda_stack.py rename to iac/components/lambda_construct.py index dbf1138..3230480 100644 --- a/iac/stacks/lambda_stack.py +++ b/iac/components/lambda_construct.py @@ -1,8 +1,6 @@ -import os - from aws_cdk import ( aws_lambda as lambda_, - NestedStack, Duration, + Duration, aws_apigateway as apigw ) from constructs import Construct @@ -10,13 +8,25 @@ from aws_cdk.aws_events import Rule, Schedule, EventField, RuleTargetInput from aws_cdk.aws_events_targets import LambdaFunction -class LambdaStack(Construct): - functions_that_need_dynamo_permissions = [] +class LambdaConstruct(Construct): + functions_that_need_dynamo_permissions: list + functions_that_need_s3_permissions: list + stage: str + stack_name: str - def create_lambda_api_gateway_integration(self, module_name: str, method: str, api_resource: Resource, - environment_variables: dict = {"STAGE": "TEST"}, authorizer=None): + def create_lambda_api_gateway_integration( + self, + module_name: str, + method: str, + api_resource: Resource, + environment_variables: dict = {"STAGE": "TEST"}, + authorizer=None + ): + function = lambda_.Function( - self, module_name.title(), + self, + id=module_name.title(), + function_name=f"{module_name}-{self.stack_name}-{self.stage}"[:63], code=lambda_.Code.from_asset(f"../src/modules/{module_name}"), handler=f"app.{module_name}_presenter.lambda_handler", runtime=lambda_.Runtime("python3.13"), @@ -25,20 +35,28 @@ def create_lambda_api_gateway_integration(self, module_name: str, method: str, a timeout=Duration.seconds(15) ) - api_resource.add_resource(module_name.replace("_", "-")).add_method(method, - integration=LambdaIntegration( - function), - authorizer=authorizer) - + api_resource.add_resource( + module_name.replace("_", "-") + ).add_method( + method, + integration=LambdaIntegration( + function + ), + authorizer=authorizer + ) return function - def create_lambda_event_bridge_integration(self, - module_name: str, - cron_schedule: Schedule.cron, - environment_variables: dict = {"STAGE": "TEST"}): + def create_lambda_event_bridge_integration( + self, + module_name: str, + cron_schedule: Schedule.cron, + environment_variables: dict = {"STAGE": "TEST"} + ): + function = lambda_.Function( self, - module_name.title(), + id=module_name.title(), + function_name=f"{module_name}-{self.stack_name}-{self.stage}"[:63], code=lambda_.Code.from_asset(f"../src/modules/{module_name}"), handler=f"app.{module_name}_presenter.lambda_handler", runtime=lambda_.Runtime("python3.13"), @@ -48,7 +66,9 @@ def create_lambda_event_bridge_integration(self, ) rule = Rule( - self, f"{module_name.title()}EventRuleForWeeklyUpload", + self, + id=f"{module_name.title()}EventRuleForWeeklyUpload", + rule_name=f"{module_name.title()}EventRuleForWeeklyUpload", schedule=cron_schedule ) @@ -61,26 +81,35 @@ def create_lambda_event_bridge_integration(self, return function - def __init__(self, scope: Construct, api_gateway_resource: Resource, environment_variables: dict) -> None: - self.github_ref = os.environ.get('GITHUB_REF_NAME') - self.stack_name = os.environ.get("STACK_NAME") - stage = '' - if 'prod' in self.github_ref: - stage = 'PROD' - elif 'homolog' in self.github_ref: - stage = 'HOMOLOG' - else: - stage = 'DEV' - - super().__init__(scope, f"{self.stack_name}_LambdaStack_{stage}") - - self.lambda_layer = lambda_.LayerVersion(self, f"{self.stack_name}_Lambda_Layer_{stage}", - code=lambda_.Code.from_asset("./build"), - compatible_runtimes=[lambda_.Runtime("python3.13")] - ) + def __init__( + self, + scope: Construct, + construct_id: str, + stage: str, + stack_name: str, + api_gateway_resource: Resource, + environment_variables: dict, + **kargs + ) -> None: + + self.stage = stage + self.stack_name = stack_name + + super().__init__(scope, construct_id, **kargs) + + self.lambda_layer = lambda_.LayerVersion( + self, + id=f"{stack_name}_LambdaLayer_{stage}", + layer_version_name=f"{stack_name}-LambdaLayer-{self.stage}", + # a pasta .build foi obtida do adjust layer directory, certifique-se de que a configuração da pasta layer gerada la esta igual + code=lambda_.Code.from_asset("./build"), + compatible_runtimes=[lambda_.Runtime("python3.13")] + ) authorizer_lambda = lambda_.Function( - self, "AuthorizerUserMssReservationApiLambda", + self, + id=f"LambdaUserMssAuthorizer-{self.stack_name}-{self.stage}", + function_name=f"lambda_user_mss_authorizer-{self.stack_name}-{self.stage}", code=lambda_.Code.from_asset("../src/shared/authorizer"), handler="user_mss_authorizer.lambda_handler", runtime=lambda_.Runtime("python3.13"), @@ -90,14 +119,14 @@ def __init__(self, scope: Construct, api_gateway_resource: Resource, environment ) token_authorizer_lambda = apigw.TokenAuthorizer( - self, "TokenAuthorizerReservationApi", + self, + id=f"TokenUserMssAuthorizer-{self.stack_name}-{self.stage}", + authorizer_name=f"user_mss_authorizer-{self.stack_name}-{self.stage}", handler=authorizer_lambda, identity_source=apigw.IdentitySource.header("Authorization"), - authorizer_name="AuthorizerUserMssReservationMssAlertLambda", results_cache_ttl=Duration.seconds(0) ) - #ready for auth self.create_booking = self.create_lambda_api_gateway_integration( module_name="create_booking", method="POST", @@ -106,7 +135,6 @@ def __init__(self, scope: Construct, api_gateway_resource: Resource, environment authorizer=token_authorizer_lambda ) - #ready for auth self.update_booking = self.create_lambda_api_gateway_integration( module_name="update_booking", method="PUT", @@ -115,7 +143,6 @@ def __init__(self, scope: Construct, api_gateway_resource: Resource, environment authorizer=token_authorizer_lambda ) - #not ready for auth AND not used? self.get_booking = self.create_lambda_api_gateway_integration( module_name="get_booking", method="GET", @@ -130,7 +157,6 @@ def __init__(self, scope: Construct, api_gateway_resource: Resource, environment environment_variables=environment_variables, ) - #not ready for auth??TODO self.delete_booking = self.create_lambda_api_gateway_integration( module_name="delete_booking", method="DELETE", @@ -139,7 +165,6 @@ def __init__(self, scope: Construct, api_gateway_resource: Resource, environment authorizer=token_authorizer_lambda ) - #not auth and unused self.get_all_bookings = self.create_lambda_api_gateway_integration( module_name="get_all_bookings", method="GET", @@ -147,7 +172,6 @@ def __init__(self, scope: Construct, api_gateway_resource: Resource, environment environment_variables=environment_variables ) - #ready for auth self.create_court = self.create_lambda_api_gateway_integration( module_name="create_court", method="POST", @@ -156,7 +180,6 @@ def __init__(self, scope: Construct, api_gateway_resource: Resource, environment authorizer=token_authorizer_lambda ) - #not ready TODO self.get_court = self.create_lambda_api_gateway_integration( module_name="get_court", method="GET", @@ -164,7 +187,6 @@ def __init__(self, scope: Construct, api_gateway_resource: Resource, environment environment_variables=environment_variables ) - #ready self.update_court = self.create_lambda_api_gateway_integration( module_name="update_court", method="PUT", @@ -173,7 +195,8 @@ def __init__(self, scope: Construct, api_gateway_resource: Resource, environment authorizer=token_authorizer_lambda ) - #ready + #TODO implementar lógica de deletar foto do bucket ao excluir a quadra pelo s3_client + # @43 ;) self.delete_court = self.create_lambda_api_gateway_integration( module_name="delete_court", method="DELETE", @@ -182,7 +205,6 @@ def __init__(self, scope: Construct, api_gateway_resource: Resource, environment authorizer=token_authorizer_lambda ) - #not ready? needed? self.get_all_courts = self.create_lambda_api_gateway_integration( module_name="get_all_courts", method="GET", @@ -190,7 +212,6 @@ def __init__(self, scope: Construct, api_gateway_resource: Resource, environment environment_variables=environment_variables ) - #yes self.health_check = self.create_lambda_api_gateway_integration( module_name="health_check", method="GET", @@ -239,5 +260,6 @@ def __init__(self, scope: Construct, api_gateway_resource: Resource, environment self.functions_that_need_s3_permissions = [ self.create_court, self.update_court, + self.delete_court, self.generate_report ] diff --git a/iac/components/s3_construct.py b/iac/components/s3_construct.py new file mode 100644 index 0000000..143de8e --- /dev/null +++ b/iac/components/s3_construct.py @@ -0,0 +1,40 @@ +from aws_cdk import ( + aws_s3 as s3, + aws_cloudfront as cloudfront, + aws_cloudfront_origins as origins, + RemovalPolicy, + Aws +) +from constructs import Construct + +class S3Construct(Construct): + + def __init__( + self, + scope: Construct, + construct_id: str, + stage: str, + stack_name: str, + **kwargs + ) -> None: + super().__init__(scope, construct_id, **kwargs) + + stage = stage.capitalize() + + self.bucket_spreadsheets = s3.Bucket( + self, + id=f"ReservationApi_Bucket_Back_{stage}", + bucket_name=f"reservationapi-spreadsheets-{stage.lower()}-{Aws.ACCOUNT_ID}-{Aws.REGION}", + versioned=True, + removal_policy=RemovalPolicy.DESTROY if not (stage == 'Prod') else RemovalPolicy.RETAIN, + block_public_access=s3.BlockPublicAccess.BLOCK_ALL + ) + + self.distribution = cloudfront.Distribution( + self, f"ReservationApiSpreadsheetsBucketDistribution{stage}", + default_behavior=cloudfront.BehaviorOptions( + origin=origins.S3Origin(self.bucket_spreadsheets), + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + ), + default_root_object=None # não obrigatório, mas evita erro se não tiver index.html + ) diff --git a/iac/components/ssm_construct.py b/iac/components/ssm_construct.py new file mode 100644 index 0000000..be37ff1 --- /dev/null +++ b/iac/components/ssm_construct.py @@ -0,0 +1,57 @@ +from constructs import Construct +from aws_cdk import Resource, aws_ssm as ssm +from aws_cdk.aws_apigateway import RestApi +from aws_cdk import aws_s3 as s3 + +class SsmConstruct(Construct): + + def __init__( + self, + scope: Construct, + construct_id: str, + stage: str, + mss_name_identification_for_path: str, + api: RestApi, + api_gateway_resource: Resource, + buckets: dict[str, s3.Bucket] = None, + extra_params: dict[str, str] = None, + **kwargs + ): + + super().__init__(scope, construct_id, **kwargs) + + # é necessário a '/' após a url pois no CD do front estamos contando como se ela ja estivesse la + + # stage lower é necessário aqui pois no actions do front, stage é recebido como lower + + stage = stage.lower() + + # aqui nao pode ter caracteres especiais, nem letras maiusculas. coloque apenas como o nome + # do microserviço tudo junto e tudo minusculo quando for passar no iac stack + + # podemos estudar passar o stackname como esse caminho. porem fiquemos atentos a caracteres especiais e letras maiusculas + + mss_name_identification_for_path = mss_name_identification_for_path.lower().replace("-", "_") + + if api: + ssm.StringParameter(self, + id=f"ApiUrl_{stage}", + parameter_name=f"/{mss_name_identification_for_path}/{stage}/api/url", + string_value=f"{api.url}{api_gateway_resource.path.lstrip('/')}/" + ) + + for logical_name, bucket in (buckets or {}).items(): + ssm.StringParameter(self, + id=f"Bucket_{logical_name}_{stage}", + parameter_name=f"/{mss_name_identification_for_path}/{stage}/buckets/{logical_name}", + string_value=bucket.bucket_name + ) + + for key, value in (extra_params or {}).items(): + safe_id = key.replace("/", "_") + ssm.StringParameter(self, + id=f"Extra_{safe_id}_{stage}", + parameter_name=f"/{mss_name_identification_for_path}/{stage}/{key}", + string_value=value + ) + \ No newline at end of file diff --git a/iac/requirements-dev.txt b/iac/requirements-dev.txt deleted file mode 100644 index 9270945..0000000 --- a/iac/requirements-dev.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==6.2.5 diff --git a/iac/requirements-infra.txt b/iac/requirements-infra.txt new file mode 100644 index 0000000..c4987ea --- /dev/null +++ b/iac/requirements-infra.txt @@ -0,0 +1 @@ +aws-cdk-lib==2.211.0 \ No newline at end of file diff --git a/iac/stacks/__init__.py b/iac/stack/__init__.py similarity index 100% rename from iac/stacks/__init__.py rename to iac/stack/__init__.py diff --git a/iac/stack/iac_stack.py b/iac/stack/iac_stack.py new file mode 100644 index 0000000..81aaf59 --- /dev/null +++ b/iac/stack/iac_stack.py @@ -0,0 +1,107 @@ +from aws_cdk import ( + Stack, aws_iam, Aws +) +from constructs import Construct +import os + +from components.apigw_construct import ApigwConstruct +from components.s3_construct import S3Construct +from components.dynamo_construct import DynamoConstruct +from components.lambda_construct import LambdaConstruct +from components.ssm_construct import SsmConstruct + + +class IacStack(Stack): + + def __init__( + self, + scope: Construct, + stack_id: str, + stage: str, + stack_name: str, + **kwargs + ) -> None: + + super().__init__(scope, stack_id, **kwargs) + + self.apigw_construct = ApigwConstruct( + self, + construct_id=f"{stack_name}-Apigw", + stage=stage, + stack_name=stack_name + ) + + self.dynamo_construct = DynamoConstruct( + self, + construct_id=f"{stack_name}-Dynamo", + stage=stage, + stack_name=stack_name + ) + + self.s3_construct = S3Construct( + self, + construct_id=f"{stack_name}-S3", + stage=stage, + stack_name=stack_name + ) + + self.ssm_construct = SsmConstruct( + self, + construct_id=f"{stack_name}-Ssm", + stage=stage, + # atenção para esse próximo parâmetro. de preferencia deixe tudo minusculo sem _ + # isso deve corresponder ao prefixo de caminho passado no CD dos outros mss (inclusive front) + # que acessam os parametros no ssm. + mss_name_identification_for_path="reservationapi", + api=self.apigw_construct.rest_api, + api_gateway_resource=self.apigw_construct.api_gateway_resource + ) + + ENVIRONMENT_VARIABLES = { + "STAGE": stage, + "DYNAMO_TABLE_NAME": self.dynamo_construct.table.table_name, + "DYNAMO_PARTITION_KEY": "PK", + "DYNAMO_SORT_KEY": "SK", + "REGION": Aws.REGION, + "USER_API_URL": os.environ.get("USER_API_URL"), + "S3_BUCKET_NAME": self.s3_construct.bucket_spreadsheets.bucket_name, + "FROM_EMAIL": os.environ.get("FROM_EMAIL"), + "HIDDEN_COPY": os.environ.get("HIDDEN_COPY"), + "S3_ASSETS_CDN": os.environ.get("S3_ASSETS_CDN") + } + + self.lambda_construct = LambdaConstruct( + self, + construct_id=f"{stack_name}-Lambda", + stage=stage, + stack_name=stack_name, + api_gateway_resource=self.apigw_construct.api_gateway_resource, + environment_variables=ENVIRONMENT_VARIABLES + ) + + for function in self.lambda_construct.functions_that_need_dynamo_permissions: + self.dynamo_construct.table.grant_read_write_data(function) + + for function in self.lambda_construct.functions_that_need_s3_permissions: + self.s3_construct.bucket_spreadsheets.grant_read_write(function) + + ses_admin_policy = aws_iam.PolicyStatement( + effect=aws_iam.Effect.ALLOW, + actions=[ + "ses:*", + ], + resources=[ + "*" + ] + ) + + functions_that_need_ses_permissions = [ + self.lambda_construct.delete_booking + ] + + for f in functions_that_need_ses_permissions: + f.add_environment("HIDDEN_COPY", os.environ.get("HIDDEN_COPY")) + f.add_environment("FROM_EMAIL", os.environ.get("FROM_EMAIL")) + f.add_environment("REPLY_TO_EMAIL", os.environ.get("REPLY_TO_EMAIL")) + f.add_to_role_policy(ses_admin_policy) + diff --git a/iac/stacks/bucket_stack.py b/iac/stacks/bucket_stack.py deleted file mode 100644 index 2e28d8d..0000000 --- a/iac/stacks/bucket_stack.py +++ /dev/null @@ -1,43 +0,0 @@ -from aws_cdk import ( - aws_s3 as s3, - aws_cloudfront as cloudfront, - aws_cloudfront_origins as origins, - aws_iam as iam, - RemovalPolicy, -) -from constructs import Construct -import os - -class BucketStack(Construct): - - def __init__(self, scope: Construct, **kwargs) -> None: - super().__init__(scope, "BucketStack", **kwargs) - - self.github_ref = os.environ.get('GITHUB_REF_NAME') - self.stack_name = os.environ.get("STACK_NAME") - - stage = '' - if 'prod' in self.github_ref: - stage = 'PROD' - elif 'homolog' in self.github_ref: - stage = 'HOMOLOG' - else: - stage = 'DEV' - - self.bucket = s3.Bucket( - self, f"RESERVATION_BACK_S3_BUCKET_{stage}", - # TODO remover isso quando voltar pra conta nova ou tentar deletar o bucket criado la com power user - bucket_name=f"{self.stack_name}-bucket-{stage}".lower(), - versioned=True, - removal_policy=RemovalPolicy.DESTROY if not (stage == 'PROD') else RemovalPolicy.RETAIN, - block_public_access=s3.BlockPublicAccess.BLOCK_ALL - ) - - self.distribution = cloudfront.Distribution( - self, f"ReservationBucketDistribution{stage}", - default_behavior=cloudfront.BehaviorOptions( - origin=origins.S3Origin(self.bucket), - viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - ), - default_root_object=None # não obrigatório, mas evita erro se não tiver index.html - ) diff --git a/iac/stacks/dynamo_stack.py b/iac/stacks/dynamo_stack.py deleted file mode 100644 index ba5680e..0000000 --- a/iac/stacks/dynamo_stack.py +++ /dev/null @@ -1,40 +0,0 @@ -from aws_cdk import ( - aws_dynamodb as dynamodb, RemovalPolicy, -) -from constructs import Construct -import os - -class DynamoStack(Construct): - table: dynamodb.Table - - def __init__(self, scope: Construct, **kwargs) -> None: - super().__init__(scope, "DynamoStack",**kwargs) - - self.github_ref = os.environ.get('GITHUB_REF_NAME') - self.stack_name = os.environ.get("STACK_NAME") - - stage = '' - if 'prod' in self.github_ref: - stage = 'PROD' - elif 'homolog' in self.github_ref: - stage = 'HOMOLOG' - else: - stage = 'DEV' - - self.table = dynamodb.Table( - self, f"{self.stack_name}_DynamoTable_{stage}", - partition_key=dynamodb.Attribute( - name="PK", - type=dynamodb.AttributeType.STRING - ), - sort_key=dynamodb.Attribute( - name="SK", - type=dynamodb.AttributeType.STRING - ), - billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, - removal_policy=RemovalPolicy.DESTROY - ) - - - - diff --git a/iac/stacks/iac_stack.py b/iac/stacks/iac_stack.py deleted file mode 100644 index e70c591..0000000 --- a/iac/stacks/iac_stack.py +++ /dev/null @@ -1,132 +0,0 @@ -from aws_cdk import ( - Stack, aws_iam -) -from constructs import Construct -from aws_cdk.aws_apigateway import RestApi, Cors, CorsOptions, GatewayResponse, ResponseType -import os - -from .bucket_stack import BucketStack -from .lambda_stack import LambdaStack -from .dynamo_stack import DynamoStack - - -class IacStack(Stack): - - def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: - super().__init__(scope, construct_id, **kwargs) - - self.github_ref = os.environ.get('GITHUB_REF_NAME') - stage = '' - if 'prod' in self.github_ref: - stage = 'PROD' - elif 'homolog' in self.github_ref: - stage = 'HOMOLOG' - else: - stage = 'DEV' - - self.aws_region = os.environ.get("AWS_REGION") - stack_name = os.environ.get("STACK_NAME") - - cors_options = CorsOptions( - allow_origins = - [ - "https://reservation.maua.br", - "https://reservation.devmaua.com" - ] - if stage == 'PROD' - else - [ - "https://reservation.hml.devmaua.com", - "https://reservation.dev.devmaua.com", - "https://localhost:3000", - "http://localhost:3000" - ], - allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allow_headers=Cors.DEFAULT_HEADERS - ) - - self.rest_api = RestApi( - self, f"{stack_name}_RestApi_{stage}", - rest_api_name=f"{stack_name}_RestApi_{stage}", - description="This is the Maua Reservation RestApi", - default_cors_preflight_options=cors_options - ) - - GatewayResponse( - self, - "AuthorizerDenyResponse", - rest_api=self.rest_api, - type=ResponseType.ACCESS_DENIED, - response_headers={ - "Access-Control-Allow-Origin": "'*'", - "Access-Control-Allow-Headers": "'*'", - "Access-Control-Allow-Methods": "'*'", - }, - status_code="403" - ) - - GatewayResponse( - self, - "AuthorizerUnauthorizedResponse", - rest_api=self.rest_api, - type=ResponseType.UNAUTHORIZED, - response_headers={ - "Access-Control-Allow-Origin": "'*'", - "Access-Control-Allow-Headers": "'*'", - "Access-Control-Allow-Methods": "'*'", - }, - status_code="401" - ) - - api_gateway_resource = self.rest_api.root.add_resource( - "reservation-api", - default_cors_preflight_options=cors_options - ) - - self.dynamo_table = DynamoStack(self) - self.s3_bucket = BucketStack(self) - - ENVIRONMENT_VARIABLES = { - "STAGE": stage, - "DYNAMO_TABLE_NAME": self.dynamo_table.table.table_name, - "DYNAMO_PARTITION_KEY": "PK", - "DYNAMO_SORT_KEY": "SK", - "REGION": self.aws_region, - "USER_API_URL": os.environ.get("USER_API_URL"), - "S3_BUCKET_NAME": self.s3_bucket.bucket.bucket_name, - "FROM_EMAIL": os.environ.get("FROM_EMAIL"), - "HIDDEN_COPY": os.environ.get("HIDDEN_COPY"), - "S3_ASSETS_CDN": os.environ.get("S3_ASSETS_CDN") - } - - - - self.lambda_stack = LambdaStack(self, api_gateway_resource=api_gateway_resource, - environment_variables=ENVIRONMENT_VARIABLES) - - for function in self.lambda_stack.functions_that_need_dynamo_permissions: - self.dynamo_table.table.grant_read_write_data(function) - - for function in self.lambda_stack.functions_that_need_s3_permissions: - self.s3_bucket.bucket.grant_read_write(function) - - ses_admin_policy = aws_iam.PolicyStatement( - effect=aws_iam.Effect.ALLOW, - actions=[ - "ses:*", - ], - resources=[ - "*" - ] - ) - - functions_that_need_ses_permissions = [ - self.lambda_stack.delete_booking - ] - - for f in functions_that_need_ses_permissions: - f.add_environment("HIDDEN_COPY", os.environ.get("HIDDEN_COPY")) - f.add_environment("FROM_EMAIL", os.environ.get("FROM_EMAIL")) - f.add_environment("REPLY_TO_EMAIL", os.environ.get("REPLY_TO_EMAIL")) - f.add_to_role_policy(ses_admin_policy) - diff --git a/requirements-app.txt b/requirements-app.txt new file mode 100644 index 0000000..185832d --- /dev/null +++ b/requirements-app.txt @@ -0,0 +1,10 @@ +pytest==8.4.1 +pytest-cov==6.2.1 +python-dotenv==1.1.1 +boto3==1.40.9 +aws-lambda-powertools==3.19.0 +aws_xray_sdk==2.14.0 + +pandas==2.2.3 +XlsxWriter==3.2.2 +requests==2.32.3 \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 2a9eb03..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,10 +0,0 @@ -pytest==6.2.5 -pytest-cov==4.0.0 -boto3==1.24.88 -python-dotenv==0.21.0 -aws-lambda-powertools==2.9.0 -aws_xray_sdk==2.11.0 - -pandas==2.2.3 -XlsxWriter==3.2.2 -requests==2.32.3 \ No newline at end of file diff --git a/requirements-layer.txt b/requirements-layer.txt deleted file mode 100644 index d6f5bfe..0000000 --- a/requirements-layer.txt +++ /dev/null @@ -1,7 +0,0 @@ -#boto3 compatibility -urllib3<1.27 - -#lambda needs -pandas==2.2.3 -XlsxWriter==3.2.2 -requests==2.32.3 \ No newline at end of file diff --git a/src/shared/authorizer/user_mss_authorizer.py b/src/shared/authorizer/user_mss_authorizer.py index b250dc1..21da681 100644 --- a/src/shared/authorizer/user_mss_authorizer.py +++ b/src/shared/authorizer/user_mss_authorizer.py @@ -34,7 +34,7 @@ def lambda_handler(event, context): # Fetching the user information from the user mss methodArn = event["methodArn"] headers = {"Authorization": f"Bearer {token}"} - response = http.request("GET", MSS_USER_API_ENDPOINT + "/reservation-mss-user/get-user", headers=headers) + response = http.request("GET", MSS_USER_API_ENDPOINT + "get-user", headers=headers) # Checking if the request was successful if response.status != 200: