diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index f79b8f1..2fedaea 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + workflow_dispatch: jobs: build: @@ -13,6 +14,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 + - name: Setup env + run: echo "VERSION=$(echo ${GITHUB_SHA} | cut -c1-7)" >> $GITHUB_ENV + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -30,6 +34,61 @@ jobs: with: context: . push: true - tags: ${{ secrets.DOCKER_USERNAME }}/fast-food:latest - - # TODO: deploy to k8s \ No newline at end of file + tags: ${{ secrets.DOCKER_USERNAME }}/fast-food:${{ env.VERSION }} + + k8s-deploy: + needs: build + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Install kubectl + run: | + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + sudo mv kubectl /usr/local/bin/kubectl + + - name: Verify kubectl version + run: kubectl version --client + + - name: Setup kubectl + run: aws eks --region ${{ secrets.AWS_REGION }} update-kubeconfig --name ${{ secrets.K8S_CLUSTER_NAME }} + + - name: Setup env + run: echo "VERSION=$(echo ${GITHUB_SHA} | cut -c1-7)" >> $GITHUB_ENV + + - name: Create dir for parsed k8s templates + run: | + mkdir -p ./k8s-deploy + + - name: Prepare k8s resource templates + env: + DB_HOSTNAME: ${{ secrets.DB_HOSTNAME }} + DB_DATABASE: ${{ secrets.DB_DATABASE }} + DB_USERNAME: ${{ secrets.DB_USERNAME }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + MERCADOPAGO_PUBLIC_KEY: ${{ secrets.MERCADOPAGO_PUBLIC_KEY }} + MERCADOPAGO_PRIVATE_ACCESS_TOKEN: ${{ secrets.MERCADOPAGO_PRIVATE_ACCESS_TOKEN }} + MERCADOPAGO_APP_USER_ID: ${{ secrets.MERCADOPAGO_APP_USER_ID }} + MERCADOPAGO_POINT_OF_SALE_ID: ${{ secrets.MERCADOPAGO_POINT_OF_SALE_ID }} + MERCADOPAGO_NOTIFICATIONS_URL: ${{ secrets.MERCADOPAGO_NOTIFICATIONS_URL }} + VERSION: ${{ env.VERSION }} + ENVIRONMENT: prod + run: | + for file in ./k8s/*.yaml; do + envsubst < "$file" > "./k8s-deploy/$(basename "$file")" + done + + - name: Deploy to k8s cluster + run: | + kubectl apply -f ./k8s-deploy/ + diff --git a/build.gradle b/build.gradle index 1e55bfb..aa899eb 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'io.jsonwebtoken:jjwt:0.9.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' implementation 'org.apache.httpcomponents:httpclient:4.5' compileOnly 'org.projectlombok:lombok' diff --git a/k8s/api-deployment.yaml b/k8s/api-deployment.template.yaml similarity index 58% rename from k8s/api-deployment.yaml rename to k8s/api-deployment.template.yaml index b43f100..c8f0b41 100644 --- a/k8s/api-deployment.yaml +++ b/k8s/api-deployment.template.yaml @@ -5,7 +5,6 @@ metadata: labels: app: api-deployment spec: - replicas: 1 selector: matchLabels: app: api-deployment @@ -18,10 +17,24 @@ spec: - name: app env: - name: DB_URL - value: "jdbc:postgresql://postgres-svc:5432/fastfood" + value: "jdbc:postgresql://$DB_HOSTNAME:5432/$DB_DATABASE" + - name: DB_USERNAME + value: "$DB_USERNAME" + - name: DB_PASSWORD + value: "$DB_PASSWORD" + - name: MERCADOPAGO_PUBLIC_KEY + value: "$MERCADOPAGO_PUBLIC_KEY" + - name: MERCADOPAGO_PRIVATE_ACCESS_TOKEN + value: "$MERCADOPAGO_PRIVATE_ACCESS_TOKEN" + - name: MERCADOPAGO_APP_USER_ID + value: "$MERCADOPAGO_APP_USER_ID" + - name: MERCADOPAGO_POINT_OF_SALE_ID + value: "$MERCADOPAGO_POINT_OF_SALE_ID" + - name: MERCADOPAGO_NOTIFICATIONS_URL + value: "$MERCADOPAGO_NOTIFICATIONS_URL" - name: SPRING_PROFILES_ACTIVE - value: "dev" - image: fiap7soat30/fast-food:latest + value: "$ENVIRONMENT" + image: fiap7soat30/fast-food:$VERSION imagePullPolicy: Always ports: - containerPort: 8080 diff --git a/k8s/api-hpa.yaml b/k8s/api-hpa.yaml index b449f08..e3bd756 100644 --- a/k8s/api-hpa.yaml +++ b/k8s/api-hpa.yaml @@ -7,8 +7,8 @@ spec: apiVersion: apps/v1 kind: Deployment name: api-deployment - minReplicas: 3 - maxReplicas: 10 + minReplicas: 2 + maxReplicas: 4 metrics: - type: Resource resource: diff --git a/k8s/api-svc.yaml b/k8s/api-svc.yaml index 7fe1549..090c8f7 100644 --- a/k8s/api-svc.yaml +++ b/k8s/api-svc.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: Service metadata: name: api-svc + annotations: + service.beta.kubernetes.io/aws-load-balancer-internal: "true" spec: selector: app: api-deployment @@ -9,5 +11,4 @@ spec: - protocol: TCP port: 80 targetPort: 8080 - nodePort: 30007 - type: NodePort \ No newline at end of file + type: LoadBalancer \ No newline at end of file diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml deleted file mode 100644 index ffd2438..0000000 --- a/k8s/configmap.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: db-config - labels: - name: db-config -data: - POSTGRES_URL: "jdbc:postgresql://postgres-svc:5432/fastfood" - POSTGRES_DB: "fastfood" - POSTGRES_USER: "postgres" - POSTGRES_PASSWORD: "123456" \ No newline at end of file diff --git a/k8s/postgres-deployment.yaml b/k8s/postgres-deployment.yaml deleted file mode 100644 index adbc697..0000000 --- a/k8s/postgres-deployment.yaml +++ /dev/null @@ -1,67 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: postgres-deployment - labels: - app: postgres-deployment -spec: - replicas: 1 - selector: - matchLabels: - app: postgres-deployment - template: - metadata: - labels: - app: postgres-deployment - spec: - containers: - - name: database - image: postgres:latest - imagePullPolicy: Always - ports: - - containerPort: 5432 - protocol: TCP - livenessProbe: - tcpSocket: - port: 5432 - initialDelaySeconds: 20 - periodSeconds: 20 - timeoutSeconds: 10 - failureThreshold: 2 - env: - - name: POSTGRES_URL - valueFrom: - configMapKeyRef: - name: db-config - key: POSTGRES_URL - - name: POSTGRES_DB - valueFrom: - configMapKeyRef: - name: db-config - key: POSTGRES_DB - - name: POSTGRES_USER - valueFrom: - configMapKeyRef: - name: db-config - key: POSTGRES_USER - - name: POSTGRES_PASSWORD - valueFrom: - configMapKeyRef: - name: db-config - key: POSTGRES_PASSWORD - volumeMounts: - - name: config-volume - mountPath: /docker-entrypoint-initdb.d/ - - name: postgres-storage - mountPath: /var/lib/postgresql/data - volumes: - - name: config-volume - configMap: - name: cm-init - items: - - key: init.sql - path: init.sql - - name: postgres-storage - persistentVolumeClaim: - claimName: postgres-pvc - restartPolicy: Always \ No newline at end of file diff --git a/k8s/postgres-pv.yaml b/k8s/postgres-pv.yaml deleted file mode 100644 index cde7796..0000000 --- a/k8s/postgres-pv.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: PersistentVolume -metadata: - name: postgres-pv - labels: - name: postgres-pv -spec: - capacity: - storage: 1Gi - accessModes: - - ReadWriteMany - storageClassName: local-storage - persistentVolumeReclaimPolicy: Delete - hostPath: - path: "/mnt/data/postgres" \ No newline at end of file diff --git a/k8s/postgres-pvc.yaml b/k8s/postgres-pvc.yaml deleted file mode 100644 index e7a93d5..0000000 --- a/k8s/postgres-pvc.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: postgres-pvc - labels: - name: postgres-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi - storageClassName: local-storage \ No newline at end of file diff --git a/k8s/postgres-svc.yaml b/k8s/postgres-svc.yaml deleted file mode 100644 index 8baef27..0000000 --- a/k8s/postgres-svc.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: postgres-svc -spec: - selector: - app: postgres-deployment - ports: - - protocol: TCP - port: 5432 - targetPort: 5432 - type: ClusterIP \ No newline at end of file diff --git a/scripts/deploy-k8s.sh b/scripts/deploy-k8s.sh index 323d513..24885a3 100755 --- a/scripts/deploy-k8s.sh +++ b/scripts/deploy-k8s.sh @@ -1,15 +1,3 @@ -# metric server -kubectl apply -f ./k8s/metrics.yaml - -# database -kubectl create configmap cm-init --from-file=init.sql -kubectl apply -f ./k8s/configmap.yaml -kubectl apply -f ./k8s/postgres-pv.yaml -kubectl apply -f ./k8s/postgres-pvc.yaml -kubectl apply -f ./k8s/postgres-svc.yaml -kubectl apply -f ./k8s/postgres-deployment.yaml - -# application kubectl apply -f ./k8s/api-svc.yaml kubectl apply -f ./k8s/api-hpa.yaml kubectl apply -f ./k8s/api-deployment.yaml diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/FastfoodApplication.java b/src/main/java/br/com/fiap/grupo30/fastfood/FastfoodApplication.java index dbba8e1..85c0f77 100644 --- a/src/main/java/br/com/fiap/grupo30/fastfood/FastfoodApplication.java +++ b/src/main/java/br/com/fiap/grupo30/fastfood/FastfoodApplication.java @@ -1,9 +1,20 @@ package br.com.fiap.grupo30.fastfood; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.servers.Server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication +@OpenAPIDefinition( + info = @Info(title = "My API", version = "v1"), + servers = { + @Server(url = "http://localhost:8080", description = "Local Development"), + @Server( + url = "https://29glms05ff.execute-api.us-east-1.amazonaws.com", + description = "Production Server") + }) public class FastfoodApplication { public static void main(String[] args) { diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/infrastructure/auth/AdminAuthorizationAspect.java b/src/main/java/br/com/fiap/grupo30/fastfood/infrastructure/auth/AdminAuthorizationAspect.java new file mode 100644 index 0000000..25e2167 --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/infrastructure/auth/AdminAuthorizationAspect.java @@ -0,0 +1,64 @@ +package br.com.fiap.grupo30.fastfood.infrastructure.auth; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Base64; +import java.util.List; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Aspect +@Component +public class AdminAuthorizationAspect { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private static String ADMIN_ROLE = "admin-group"; + private static String BEARER_TYPE = "Bearer"; + private static Integer JWT_PARTS = 3; + + @Before("@annotation(AdminRequired)") + public void checkAdminRole() throws Exception { + ServletRequestAttributes attrs = + (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = attrs.getRequest(); + HttpServletResponse response = attrs.getResponse(); + + String authorizationHeader = request.getHeader("Authorization"); + + if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER_TYPE)) { + response.sendError( + HttpServletResponse.SC_UNAUTHORIZED, "Missing or invalid Authorization header"); + return; + } + + String jwtToken = authorizationHeader.substring(7); + + String[] tokenParts = jwtToken.split("\\."); + + if (tokenParts.length != JWT_PARTS) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token structure"); + return; + } + + String payload = new String(Base64.getDecoder().decode(tokenParts[1])); + JsonNode jsonNode = objectMapper.readTree(payload); + + JsonNode groupsNode = jsonNode.get("cognito:groups"); + if (groupsNode != null && groupsNode.isArray()) { + List groups = objectMapper.convertValue(groupsNode, List.class); + if (!groups.contains(ADMIN_ROLE)) { + response.sendError( + HttpServletResponse.SC_UNAUTHORIZED, "User does not have admin role"); + return; + } + } else { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "No cognito:groups found"); + return; + } + } +} diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/infrastructure/auth/AdminRequired.java b/src/main/java/br/com/fiap/grupo30/fastfood/infrastructure/auth/AdminRequired.java new file mode 100644 index 0000000..61cff40 --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/infrastructure/auth/AdminRequired.java @@ -0,0 +1,10 @@ +package br.com.fiap.grupo30.fastfood.infrastructure.auth; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface AdminRequired {} diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/presentation/controllers/OrderController.java b/src/main/java/br/com/fiap/grupo30/fastfood/presentation/controllers/OrderController.java index 14a0d1a..f1ad40c 100644 --- a/src/main/java/br/com/fiap/grupo30/fastfood/presentation/controllers/OrderController.java +++ b/src/main/java/br/com/fiap/grupo30/fastfood/presentation/controllers/OrderController.java @@ -2,6 +2,7 @@ import br.com.fiap.grupo30.fastfood.domain.usecases.order.*; import br.com.fiap.grupo30.fastfood.infrastructure.gateways.CustomerGateway; +import br.com.fiap.grupo30.fastfood.infrastructure.auth.AdminRequired; import br.com.fiap.grupo30.fastfood.presentation.presenters.dto.AddCustomerCpfRequest; import br.com.fiap.grupo30.fastfood.presentation.presenters.dto.AddOrderProductRequest; import br.com.fiap.grupo30.fastfood.presentation.presenters.dto.OrderDTO; @@ -140,6 +141,7 @@ public ResponseEntity submitOrder(@PathVariable Long orderId) { return ResponseEntity.ok().body(order); } + @AdminRequired() @PostMapping(value = "/{orderId}/prepare") @Operation( summary = "Start preparing an order", @@ -150,6 +152,7 @@ public ResponseEntity startPreparingOrder(@PathVariable Long orderId) return ResponseEntity.ok().body(order); } + @AdminRequired() @PostMapping(value = "/{orderId}/ready") @Operation( summary = "Finish preparing an order", @@ -160,6 +163,7 @@ public ResponseEntity finishPreparingOrder(@PathVariable Long orderId) return ResponseEntity.ok().body(order); } + @AdminRequired() @PostMapping(value = "/{orderId}/deliver") @Operation( summary = "Deliver an order", diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/presentation/controllers/PaymentResource.java b/src/main/java/br/com/fiap/grupo30/fastfood/presentation/controllers/PaymentResource.java index fb6fc03..df495fa 100644 --- a/src/main/java/br/com/fiap/grupo30/fastfood/presentation/controllers/PaymentResource.java +++ b/src/main/java/br/com/fiap/grupo30/fastfood/presentation/controllers/PaymentResource.java @@ -2,6 +2,7 @@ import br.com.fiap.grupo30.fastfood.domain.usecases.payment.CollectOrderPaymentViaCashUseCase; import br.com.fiap.grupo30.fastfood.domain.usecases.payment.GeneratePaymentQrCodeUseCase; +import br.com.fiap.grupo30.fastfood.infrastructure.auth.AdminRequired; import br.com.fiap.grupo30.fastfood.presentation.presenters.dto.CollectPaymentViaCashRequest; import br.com.fiap.grupo30.fastfood.presentation.presenters.dto.OrderDTO; import br.com.fiap.grupo30.fastfood.presentation.presenters.dto.PaymentQrCodeDTO; @@ -41,6 +42,7 @@ public ResponseEntity generateQrCodeForPaymentCollection( return ResponseEntity.ok().body(qrCode); } + @AdminRequired() @PostMapping(value = "/{orderId}/collect") @Operation(summary = "Collect payment by cash") public ResponseEntity collectPaymentByBash( diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/presentation/controllers/ProductController.java b/src/main/java/br/com/fiap/grupo30/fastfood/presentation/controllers/ProductController.java index e7d4120..5426625 100644 --- a/src/main/java/br/com/fiap/grupo30/fastfood/presentation/controllers/ProductController.java +++ b/src/main/java/br/com/fiap/grupo30/fastfood/presentation/controllers/ProductController.java @@ -2,6 +2,7 @@ import br.com.fiap.grupo30.fastfood.domain.usecases.product.*; import br.com.fiap.grupo30.fastfood.infrastructure.gateways.CategoryGateway; +import br.com.fiap.grupo30.fastfood.infrastructure.auth.AdminRequired; import br.com.fiap.grupo30.fastfood.presentation.presenters.dto.ProductDTO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -64,6 +65,7 @@ public ResponseEntity findProductById(@PathVariable Long id) { return ResponseEntity.ok().body(dto); } + @AdminRequired() @PostMapping @Operation( summary = "Create a new product", @@ -89,6 +91,7 @@ public ResponseEntity createProduct(@RequestBody @Valid ProductDTO d return ResponseEntity.created(uri).body(dtoCreated); } + @AdminRequired() @PutMapping(value = PATH_VARIABLE_ID) @Operation( summary = "Update a product", @@ -110,6 +113,7 @@ public ResponseEntity updateProduct( return ResponseEntity.ok().body(dtoUpdated); } + @AdminRequired() @DeleteMapping(value = PATH_VARIABLE_ID) @Operation( summary = "Delete a product", diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 7dc975e..b09345f 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -15,8 +15,8 @@ spring.datasource.hikari.connection-timeout=60000 # app integrations.mercadopago.base-url=https://api.mercadopago.com -integrations.mercadopago.public-key=APP_USR-f597bfbc-e6dd-43db-9061-64652759b605 -integrations.mercadopago.access-token=APP_USR-5669469328830836-072119-fbf214281e7130fae8bbf302da74c439-1910105219 -integrations.mercadopago.app-user-id=1910105219 -integrations.mercadopago.point-of-sale-id=STORE00001POS001 +integrations.mercadopago.public-key=${MERCADOPAGO_PUBLIC_KEY:fake} +integrations.mercadopago.access-token=${MERCADOPAGO_PRIVATE_ACCESS_TOKEN:fake} +integrations.mercadopago.app-user-id=${MERCADOPAGO_APP_USER_ID:404} +integrations.mercadopago.point-of-sale-id=${MERCADOPAGO_POINT_OF_SALE_ID:fake} integrations.mercadopago.notifications-url=${MERCADOPAGO_NOTIFICATIONS_URL:https://e916-189-29-149-200.ngrok-free.app/webhooks/mercadopago} diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 4079659..220be02 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -16,8 +16,8 @@ spring.jpa.properties.hibernate.format_sql=true # app integrations.mercadopago.base-url=https://api.mercadopago.com -integrations.mercadopago.public-key=APP_USR-f597bfbc-e6dd-43db-9061-64652759b605 -integrations.mercadopago.access-token=APP_USR-5669469328830836-072119-fbf214281e7130fae8bbf302da74c439-1910105219 -integrations.mercadopago.app-user-id=1910105219 -integrations.mercadopago.point-of-sale-id=STORE00001POS001 +integrations.mercadopago.public-key=${MERCADOPAGO_PUBLIC_KEY:fake} +integrations.mercadopago.access-token=${MERCADOPAGO_PRIVATE_ACCESS_TOKEN:fake} +integrations.mercadopago.app-user-id=${MERCADOPAGO_APP_USER_ID:404} +integrations.mercadopago.point-of-sale-id=${MERCADOPAGO_POINT_OF_SALE_ID:fake} integrations.mercadopago.notifications-url=${MERCADOPAGO_NOTIFICATIONS_URL:https://e916-189-29-149-200.ngrok-free.app/webhooks/mercadopago}