diff --git a/.github/workflows/deploy-aws.yml b/.github/workflows/deploy-aws.yml new file mode 100644 index 00000000..f67b4ed7 --- /dev/null +++ b/.github/workflows/deploy-aws.yml @@ -0,0 +1,253 @@ +name: Deploy to AWS + +on: + workflow_dispatch: + push: + branches: + - infra-test + + paths: + - 'client/**' + - 'cms/**' + - '.github/workflows/*' + - 'infrastructure/**' + - 'package.json' + +jobs: + set_environment: + runs-on: ubuntu-latest + name: Set Deployment Environment + outputs: + env_name: ${{ steps.set_env.outputs.env_name }} + steps: + - id: set_env + run: echo "env_name=${{ github.ref_name == 'infra-test' && 'dev' || github.ref_name }}" >> $GITHUB_OUTPUT + + trigger_build: + runs-on: ubuntu-latest + outputs: + build_cms: ${{ steps.changes.outputs.cms == 'true' || github.ref_name == 'staging' || github.ref_name == 'main' }} + build_client: ${{ steps.changes.outputs.client == 'true' || github.ref_name == 'staging' || github.ref_name == 'main' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Detect changes in client and CMS paths + uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + client: + - '.github/workflows/**' + - 'client/**' + cms: + - '.github/workflows/**' + - 'cms/**' + + build_client: + needs: [set_environment, trigger_build] + if: ${{ github.event_name == 'workflow_dispatch' || needs.trigger_build.outputs.build_client == 'true' }} + environment: + name: ${{ needs.set_environment.outputs.env_name }} + runs-on: ubuntu-latest + name: Build Client image and push to Amazon ECR + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.PIPELINE_USER_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.PIPELINE_USER_SECRET_ACCESS_KEY }} + aws-region: ${{ vars.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + with: + mask-password: 'true' + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Debug environment variables + run: | + echo "NODE_ENV: ${{ vars.NODE_ENV }}" + echo "NEXT_PUBLIC_URL: ${{ vars.NEXT_PUBLIC_URL }}" + echo "NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }}" + echo "NEXT_PUBLIC_MAPBOX_API_TOKEN: ${{ vars.NEXT_PUBLIC_MAPBOX_API_TOKEN }}" + echo "NEXT_PUBLIC_MAPBOX_USERNAME: ${{ vars.NEXT_PUBLIC_MAPBOX_USERNAME }}" + echo "NEXT_PUBLIC_MAPBOX_STYLE_ID: ${{ vars.NEXT_PUBLIC_MAPBOX_STYLE_ID }}" + echo "NEXT_PUBLIC_MATOMO_URL: ${{ vars.NEXT_PUBLIC_MATOMO_URL }}" + echo "NEXT_PUBLIC_MATOMO_SITE_ID: ${{ vars.NEXT_PUBLIC_MATOMO_SITE_ID }}" + echo "NEXT_PUBLIC_PREVIEW_SECRET: ${{ secrets.NEXT_PUBLIC_PREVIEW_SECRET != '' && '***SET***' || '***NOT SET***' }}" + echo "CLIENT_REPOSITORY_NAME: ${{ vars.CLIENT_REPOSITORY_NAME }}" + echo "Environment: ${{ needs.set_environment.outputs.env_name }}" + + - name: Build, tag, and push Client image to Amazon ECR + uses: docker/build-push-action@v5 + with: + build-args: | + NEXT_PUBLIC_ENVIRONMENT=${{ vars.NODE_ENV }} + NEXT_PUBLIC_URL=${{ vars.NEXT_PUBLIC_URL }} + NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL }} + NEXT_PUBLIC_BASE_PATH=${{ vars.NEXT_PUBLIC_BASE_PATH }} + NEXT_PUBLIC_MAPBOX_API_TOKEN=${{ vars.NEXT_PUBLIC_MAPBOX_API_TOKEN }} + NEXT_PUBLIC_MAPBOX_USERNAME=${{ vars.NEXT_PUBLIC_MAPBOX_USERNAME }} + NEXT_PUBLIC_MAPBOX_STYLE_ID=${{ vars.NEXT_PUBLIC_MAPBOX_STYLE_ID }} + NEXT_PUBLIC_MATOMO_URL=${{ vars.NEXT_PUBLIC_MATOMO_URL }} + NEXT_PUBLIC_MATOMO_SITE_ID=${{ vars.NEXT_PUBLIC_MATOMO_SITE_ID }} + NEXT_PUBLIC_PREVIEW_SECRET=${{ secrets.NEXT_PUBLIC_PREVIEW_SECRET }} + context: . + cache-from: type=gha + cache-to: type=gha,mode=max + file: ./client/Dockerfile.prod + push: true + tags: | + ${{ steps.login-ecr.outputs.registry }}/${{ vars.CLIENT_REPOSITORY_NAME }}:${{ github.sha }} + ${{ steps.login-ecr.outputs.registry }}/${{ vars.CLIENT_REPOSITORY_NAME }}:${{ needs.set_environment.outputs.env_name }} + + build_cms: + needs: [set_environment, trigger_build] + if: ${{ github.event_name == 'workflow_dispatch' || needs.trigger_build.outputs.build_cms == 'true' }} + environment: + name: ${{ needs.set_environment.outputs.env_name }} + runs-on: ubuntu-latest + name: Build CMS image and push to Amazon ECR + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.PIPELINE_USER_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.PIPELINE_USER_SECRET_ACCESS_KEY }} + aws-region: ${{ vars.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + with: + mask-password: 'true' + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build, tag, and push CMS image to Amazon ECR + uses: docker/build-push-action@v5 + with: + build-args: | + NODE_ENV=${{ vars.NODE_ENV }} + CMS_URL=${{ vars.CMS_URL }} + DATABASE_CLIENT=postgres + DATABASE_SSL=true + DATABASE_SSL_REJECT_UNAUTHORIZED=false + BUCKET_BUCKET=${{ vars.BUCKET_BUCKET }} + BUCKET_REGION=${{ vars.BUCKET_REGION }} + BUCKET_ENDPOINT=${{ vars.BUCKET_ENDPOINT }} + BUCKET_ACCESS_KEY=${{ secrets.BUCKET_ACCESS_KEY }} + BUCKET_SECRET_KEY=${{ secrets.BUCKET_SECRET_KEY }} + STRAPI_MEDIA_LIBRARY_PROVIDER=${{ vars.STRAPI_MEDIA_LIBRARY_PROVIDER }} + STRAPI_ADMIN_MAPBOX_ACCESS_TOKEN=${{ vars.NEXT_PUBLIC_MAPBOX_API_TOKEN }} + STRAPI_ADMIN_MAPBOX_USERNAME=${{ vars.NEXT_PUBLIC_MAPBOX_USERNAME }} + STRAPI_ADMIN_MAPBOX_STYLE_ID=${{ vars.NEXT_PUBLIC_MAPBOX_STYLE_ID }} + STRAPI_ADMIN_API_BASE_URL=${{ vars.STRAPI_ADMIN_API_BASE_URL }} + DATABASE_URL=${{ secrets.DATABASE_URL }} + DATABASE_HOST=${{ secrets.DATABASE_HOST }} + DATABASE_NAME=${{ secrets.DATABASE_NAME }} + DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }} + DATABASE_USERNAME=${{ secrets.DATABASE_USERNAME }} + DATABASE_PORT=${{ secrets.DATABASE_PORT }} + AWS_SES_ACCESS_KEY_ID=${{ secrets.AWS_SES_ACCESS_KEY_ID }} + AWS_SES_ACCESS_KEY_SECRET=${{ secrets.AWS_SES_ACCESS_KEY_SECRET }} + AWS_SES_DOMAIN=${{ secrets.AWS_SES_DOMAIN }} + ADMIN_JWT_SECRET=${{ secrets.ADMIN_JWT_SECRET }} + API_TOKEN_SALT=${{ secrets.API_TOKEN_SALT }} + JWT_SECRET=${{ secrets.JWT_SECRET }} + TRANSFER_TOKEN_SALT=${{ secrets.TRANSFER_TOKEN_SALT }} + APP_KEYS=${{ secrets.APP_KEYS }} + PORT=1337 + context: . + cache-from: type=gha + cache-to: type=gha,mode=max + file: ./cms/Dockerfile.prod + push: true + tags: | + ${{ steps.login-ecr.outputs.registry }}/${{ vars.CMS_REPOSITORY_NAME }}:${{ github.sha }} + ${{ steps.login-ecr.outputs.registry }}/${{ vars.CMS_REPOSITORY_NAME }}:${{ needs.set_environment.outputs.env_name }} + + deploy: + name: Deploy Services to Amazon EBS + needs: [set_environment, build_client, build_cms] + if: > + !failure() && + ( + needs.build_client.result == 'success' || + needs.build_cms.result == 'success' + ) + runs-on: ubuntu-latest + environment: + name: ${{ needs.set_environment.outputs.env_name }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.PIPELINE_USER_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.PIPELINE_USER_SECRET_ACCESS_KEY }} + aws-region: ${{ vars.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Generate docker compose file + working-directory: infrastructure/terraform/source_bundle + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY_CLIENT: ${{ vars.CLIENT_REPOSITORY_NAME }} + ECR_REPOSITORY_CMS: ${{ vars.CMS_REPOSITORY_NAME }} + IMAGE_TAG: ${{ needs.set_environment.outputs.env_name }} + run: | + cat <> docker-compose.yml + services: + client: + image: $ECR_REGISTRY/$ECR_REPOSITORY_CLIENT:$IMAGE_TAG + restart: unless-stopped + cms: + image: $ECR_REGISTRY/$ECR_REPOSITORY_CMS:$IMAGE_TAG + restart: unless-stopped + nginx: + image: nginx + restart: unless-stopped + volumes: + - ./proxy/conf.d:/etc/nginx/conf.d + - "\${EB_LOG_BASE_DIR}/nginx:/var/log/nginx" + ports: + - 80:80 + depends_on: + - cms + - client + EOF + + - name: Generate zip file + working-directory: infrastructure/terraform/source_bundle + run: | + zip -r deploy.zip * .[^.]* + + - name: Deploy to Amazon EB + uses: einaregilsson/beanstalk-deploy@v21 + with: + aws_access_key: ${{ secrets.PIPELINE_USER_ACCESS_KEY_ID }} + aws_secret_key: ${{ secrets.PIPELINE_USER_SECRET_ACCESS_KEY }} + application_name: ${{ vars.PROJECT_NAME}}-${{ needs.set_environment.outputs.env_name }} + environment_name: ${{ vars.PROJECT_NAME}}-${{ needs.set_environment.outputs.env_name }}-env + region: ${{ vars.AWS_REGION }} + version_label: ${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }} + deployment_package: infrastructure/terraform/source_bundle/deploy.zip + wait_for_deployment: true diff --git a/client/.env.example b/client/.env.example index 2b0e789a..7fa09bfe 100644 --- a/client/.env.example +++ b/client/.env.example @@ -17,6 +17,10 @@ NEXT_PUBLIC_MAPBOX_API_TOKEN= NEXT_PUBLIC_MAPBOX_USERNAME= NEXT_PUBLIC_MAPBOX_STYLE_ID= +# Matomo Analytics tracking URL and ID +NEXT_PUBLIC_MATOMO_URL= +NEXT_PUBLIC_MATOMO_SITE_ID= + # Google Analytics tracking ID. # Development, Preview, Production # If you're working with an Google Analytics 4 property, you have a Measurement ID instead of a Tracking ID. diff --git a/client/Dockerfile.prod b/client/Dockerfile.prod index ade666e9..d06e6754 100644 --- a/client/Dockerfile.prod +++ b/client/Dockerfile.prod @@ -4,6 +4,31 @@ FROM node:18-alpine AS builder # Optional: Set working directory WORKDIR /app +# Declare build arguments +ARG NEXT_PUBLIC_ENVIRONMENT +ARG NEXT_PUBLIC_URL +ARG NEXT_PUBLIC_API_URL +ARG NEXT_PUBLIC_BASE_PATH +ARG NEXT_PUBLIC_MAPBOX_API_TOKEN +ARG NEXT_PUBLIC_MAPBOX_USERNAME +ARG NEXT_PUBLIC_MAPBOX_STYLE_ID +ARG NEXT_PUBLIC_MATOMO_URL +ARG NEXT_PUBLIC_MATOMO_SITE_ID +ARG NEXT_PUBLIC_PREVIEW_SECRET + +# Set environment variables from build args +ENV NEXT_PUBLIC_ENVIRONMENT=$NEXT_PUBLIC_ENVIRONMENT \ + NEXT_PUBLIC_URL=$NEXT_PUBLIC_URL \ + NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ + NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH \ + NEXT_PUBLIC_MAPBOX_API_TOKEN=$NEXT_PUBLIC_MAPBOX_API_TOKEN \ + NEXT_PUBLIC_MAPBOX_USERNAME=$NEXT_PUBLIC_MAPBOX_USERNAME \ + NEXT_PUBLIC_MAPBOX_STYLE_ID=$NEXT_PUBLIC_MAPBOX_STYLE_ID \ + NEXT_PUBLIC_MATOMO_URL=$NEXT_PUBLIC_MATOMO_URL \ + NEXT_PUBLIC_MATOMO_SITE_ID=$NEXT_PUBLIC_MATOMO_SITE_ID \ + NEXT_PUBLIC_PREVIEW_SECRET=$NEXT_PUBLIC_PREVIEW_SECRET \ + NODE_ENV=production + # Install dependencies COPY .yarn ./.yarn COPY package.json .yarnrc.yml yarn.lock client/package.json ./ @@ -14,7 +39,6 @@ RUN yarn install COPY client . # Build with standalone output -ENV NODE_ENV=production RUN yarn build # ---------- Runner ---------- @@ -23,14 +47,48 @@ FROM node:18-alpine AS runner # Set working dir WORKDIR /app +# Install sharp and dependencies for image optimization +RUN apk add --no-cache \ + libc6-compat \ + && corepack enable \ + && yarn add sharp + +# Declare build arguments for runtime +ARG NEXT_PUBLIC_ENVIRONMENT +ARG NEXT_PUBLIC_URL +ARG NEXT_PUBLIC_API_URL +ARG NEXT_PUBLIC_BASE_PATH +ARG NEXT_PUBLIC_MAPBOX_API_TOKEN +ARG NEXT_PUBLIC_MAPBOX_USERNAME +ARG NEXT_PUBLIC_MAPBOX_STYLE_ID +ARG NEXT_PUBLIC_MATOMO_URL +ARG NEXT_PUBLIC_MATOMO_SITE_ID +ARG NEXT_PUBLIC_PREVIEW_SECRET + +# Set environment variables in runtime +ENV NEXT_PUBLIC_ENVIRONMENT=$NEXT_PUBLIC_ENVIRONMENT \ + NEXT_PUBLIC_URL=$NEXT_PUBLIC_URL \ + NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ + NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH \ + NEXT_PUBLIC_MAPBOX_API_TOKEN=$NEXT_PUBLIC_MAPBOX_API_TOKEN \ + NEXT_PUBLIC_MAPBOX_USERNAME=$NEXT_PUBLIC_MAPBOX_USERNAME \ + NEXT_PUBLIC_MAPBOX_STYLE_ID=$NEXT_PUBLIC_MAPBOX_STYLE_ID \ + NEXT_PUBLIC_MATOMO_URL=$NEXT_PUBLIC_MATOMO_URL \ + NEXT_PUBLIC_MATOMO_SITE_ID=$NEXT_PUBLIC_MATOMO_SITE_ID \ + NEXT_PUBLIC_PREVIEW_SECRET=$NEXT_PUBLIC_PREVIEW_SECRET \ + NODE_ENV=production + # Create user RUN addgroup -g 1001 -S nextjs \ && adduser -S nextjs -u 1001 -G nextjs # Copy only the necessary files -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/public ./public -COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nextjs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nextjs /app/public ./public +COPY --from=builder --chown=nextjs:nextjs /app/.next/static ./.next/static + +# Create cache directory with proper permissions +RUN mkdir -p .next/cache && chown -R nextjs:nextjs .next/cache # Ensure proper permissions USER nextjs diff --git a/client/README.md b/client/README.md index f4da3c4c..8c452a52 100644 --- a/client/README.md +++ b/client/README.md @@ -32,3 +32,30 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. + +### 🔐 GitHub Secrets + +These secrets are required for CI/CD workflows: + +| Name | Description | Example / Notes | +| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| `NEXT_PUBLIC_URL` | Public canonical URL of the deployment. Used for generating absolute links and metadata. | `http://localhost:3000`, `https://esa-gda-comms-staging-mfafc.ondigitalocean.app`, or `https://impact-sphere-gda.esa.int` | +| `NEXT_PUBLIC_ENVIRONMENT` | Defines the current environment (`development`, `staging`, `production`). Used to conditionally enable analytics and other features. | `development` | +| `NEXT_PUBLIC_API_URL` | API endpoint for the CMS (Strapi). Varies by environment. | `http://localhost:1337/api` (local) / `https://impact-sphere-gda.esa.int/cms/impact-sphere/cms/api` (prod) | +| `NEXT_PUBLIC_BASE_PATH` | Base path for assets and routes in staging/production (if deployed under a subdirectory). | `/impact-sphere/` | +| `NEXT_PUBLIC_PREVIEW_SECRET` | Secret token that authorizes preview access to unpublished content from the CMS. | +| `NEXT_PUBLIC_MAPBOX_API_TOKEN` | Mapbox access token used for maps rendering. | Provided by project account (Vizzuality). | +| `NEXT_PUBLIC_MAPBOX_USERNAME` | Mapbox account username used to access and manage project map styles. | +| `NEXT_PUBLIC_MAPBOX_STYLE_ID` | Identifier of the Mapbox style applied to the main map visualization. | +| `NEXT_PUBLIC_MATOMO_URL` | Base URL of the Matomo analytics instance used for tracking. | +| `NEXT_PUBLIC_MATOMO_SITE_ID` | Matomo site ID corresponding to this project’s production instance. | +| `RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED` | Disables duplicate atom key warnings triggered during React hot reload in development. | +| `AWS_ACCESS_KEY_ID` | AWS access key used by CI/CD workflows for deployment. Managed securely in GitHub Secrets. | +| `AWS_SECRET_ACCESS_KEY` | AWS secret key used for authenticated deployment operations. Must remain private and never be committed. | + +> 🧠 **Notes** +> +> - All secrets for **staging** and **production** are configured in **GitHub → Settings → Secrets and variables → Actions**. +> - Local development values belong in a `.env.local` file (never committed). +> - `NEXT_PUBLIC_` variables are exposed to the frontend and should not contain sensitive credentials. +> - Analytics (`Matomo`) are only active when `NEXT_PUBLIC_ENVIRONMENT=production`. diff --git a/client/next.config.mjs b/client/next.config.mjs index 20014113..2e2ba62a 100644 --- a/client/next.config.mjs +++ b/client/next.config.mjs @@ -13,6 +13,7 @@ const nextConfig = { 'fra1.digitaloceanspaces.com', 'esa-gda-comms-staging-mfafc.ondigitalocean.app', 'https://impact-sphere-gda.esa.int', + 'esa-dev-public.s3.amazonaws.com', ], }, env: { diff --git a/client/package.json b/client/package.json index 76922bff..8e001b9d 100644 --- a/client/package.json +++ b/client/package.json @@ -41,6 +41,7 @@ "@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-tooltip": "^1.0.6", "@recoiljs/refine": "^0.1.1", + "@socialgouv/matomo-next": "^1.10.0", "@svgr/webpack": "^8.1.0", "@t3-oss/env-nextjs": "0.4.1", "@tanstack/react-query": "^4.29.18", diff --git a/client/src/app/health/route.ts b/client/src/app/health/route.ts new file mode 100644 index 00000000..68a734c8 --- /dev/null +++ b/client/src/app/health/route.ts @@ -0,0 +1,3 @@ +export async function GET() { + return Response.json({ status: 'ok' }, { status: 200 }); +} diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx index 044921c3..12c1d7cc 100644 --- a/client/src/app/layout.tsx +++ b/client/src/app/layout.tsx @@ -1,14 +1,21 @@ import '@/styles/globals.css'; import '@/styles/mapbox.css'; +import { Suspense } from 'react'; + import Providers from '@/app/layout-providers'; +import { MatomoAnalytics } from '@/containers/matomo-analytics'; + export default function RootLayout({ children }: { children: React.ReactNode }) { return (
{children}
+ + +
diff --git a/client/src/containers/globe/top-stories/index.tsx b/client/src/containers/globe/top-stories/index.tsx index 7639c816..80015407 100644 --- a/client/src/containers/globe/top-stories/index.tsx +++ b/client/src/containers/globe/top-stories/index.tsx @@ -6,7 +6,7 @@ import TopStoriesItem from './item'; const TopStories = () => { const { data: topStories } = useGetTopStories({ - 'pagination[limit]': 10, + // 'pagination[limit]': 10, populate: 'story,cover_image', sort: 'index:asc', }); diff --git a/client/src/containers/matomo-analytics/index.tsx b/client/src/containers/matomo-analytics/index.tsx new file mode 100644 index 00000000..6c6b5bb6 --- /dev/null +++ b/client/src/containers/matomo-analytics/index.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { useEffect } from 'react'; + +import { usePathname, useSearchParams } from 'next/navigation'; + +import { trackAppRouter } from '@socialgouv/matomo-next'; + +import { env } from '@/env.mjs'; + +// https://github.com/SocialGouv/matomo-next + +const MATOMO_URL = env.NEXT_PUBLIC_MATOMO_URL; +const MATOMO_SITE_ID = env.NEXT_PUBLIC_MATOMO_SITE_ID; +const ENVIRONMENT = env.NEXT_PUBLIC_ENVIRONMENT || process.env.NODE_ENV; + +export function MatomoAnalytics() { + const pathname = usePathname(); + const searchParams = useSearchParams(); + + useEffect(() => { + if (ENVIRONMENT !== 'production') { + console.info('Tracking Analytics disabled: non-production environment'); + return; + } + + if (!MATOMO_URL || !MATOMO_SITE_ID) { + return; + } + const urlSearchParams = searchParams + ? new URLSearchParams(Array.from(searchParams.entries())) + : new URLSearchParams(); + + trackAppRouter({ + url: MATOMO_URL!, + siteId: MATOMO_SITE_ID!, + pathname, + searchParams: urlSearchParams, + onInitialization: () => console.info('Initializing Matomo'), + onScriptLoadingError: () => console.error('Error loading Matomo script'), + }); + }, [pathname, searchParams]); + return null; +} diff --git a/client/src/env.mjs b/client/src/env.mjs index 7f86c7ee..de9eb487 100644 --- a/client/src/env.mjs +++ b/client/src/env.mjs @@ -33,6 +33,8 @@ export const env = createEnv({ NEXT_PUBLIC_PREVIEW_SECRET: z.string().optional(), NEXT_PUBLIC_MAPBOX_USERNAME: z.string().optional(), NEXT_PUBLIC_MAPBOX_STYLE_ID: z.string().optional(), + NEXT_PUBLIC_MATOMO_URL: z.string().url(), + NEXT_PUBLIC_MATOMO_SITE_ID: z.string(), }, /* * Due to how Next.js bundles environment variables on Edge and Client, @@ -45,6 +47,8 @@ export const env = createEnv({ NEXT_PUBLIC_URL: process.env.NEXT_PUBLIC_URL, NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, NEXT_PUBLIC_MAPBOX_API_TOKEN: process.env.NEXT_PUBLIC_MAPBOX_API_TOKEN, + NEXT_PUBLIC_MATOMO_URL: process.env.NEXT_PUBLIC_MATOMO_URL, + NEXT_PUBLIC_MATOMO_SITE_ID: process.env.NEXT_PUBLIC_MATOMO_SITE_ID, NEXT_PUBLIC_GA_TRACKING_ID: process.env.NEXT_PUBLIC_GA_TRACKING_ID, RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED: process.env.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED, diff --git a/cms/Dockerfile.prod b/cms/Dockerfile.prod index 90bf1600..3b935ed0 100644 --- a/cms/Dockerfile.prod +++ b/cms/Dockerfile.prod @@ -12,7 +12,75 @@ RUN apt-get update -y && \ libvips-dev \ && apt-get clean -ENV NODE_ENV production +# Declare build arguments +ARG NODE_ENV +ARG CMS_URL +ARG DATABASE_CLIENT +ARG DATABASE_SSL +ARG DATABASE_SSL_REJECT_UNAUTHORIZED +ARG AWS_S3_BUCKET +ARG AWS_S3_REGION +ARG AWS_S3_BUCKET_URL +ARG STRAPI_ADMIN_MAPBOX_ACCESS_TOKEN +ARG STRAPI_ADMIN_MAPBOX_USERNAME +ARG STRAPI_ADMIN_MAPBOX_STYLE_ID +ARG STRAPI_ADMIN_API_BASE_URL +ARG DATABASE_URL +ARG DATABASE_HOST +ARG DATABASE_NAME +ARG DATABASE_PASSWORD +ARG DATABASE_USERNAME +ARG DATABASE_PORT +ARG AWS_SES_ACCESS_KEY_ID +ARG AWS_SES_ACCESS_KEY_SECRET +ARG AWS_SES_DOMAIN +ARG ADMIN_JWT_SECRET +ARG API_TOKEN_SALT +ARG JWT_SECRET +ARG TRANSFER_TOKEN_SALT +ARG APP_KEYS +ARG PORT +ARG BUCKET_BUCKET +ARG BUCKET_REGION +ARG BUCKET_ENDPOINT +ARG BUCKET_ACCESS_KEY +ARG BUCKET_SECRET_KEY +ARG STRAPI_MEDIA_LIBRARY_PROVIDER + +# Set environment variables from build args +ENV NODE_ENV=$NODE_ENV \ + CMS_URL=$CMS_URL \ + DATABASE_CLIENT=$DATABASE_CLIENT \ + DATABASE_SSL=$DATABASE_SSL \ + DATABASE_SSL_REJECT_UNAUTHORIZED=$DATABASE_SSL_REJECT_UNAUTHORIZED \ + AWS_S3_BUCKET=$AWS_S3_BUCKET \ + AWS_S3_REGION=$AWS_S3_REGION \ + AWS_S3_BUCKET_URL=$AWS_S3_BUCKET_URL \ + STRAPI_ADMIN_MAPBOX_ACCESS_TOKEN=$STRAPI_ADMIN_MAPBOX_ACCESS_TOKEN \ + STRAPI_ADMIN_MAPBOX_USERNAME=$STRAPI_ADMIN_MAPBOX_USERNAME \ + STRAPI_ADMIN_MAPBOX_STYLE_ID=$STRAPI_ADMIN_MAPBOX_STYLE_ID \ + STRAPI_ADMIN_API_BASE_URL=$STRAPI_ADMIN_API_BASE_URL \ + DATABASE_URL=$DATABASE_URL \ + DATABASE_HOST=$DATABASE_HOST \ + DATABASE_NAME=$DATABASE_NAME \ + DATABASE_PASSWORD=$DATABASE_PASSWORD \ + DATABASE_USERNAME=$DATABASE_USERNAME \ + DATABASE_PORT=$DATABASE_PORT \ + AWS_SES_ACCESS_KEY_ID=$AWS_SES_ACCESS_KEY_ID \ + AWS_SES_ACCESS_KEY_SECRET=$AWS_SES_ACCESS_KEY_SECRET \ + AWS_SES_DOMAIN=$AWS_SES_DOMAIN \ + ADMIN_JWT_SECRET=$ADMIN_JWT_SECRET \ + API_TOKEN_SALT=$API_TOKEN_SALT \ + JWT_SECRET=$JWT_SECRET \ + TRANSFER_TOKEN_SALT=$TRANSFER_TOKEN_SALT \ + APP_KEYS=$APP_KEYS \ + PORT=$PORT \ + BUCKET_BUCKET=$BUCKET_BUCKET \ + BUCKET_REGION=$BUCKET_REGION \ + BUCKET_ENDPOINT=$BUCKET_ENDPOINT \ + BUCKET_ACCESS_KEY=$BUCKET_ACCESS_KEY \ + BUCKET_SECRET_KEY=$BUCKET_SECRET_KEY \ + STRAPI_MEDIA_LIBRARY_PROVIDER=$STRAPI_MEDIA_LIBRARY_PROVIDER WORKDIR /app COPY .yarn ./.yarn @@ -30,10 +98,81 @@ RUN yarn build FROM node:18.16-bookworm-slim AS runner RUN apt-get update -y && \ apt-get upgrade -y && \ - apt-get install -y libvips-dev && \ + apt-get install -y libvips-dev ca-certificates curl && \ apt-get clean -ENV NODE_ENV production +# Download RDS certificate bundle +RUN curl -o /etc/ssl/certs/rds-global-bundle.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem + +# Declare build arguments for runtime +ARG NODE_ENV +ARG CMS_URL +ARG DATABASE_CLIENT +ARG DATABASE_SSL +ARG DATABASE_SSL_REJECT_UNAUTHORIZED +ARG AWS_S3_BUCKET +ARG AWS_S3_REGION +ARG AWS_S3_BUCKET_URL +ARG STRAPI_ADMIN_MAPBOX_ACCESS_TOKEN +ARG STRAPI_ADMIN_MAPBOX_USERNAME +ARG STRAPI_ADMIN_MAPBOX_STYLE_ID +ARG STRAPI_ADMIN_API_BASE_URL +ARG DATABASE_URL +ARG DATABASE_HOST +ARG DATABASE_NAME +ARG DATABASE_PASSWORD +ARG DATABASE_USERNAME +ARG DATABASE_PORT +ARG AWS_SES_ACCESS_KEY_ID +ARG AWS_SES_ACCESS_KEY_SECRET +ARG AWS_SES_DOMAIN +ARG ADMIN_JWT_SECRET +ARG API_TOKEN_SALT +ARG JWT_SECRET +ARG TRANSFER_TOKEN_SALT +ARG APP_KEYS +ARG PORT +ARG BUCKET_BUCKET +ARG BUCKET_REGION +ARG BUCKET_ENDPOINT +ARG BUCKET_ACCESS_KEY +ARG BUCKET_SECRET_KEY +ARG STRAPI_MEDIA_LIBRARY_PROVIDER + +# Set environment variables in runtime +ENV NODE_ENV=$NODE_ENV \ + CMS_URL=$CMS_URL \ + DATABASE_CLIENT=$DATABASE_CLIENT \ + DATABASE_SSL=$DATABASE_SSL \ + DATABASE_SSL_REJECT_UNAUTHORIZED=$DATABASE_SSL_REJECT_UNAUTHORIZED \ + AWS_S3_BUCKET=$AWS_S3_BUCKET \ + AWS_S3_REGION=$AWS_S3_REGION \ + AWS_S3_BUCKET_URL=$AWS_S3_BUCKET_URL \ + STRAPI_ADMIN_MAPBOX_ACCESS_TOKEN=$STRAPI_ADMIN_MAPBOX_ACCESS_TOKEN \ + STRAPI_ADMIN_MAPBOX_USERNAME=$STRAPI_ADMIN_MAPBOX_USERNAME \ + STRAPI_ADMIN_MAPBOX_STYLE_ID=$STRAPI_ADMIN_MAPBOX_STYLE_ID \ + STRAPI_ADMIN_API_BASE_URL=$STRAPI_ADMIN_API_BASE_URL \ + DATABASE_URL=$DATABASE_URL \ + DATABASE_HOST=$DATABASE_HOST \ + DATABASE_NAME=$DATABASE_NAME \ + DATABASE_PASSWORD=$DATABASE_PASSWORD \ + DATABASE_USERNAME=$DATABASE_USERNAME \ + DATABASE_PORT=$DATABASE_PORT \ + AWS_SES_ACCESS_KEY_ID=$AWS_SES_ACCESS_KEY_ID \ + AWS_SES_ACCESS_KEY_SECRET=$AWS_SES_ACCESS_KEY_SECRET \ + AWS_SES_DOMAIN=$AWS_SES_DOMAIN \ + ADMIN_JWT_SECRET=$ADMIN_JWT_SECRET \ + API_TOKEN_SALT=$API_TOKEN_SALT \ + JWT_SECRET=$JWT_SECRET \ + TRANSFER_TOKEN_SALT=$TRANSFER_TOKEN_SALT \ + APP_KEYS=$APP_KEYS \ + PORT=$PORT \ + BUCKET_BUCKET=$BUCKET_BUCKET \ + BUCKET_REGION=$BUCKET_REGION \ + BUCKET_ENDPOINT=$BUCKET_ENDPOINT \ + BUCKET_ACCESS_KEY=$BUCKET_ACCESS_KEY \ + BUCKET_SECRET_KEY=$BUCKET_SECRET_KEY \ + STRAPI_MEDIA_LIBRARY_PROVIDER=$STRAPI_MEDIA_LIBRARY_PROVIDER WORKDIR /app diff --git a/cms/config/middlewares.ts b/cms/config/middlewares.ts index bd866e53..11fcf7e9 100644 --- a/cms/config/middlewares.ts +++ b/cms/config/middlewares.ts @@ -8,12 +8,37 @@ export default ({ env }) => [ directives: { 'connect-src': ["'self'", 'https:'], 'script-src': ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'api.mapbox.com'], - 'img-src': ["'self'", 'data:', 'blob:', `*.${env('BUCKET_REGION')}.digitaloceanspaces.com`, `${env('BUCKET_REGION')}.digitaloceanspaces.com`, 'api.mapbox.com'], - 'media-src': ["'self'", 'data:', 'blob:', `*.${env('BUCKET_REGION')}.digitaloceanspaces.com`, `${env('BUCKET_REGION')}.digitaloceanspaces.com`], + 'img-src': [ + "'self'", + 'data:', + 'blob:', + // Support both DigitalOcean Spaces (for legacy/staging) and AWS S3 + `*.${env('BUCKET_REGION')}.digitaloceanspaces.com`, + `${env('BUCKET_REGION')}.digitaloceanspaces.com`, + // AWS S3 - both regional and legacy global URL formats + `*.s3.${env('BUCKET_REGION', 'eu-west-3')}.amazonaws.com`, + `s3.${env('BUCKET_REGION', 'eu-west-3')}.amazonaws.com`, + '*.s3.amazonaws.com', // Legacy virtual-hosted-style URL + 's3.amazonaws.com', // Legacy path-style URL + 'api.mapbox.com', + ], + 'media-src': [ + "'self'", + 'data:', + 'blob:', + // Support both DigitalOcean Spaces (for legacy/staging) and AWS S3 + `*.${env('BUCKET_REGION')}.digitaloceanspaces.com`, + `${env('BUCKET_REGION')}.digitaloceanspaces.com`, + // AWS S3 - both regional and legacy global URL formats + `*.s3.${env('BUCKET_REGION', 'eu-west-3')}.amazonaws.com`, + `s3.${env('BUCKET_REGION', 'eu-west-3')}.amazonaws.com`, + '*.s3.amazonaws.com', // Legacy virtual-hosted-style URL + 's3.amazonaws.com', // Legacy path-style URL + ], 'worker-src': ['blob:'], upgradeInsecureRequests: null, }, - } + }, }, }, 'strapi::cors', @@ -21,11 +46,11 @@ export default ({ env }) => [ 'strapi::logger', 'strapi::query', { - name: "strapi::body", + name: 'strapi::body', config: { - formLimit: "256mb", // modify form body - jsonLimit: "256mb", // modify JSON body - textLimit: "256mb", // modify text body + formLimit: '256mb', // modify form body + jsonLimit: '256mb', // modify JSON body + textLimit: '256mb', // modify text body formidable: { maxFileSize: 300 * 1024 * 1024, // multipart data, modify here limit of uploaded file size }, diff --git a/cms/config/plugins.ts b/cms/config/plugins.ts index f34daf1b..82616b4f 100644 --- a/cms/config/plugins.ts +++ b/cms/config/plugins.ts @@ -1,28 +1,29 @@ - module.exports = ({ env }) => ({ documentation: { config: { - "x-strapi-config": { + 'x-strapi-config': { mutateDocumentation: (generatedDocumentationDraft) => { - console.log("generatedDocumentationDraft", generatedDocumentationDraft); + console.log('generatedDocumentationDraft', generatedDocumentationDraft); Object.keys(generatedDocumentationDraft.paths).forEach((path) => { // check if it has {id} in the path - if (path.includes("{id}")) { + if (path.includes('{id}')) { // add `populate` as params if (generatedDocumentationDraft.paths[path].get) { - if (!generatedDocumentationDraft.paths[path].get.parameters.find((param) => param.name === "populate")) { - generatedDocumentationDraft.paths[path].get.parameters.push( - { - "name": "populate", - "in": "query", - "description": "Relations to return", - "deprecated": false, - "required": false, - "schema": { - "type": "string" - } + if ( + !generatedDocumentationDraft.paths[path].get.parameters.find( + (param) => param.name === 'populate' + ) + ) { + generatedDocumentationDraft.paths[path].get.parameters.push({ + name: 'populate', + in: 'query', + description: 'Relations to return', + deprecated: false, + required: false, + schema: { + type: 'string', }, - ); + }); } } } @@ -32,25 +33,32 @@ module.exports = ({ env }) => ({ 'strapi-plugin-populate-deep': { config: { defaultDepth: 5, // Default is 5 - } + }, }, }, }, 'map-field': { enabled: true, - resolve: './src/plugins/map-field' + resolve: './src/plugins/map-field', }, - ...(env('STRAPI_MEDIA_LIBRARY_PROVIDER') === 'digitalocean' && { + // S3-compatible upload configuration (supports both AWS S3 and DigitalOcean Spaces) + ...((env('STRAPI_MEDIA_LIBRARY_PROVIDER') === 'digitalocean' || + env('STRAPI_MEDIA_LIBRARY_PROVIDER') === 'aws-s3') && { upload: { config: { - provider: "aws-s3", + provider: 'aws-s3', providerOptions: { - accessKeyId: env("BUCKET_ACCESS_KEY"), - secretAccessKey: env("BUCKET_SECRET_KEY"), - endpoint: env("BUCKET_ENDPOINT"), - region: env("BUCKET_REGION"), - params: { - Bucket: env("BUCKET_BUCKET") + s3Options: { + credentials: { + accessKeyId: env('BUCKET_ACCESS_KEY'), + secretAccessKey: env('BUCKET_SECRET_KEY'), + }, + region: env('BUCKET_REGION'), + ...(env('BUCKET_ENDPOINT') && { endpoint: env('BUCKET_ENDPOINT') }), + params: { + Bucket: env('BUCKET_BUCKET'), + ACL: null, // Disable ACLs - bucket uses BucketOwnerEnforced ownership + }, }, }, }, @@ -65,7 +73,7 @@ module.exports = ({ env }) => ({ url: `${env('STRAPI_ADMIN_PREVIEW_URL')}/api/preview`, query: { secret: env('STRAPI_ADMIN_PREVIEW_SECRET'), - slug: '{id}' + slug: '{id}', }, }, published: { @@ -76,4 +84,3 @@ module.exports = ({ env }) => ({ }, }, }); - diff --git a/infrastructure/.gitignore b/infrastructure/.gitignore deleted file mode 100644 index fdd52eb4..00000000 --- a/infrastructure/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -base/.terraform -base/vars/* -!base/vars/terraform.tfvars - -state/.terraform -state/vars/* -!state/vars/terraform.tfvars - -state/terraform.tfstate -state/terraform.tfstate.backup - diff --git a/infrastructure/README.md b/infrastructure/README.md deleted file mode 100644 index 56768b09..00000000 --- a/infrastructure/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Architecture - -The project is composed of the following components: -- cms: a headless Strapi CMS that provides a REST API to manage the content of the website -- client: a Next.js application that consumes the CMS API and renders the website - -Both are deployed on DigitalOcean using the following services: -- Container Registry: to store the Docker images -- Spaces: to store the terraform state and optionaly the CMS media files -- Managed Database: to host the database -- Platform Apps: to host the Docker images of the CMS and the client - -# Deployment - -The deployment is automated using a GH Action that builds the Docker images and deploys them to DigitalOcean Platform App. - -## Requirements - -The following secrets must be defined in the GH repository: -- `DIGITALOCEAN_ACCESS_TOKEN`: the DigitalOcean API token which allows GH to deploy the images to the Container Registry and update Platform Apps - -# Infrastructure as Code - -The resources required to deploy the solution are defined in the `infrastructure` folder. The infrastructure is defined using Terraform. - -There are two Terraform projects in the `infrastructure` folder: -- state: to store the Terraform remote state in an Spaces bucket -- base: to deploy the infrastructure, using the remote state stored in the Spaces bucket - -The `state` project must be deployed first, and then the `base` project can be deployed. - -## Requirements - -All terraform variables needs to be properly setup including the following: - - `do_token`: DigitalOcean API token - - `do_spaces_client_id`: DigitalOcean Spaces key - - `do_spaces_secret_key`: DigitalOcean Spaces secret - -`GITHUB_TOKEN` and `GITHUB_OWNER` must be defined in the environment when Terraform code is applied to allow it to update secrets in the GH repository. diff --git a/infrastructure/base/.terraform.lock.hcl b/infrastructure/base/.terraform.lock.hcl deleted file mode 100644 index f6cfd983..00000000 --- a/infrastructure/base/.terraform.lock.hcl +++ /dev/null @@ -1,66 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/digitalocean/digitalocean" { - version = "2.30.0" - constraints = "~> 2.0, ~> 2.30" - hashes = [ - "h1:2IS8Ng87PDjkNzTfIF6RzyHQywNOBO8iYwDOiAxk8IM=", - "zh:0a6f0d7ac89c6f1835df9f8dc4464eb42893449a4d4f3a9e832472a3e4184c03", - "zh:299b78108f01f3ddcc35424ed20b79a810610612e3ee13958d7cd45a16d53628", - "zh:2dfcc11c79f058f76aa0545f842dede804cdc1cd40f48c3573312b7f93882923", - "zh:33271b4d8c75cef65b7f3d2bc3afbaa412849cea49593715806b25a9ffc8d03e", - "zh:448de7e2d46c4619cb98921c70b6a35a91256329457d27023a813c01635dfe65", - "zh:53bc104ae2bacbcaa5b0a4ce2caf06c1ac942c6114f2b5a12869d020c7580cc6", - "zh:54ee5aafe43b12b87901036d13fc399f511d4f5f6fef784a07a695bdeed300f1", - "zh:563ac07ff6d3379d23749e930007179a63e3b13317b214db5b8faf43fef21aea", - "zh:8f6a53f53b880f20e1f3953727c91888bf06aad5ba28dba9a69621b042cf2eb0", - "zh:9089f77da041e64e112e3efe2c013d7cb032362544724a672579919471a78571", - "zh:951fa4e16d05bb3e717a1f3ac0b91487eb554088fc0f96e188586e729a925d3b", - "zh:a287a3fc416a3e8b4794ed89bd24978b5d53ae110091ab7986c609a9e048c847", - "zh:b09ee82f32c819c477117b7692888e7d4b5a403316c3bab3bd55bae1133438b1", - "zh:c00fccb3699abc6277eb7750b0b85d8cd1f0a0f84c41d388e90ef039a830a5ca", - "zh:ce95cfc5e67276f8ded53cf8a5872720f17d1b0a1cbdef844a773d302524adef", - "zh:deb5add87e3040d18bfc111ec82cf61c3ebc1ebea1d594562952058ae061970c", - ] -} - -provider "registry.terraform.io/hashicorp/github" { - version = "5.37.0" - hashes = [ - "h1:uPmf3/0IVrCHUx55PltKKFDa2RnZtIfOaHid7LMXACU=", - "zh:0dfc44b954d02165330080836ae73dd47490ac5c4c1655bf59ab7dea3142617a", - "zh:164dde511b92d7568df38302d5300d127fd63646983f161fdf614c3e7b7bfff4", - "zh:394234dce2ffd3b7c4dd1a2bc74f4c67c0d972502c962c5320ab19860f99b3d7", - "zh:3a828099a1b9910555d1977290ca72fbc9bd217d1ba8956fe1889c712d2b63ad", - "zh:3f00d738069da7c85305eba4ab1633bdc3a3b3453d16cdccfa759c6ec2595a99", - "zh:548b7dadc86ba6b7eae6fe5b8498995fa9f2e7b647ca02edcae55d158abae049", - "zh:598424868f0d26974dcaf1c37abbaab9ad42c3517f3c2c072f14ebb92ae35aea", - "zh:9747adf559fe826ae94d205a64670f2fe243342e3d30354f739dfc7f29cad170", - "zh:9ebfb1cd63571a9ac8c9bc61eae43fa79ca2fbdc9c87f8374e86be412af7218a", - "zh:9ec05a279bb2b71859e67476eca83c24e7840ee52d8e585935b97d5b9588ea22", - "zh:c05b9e5c0581ef837f56761e96295e2b9baeb6749e47feccd85d55afcaac7cb9", - "zh:c094294a907558e03811ef8ff2711845a6aea6d740df3c1dc4b4cb5bf414dd62", - "zh:f1370689625826553ba5dfc2733c4809f90a6a89158218b5a345210a920a62f1", - "zh:fdfd5bdcfb028df3985978a7fda6541945e89285a02ef39288a2506fbb01edc2", - ] -} - -provider "registry.terraform.io/hashicorp/random" { - version = "3.5.1" - hashes = [ - "h1:IL9mSatmwov+e0+++YX2V6uel+dV6bn+fC/cnGDK3Ck=", - "zh:04e3fbd610cb52c1017d282531364b9c53ef72b6bc533acb2a90671957324a64", - "zh:119197103301ebaf7efb91df8f0b6e0dd31e6ff943d231af35ee1831c599188d", - "zh:4d2b219d09abf3b1bb4df93d399ed156cadd61f44ad3baf5cf2954df2fba0831", - "zh:6130bdde527587bbe2dcaa7150363e96dbc5250ea20154176d82bc69df5d4ce3", - "zh:6cc326cd4000f724d3086ee05587e7710f032f94fc9af35e96a386a1c6f2214f", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:b6d88e1d28cf2dfa24e9fdcc3efc77adcdc1c3c3b5c7ce503a423efbdd6de57b", - "zh:ba74c592622ecbcef9dc2a4d81ed321c4e44cddf7da799faa324da9bf52a22b2", - "zh:c7c5cde98fe4ef1143bd1b3ec5dc04baf0d4cc3ca2c5c7d40d17c0e9b2076865", - "zh:dac4bad52c940cd0dfc27893507c1e92393846b024c5a9db159a93c534a3da03", - "zh:de8febe2a2acd9ac454b844a4106ed295ae9520ef54dc8ed2faf29f12716b602", - "zh:eab0d0495e7e711cca367f7d4df6e322e6c562fc52151ec931176115b83ed014", - ] -} diff --git a/infrastructure/base/main.tf b/infrastructure/base/main.tf deleted file mode 100644 index f0184103..00000000 --- a/infrastructure/base/main.tf +++ /dev/null @@ -1,131 +0,0 @@ -# DO allows to have only one container registry per account -# skip this action if registry already exists -module "container_registry" { - source = "./modules/container_registry" - - registry_name = var.container_registry_name - do_region = var.do_region -} - -resource "random_password" "api_token_salt" { - length = 32 - special = true - override_special = "!#$%&*()-_=+[]{}<>:?" -} - -resource "random_password" "admin_jwt_secret" { - length = 32 - special = true - override_special = "!#$%&*()-_=+[]{}<>:?" -} - -resource "random_password" "transfer_token_salt" { - length = 32 - special = true - override_special = "!#$%&*()-_=+[]{}<>:?" -} - -resource "random_password" "jwt_secret" { - length = 32 - special = true - override_special = "!#$%&*()-_=+[]{}<>:?" -} - -resource "random_password" "preview_secret" { - length = 12 - special = true - override_special = "!#$%&*()-_=+[]{}<>:?" -} - -locals { - staging_cms_env = { - HOST = "0.0.0.0" - PORT = 1337 - APP_KEYS = "toBeModified1,toBeModified2" - API_TOKEN_SALT = random_password.api_token_salt.result - ADMIN_JWT_SECRET = random_password.admin_jwt_secret.result - TRANSFER_TOKEN_SALT = random_password.transfer_token_salt.result - JWT_SECRET = random_password.jwt_secret.result - CMS_URL = "${module.staging.app_url}/impact-sphere/cms" - STRAPI_ADMIN_API_BASE_URL = "${module.staging.app_url}/impact-sphere/cms/api" - STRAPI_ADMIN_MAPBOX_ACCESS_TOKEN = var.mapbox_api_token - STRAPI_ADMIN_PREVIEW_URL = "${module.staging.app_url}/impact-sphere" - STRAPI_ADMIN_PREVIEW_SECRET = random_password.preview_secret.result - STRAPI_MEDIA_LIBRARY_PROVIDER = "digitalocean" - STRAPI_ADMIN_MAPBOX_USERNAME = var.mapbox_username - STRAPI_ADMIN_MAPBOX_STYLE_ID = var.mapbox_style_id - - # DigitalOcean Spaces to store media content - BUCKET_ACCESS_KEY = var.do_spaces_client_id - BUCKET_SECRET_KEY = var.do_spaces_secret_key - BUCKET_REGION = var.do_region - BUCKET_BUCKET = "${var.project_name}-staging-cms" - BUCKET_ENDPOINT = "https://${var.do_region}.digitaloceanspaces.com" - - # DigitalOcean Spaces to store map data - BUCKET_MAP_DATA_ENDPOINT = "https://${var.project_name}-staging.${var.do_region}.cdn.digitaloceanspaces.com" - - # Database - DATABASE_CLIENT = "postgres" - DATABASE_HOST = module.staging.postgresql_host - DATABASE_PORT = module.staging.postgresql_port - DATABASE_NAME = var.postgres_db_name - DATABASE_USERNAME = module.staging.postgresql_username - DATABASE_PASSWORD = module.staging.postgresql_password - DATABASE_SSL = true - DATABASE_SSL_REJECT_UNAUTHORIZED = false - - } - staging_client_env = { - NEXT_PUBLIC_URL = "${module.staging.app_url}/impact-sphere" - NEXT_PUBLIC_BASE_PATH = "/impact-sphere" - NEXT_PUBLIC_ENVIRONMENT = "production" - NEXT_PUBLIC_API_URL = "${module.staging.app_url}/impact-sphere/cms/api" - NEXT_PUBLIC_GA_TRACKING_ID = var.ga_tracking_id - NEXT_PUBLIC_MAPBOX_API_TOKEN = var.mapbox_api_token - NEXT_PUBLIC_PREVIEW_SECRET = random_password.preview_secret.result - NEXT_PUBLIC_MAPBOX_USERNAME = var.mapbox_username - NEXT_PUBLIC_MAPBOX_STYLE_ID = var.mapbox_style_id - LOG_LEVEL = "info" - RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED = false - } -} - -module "github_values" { - source = "./modules/github_values" - repo_name = var.repo_name - secret_map = { - STAGING_CMS_ENV_FILE = join("\n", [for key, value in local.staging_cms_env : "${key}=${value}"]) - STAGING_CLIENT_ENV_FILE = join("\n", [for key, value in local.staging_client_env : "${key}=${value}"]) - } -} - -# can be run only after container registry is created and it contains docker images -# provide correct docker image tag during creation of app -# docker images should be automatically build and pushed to docker registry by CI/CD -module "staging" { - source = "./modules/env" - do_project_name = var.do_project_name - project_name = var.project_name - environment = "staging" - do_region = var.do_region - postgres_size = var.postgres_size - postgres_db_name = var.postgres_db_name - do_app_instance = var.do_app_instance - do_app_instance_count = var.do_app_instance_count - do_app_image_tag = var.do_app_image_tag - do_space_name = "${var.project_name}-staging" - do_cms_space_name = "${var.project_name}-staging-cms" -} - -resource "digitalocean_spaces_bucket_cors_configuration" "staging_cors" { - bucket = module.staging.space_id - region = var.do_region - - cors_rule { - allowed_headers = ["*"] - allowed_methods = ["GET", "HEAD"] - allowed_origins = ["*"] - max_age_seconds = 3000 - } -} diff --git a/infrastructure/base/modules/app/main.tf b/infrastructure/base/modules/app/main.tf deleted file mode 100644 index e6bd7203..00000000 --- a/infrastructure/base/modules/app/main.tf +++ /dev/null @@ -1,81 +0,0 @@ -terraform { - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = "~> 2.0" - } - } -} - -data "digitalocean_project" "project" { - name = var.do_project_name -} - -resource "digitalocean_app" "app" { - spec { - name = "${var.project_name}-${var.environment}" - region = var.do_region - - alert { - rule = "DEPLOYMENT_FAILED" - } - alert { - rule = "DOMAIN_FAILED" - } - - service { - name = "${var.project_name}-${var.environment}-client" - http_port = 8080 - instance_count = var.do_app_instance_count - instance_size_slug = var.do_app_instance - - ##### FIRST RUN ##### - ##### Uncomment the git block and comment image block if container repository does not have an image yet ##### -# git { -# branch = "main" -# repo_clone_url = "https://github.com/digitalocean/sample-dockerfile.git" -# } - - image { - registry_type = "DOCR" - repository = "${var.project_name}-${var.environment}-client" - tag = var.do_app_image_tag - } - - routes { - path = "/impact-sphere" - preserve_path_prefix = true - } - } - - service { - name = "${var.project_name}-${var.environment}-cms" - http_port = 8081 - instance_count = var.do_app_instance_count - instance_size_slug = var.do_app_instance - - ##### FIRST RUN ##### - ##### Uncomment the git block and comment image block if container repository does not have an image yet ##### -# git { -# branch = "main" -# repo_clone_url = "https://github.com/digitalocean/sample-dockerfile.git" -# } - - image { - registry_type = "DOCR" - repository = "${var.project_name}-${var.environment}-cms" - tag = var.do_app_image_tag - } - - routes { - path = "/impact-sphere/cms" - preserve_path_prefix = false - } - } - } -} - -resource "digitalocean_project_resources" "app" { - project = data.digitalocean_project.project.id - resources = [digitalocean_app.app.urn] -} diff --git a/infrastructure/base/modules/app/outputs.tf b/infrastructure/base/modules/app/outputs.tf deleted file mode 100644 index 00703fd5..00000000 --- a/infrastructure/base/modules/app/outputs.tf +++ /dev/null @@ -1,11 +0,0 @@ -output "app_id" { - value = digitalocean_app.app.id -} - -output "url" { - value = digitalocean_app.app.live_url -} - -output "domain" { - value = digitalocean_app.app.spec.0.domain -} diff --git a/infrastructure/base/modules/app/variables.tf b/infrastructure/base/modules/app/variables.tf deleted file mode 100644 index b9625341..00000000 --- a/infrastructure/base/modules/app/variables.tf +++ /dev/null @@ -1,36 +0,0 @@ -variable "do_region" { - type = string - description = "DigitalOcean Region" -} - -variable "do_project_name" { - type = string - description = "Project name at DO" -} - -variable "project_name" { - type = string - description = "Short name of the project, will be used to prefix created resources" -} - -variable "environment" { - type = string - description = "Name of the environment, will be used to prefix created resources" -} - -variable "do_app_instance" { - type = string - description = "DigitalOcean Droplet size" - default = "basic-xs" -} - -variable "do_app_instance_count" { - type = number - description = "Number of instances to create" - default = 1 -} - -variable "do_app_image_tag" { - type = string - description = "Tag of image from DO container registry" -} diff --git a/infrastructure/base/modules/container_registry/main.tf b/infrastructure/base/modules/container_registry/main.tf deleted file mode 100644 index 7a29d032..00000000 --- a/infrastructure/base/modules/container_registry/main.tf +++ /dev/null @@ -1,19 +0,0 @@ -## -# Module to build the DO container registry -# DO allows to have only one registry per account!!! If your account already have container registry, skip this action -## - -terraform { - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = "~> 2.0" - } - } -} - -resource "digitalocean_container_registry" "container_registry" { - name = var.registry_name - subscription_tier_slug = "basic" - region = var.do_region -} diff --git a/infrastructure/base/modules/container_registry/outputs.tf b/infrastructure/base/modules/container_registry/outputs.tf deleted file mode 100644 index d26a0c78..00000000 --- a/infrastructure/base/modules/container_registry/outputs.tf +++ /dev/null @@ -1,3 +0,0 @@ -output "container_registry_endpoint" { - value = digitalocean_container_registry.container_registry.endpoint -} diff --git a/infrastructure/base/modules/container_registry/variables.tf b/infrastructure/base/modules/container_registry/variables.tf deleted file mode 100644 index ae26835c..00000000 --- a/infrastructure/base/modules/container_registry/variables.tf +++ /dev/null @@ -1,9 +0,0 @@ -variable "do_region" { - type = string - description = "DigitalOcean Region" -} - -variable "registry_name" { - type = string - description = "Name of DO container registry" -} diff --git a/infrastructure/base/modules/env/main.tf b/infrastructure/base/modules/env/main.tf deleted file mode 100644 index 445c3276..00000000 --- a/infrastructure/base/modules/env/main.tf +++ /dev/null @@ -1,65 +0,0 @@ -terraform { - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = "~> 2.0" - } - } -} - -module "postgresql" { - source = "../postgresql" - - do_project_name = var.do_project_name - project_name = var.project_name - environment = var.environment - do_region = var.do_region - postgres_size = var.postgres_size - postgres_db_name = var.postgres_db_name - app_id = module.app.app_id -} - -module "space" { - source = "../space" - - do_project_name = var.do_project_name - do_region = var.do_region - do_space_name = var.do_space_name -} - -module "space_cms" { - source = "../space" - - do_project_name = var.do_project_name - do_region = var.do_region - do_space_name = var.do_cms_space_name -} - -module "app" { - source = "../app" - - do_project_name = var.do_project_name - project_name = var.project_name - environment = var.environment - do_region = var.do_region - do_app_instance = var.do_app_instance - do_app_instance_count = var.do_app_instance_count - do_app_image_tag = var.do_app_image_tag -} - -resource "digitalocean_cdn" "space_cdn" { - origin = module.space.bucket_domain_name -} - -resource "digitalocean_spaces_bucket_cors_configuration" "space_cms_cors" { - bucket = module.space_cms.space_id - region = var.do_region - - cors_rule { - allowed_headers = ["*"] - allowed_methods = ["GET", "HEAD", "PUT", "POST", "DELETE"] - allowed_origins = ["*"] - expose_headers = ["ETag", "Access-Control-Allow-Origin"] - max_age_seconds = 3000 - } -} diff --git a/infrastructure/base/modules/env/outputs.tf b/infrastructure/base/modules/env/outputs.tf deleted file mode 100644 index 4936f584..00000000 --- a/infrastructure/base/modules/env/outputs.tf +++ /dev/null @@ -1,27 +0,0 @@ -output "postgresql_host" { - value = module.postgresql.host -} - -output "postgresql_port" { - value = module.postgresql.port -} - -output "postgresql_username" { - value = module.postgresql.user -} - -output "postgresql_password" { - value = module.postgresql.password -} - -output "app_url" { - value = module.app.url -} - -output "app_domain" { - value = module.app.domain -} - -output "space_id" { - value = module.space.space_id -} diff --git a/infrastructure/base/modules/env/variables.tf b/infrastructure/base/modules/env/variables.tf deleted file mode 100644 index c5a6ccc5..00000000 --- a/infrastructure/base/modules/env/variables.tf +++ /dev/null @@ -1,63 +0,0 @@ -variable "do_region" { - type = string - description = "DigitalOcean Region" -} - -variable "do_project_name" { - type = string - description = "Project name at DO" -} - -variable "project_name" { - type = string - description = "Short name of the project, will be used to prefix created resources" -} - -variable "environment" { - type = string - description = "Name of the environment, will be used to prefix created resources" -} - -variable "do_app_instance" { - type = string - description = "DigitalOcean Droplet size" - default = "basic-xs" -} - -variable "postgres_size" { - type = string - description = "DigitalOcean PostgreSQL size" - default = "db-s-1vcpu-1gb" -} - -variable "postgres_db_name" { - type = string - description = "Name of PostgreSQL database" -} - -variable "do_app_instance_count" { - type = number - description = "Number of instances to create" - default = 1 -} - -variable "do_app_image_tag" { - type = string - description = "Tag of image from DO container registry" -} - -variable "do_space_name" { - type = string - description = "Name of the space" -} - -variable "do_space_acl" { - type = string - description = "ACL rules for spaces" - default = "private" -} - -variable "do_cms_space_name" { - type = string - description = "Name of the space" -} diff --git a/infrastructure/base/modules/github_values/main.tf b/infrastructure/base/modules/github_values/main.tf deleted file mode 100644 index c4c9fb4b..00000000 --- a/infrastructure/base/modules/github_values/main.tf +++ /dev/null @@ -1,24 +0,0 @@ -terraform { - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = "~> 2.0" - } - } -} - -resource "github_actions_secret" "github_secret" { - for_each = var.secret_map - - repository = var.repo_name - secret_name = each.key - plaintext_value = each.value -} - -resource "github_actions_variable" "github_variable" { - for_each = var.variable_map - - repository = var.repo_name - variable_name = each.key - value = each.value -} diff --git a/infrastructure/base/modules/github_values/variables.tf b/infrastructure/base/modules/github_values/variables.tf deleted file mode 100644 index 57afdfd4..00000000 --- a/infrastructure/base/modules/github_values/variables.tf +++ /dev/null @@ -1,13 +0,0 @@ -variable "repo_name" { - type = string -} - -variable "secret_map" { - type = map(string) - default = {} -} - -variable "variable_map" { - type = map(string) - default = {} -} diff --git a/infrastructure/base/modules/postgresql/main.tf b/infrastructure/base/modules/postgresql/main.tf deleted file mode 100644 index d53bc88c..00000000 --- a/infrastructure/base/modules/postgresql/main.tf +++ /dev/null @@ -1,44 +0,0 @@ -## -# Module to build the DO Postgres DB -## - -terraform { - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = "~> 2.0" - } - } -} - -data "digitalocean_project" "project" { - name = var.do_project_name -} - -resource "digitalocean_database_cluster" "postgres_cluster" { - name = "${var.project_name}-${var.environment}" - engine = "pg" - version = var.postgres_version - size = var.postgres_size - region = var.do_region - node_count = var.postgres_node_count -} - -resource "digitalocean_database_db" "postgres_database" { - cluster_id = digitalocean_database_cluster.postgres_cluster.id - name = var.postgres_db_name -} - -resource "digitalocean_database_firewall" "postgres_cluster_firewall" { - cluster_id = digitalocean_database_cluster.postgres_cluster.id - - rule { - type = "app" - value = var.app_id - } -} - -resource "digitalocean_project_resources" "postgres_cluster" { - project = data.digitalocean_project.project.id - resources = [digitalocean_database_cluster.postgres_cluster.urn] -} diff --git a/infrastructure/base/modules/postgresql/outputs.tf b/infrastructure/base/modules/postgresql/outputs.tf deleted file mode 100644 index 8b5e4a78..00000000 --- a/infrastructure/base/modules/postgresql/outputs.tf +++ /dev/null @@ -1,17 +0,0 @@ -output "user" { - value = digitalocean_database_cluster.postgres_cluster.user - sensitive = true -} - -output "password" { - value = digitalocean_database_cluster.postgres_cluster.password - sensitive = true -} - -output "host" { - value = digitalocean_database_cluster.postgres_cluster.host -} - -output "port" { - value = digitalocean_database_cluster.postgres_cluster.port -} diff --git a/infrastructure/base/modules/postgresql/variables.tf b/infrastructure/base/modules/postgresql/variables.tf deleted file mode 100644 index 35a53844..00000000 --- a/infrastructure/base/modules/postgresql/variables.tf +++ /dev/null @@ -1,47 +0,0 @@ -variable "do_region" { - type = string - description = "DigitalOcean Region" -} - -variable "do_project_name" { - type = string - description = "Project name at DO" -} - -variable "project_name" { - type = string - description = "Short name of the project, will be used to prefix created resources" -} - -variable "environment" { - type = string - description = "Name of the environment, will be used to prefix created resources" -} - -variable "postgres_size" { - type = string - description = "DigitalOcean PostgreSQL size" - default = "db-s-1vcpu-1gb" -} - -variable "postgres_db_name" { - type = string - description = "Name of PostgreSQL database" -} - -variable "postgres_version" { - type = string - description = "PostgreSQL version" - default = "15" -} - -variable "postgres_node_count" { - type = number - description = "Number of PostgreSQL nodes" - default = 1 -} - -variable "app_id" { - type = string - description = "DigitalOcean App ID" -} diff --git a/infrastructure/base/modules/space/main.tf b/infrastructure/base/modules/space/main.tf deleted file mode 100644 index f3d441d0..00000000 --- a/infrastructure/base/modules/space/main.tf +++ /dev/null @@ -1,23 +0,0 @@ -terraform { - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = "~> 2.0" - } - } -} - -data "digitalocean_project" "project" { - name = var.do_project_name -} - -resource "digitalocean_spaces_bucket" "project_space" { - name = var.do_space_name - region = var.do_region - acl = var.do_space_acl -} - -resource "digitalocean_project_resources" "project_space" { - project = data.digitalocean_project.project.id - resources = [digitalocean_spaces_bucket.project_space.urn] -} diff --git a/infrastructure/base/modules/space/outputs.tf b/infrastructure/base/modules/space/outputs.tf deleted file mode 100644 index c0c89c71..00000000 --- a/infrastructure/base/modules/space/outputs.tf +++ /dev/null @@ -1,7 +0,0 @@ -output "space_id" { - value = digitalocean_spaces_bucket.project_space.id -} - -output "bucket_domain_name" { - value = digitalocean_spaces_bucket.project_space.bucket_domain_name -} diff --git a/infrastructure/base/modules/space/variables.tf b/infrastructure/base/modules/space/variables.tf deleted file mode 100644 index 65986d16..00000000 --- a/infrastructure/base/modules/space/variables.tf +++ /dev/null @@ -1,20 +0,0 @@ -variable "do_region" { - type = string - description = "DigitalOcean Region" -} - -variable "do_project_name" { - type = string - description = "Project name at DO" -} - -variable "do_space_name" { - type = string - description = "Name of the space" -} - -variable "do_space_acl" { - type = string - description = "ACL rules for spaces" - default = "private" -} diff --git a/infrastructure/base/outputs.tf b/infrastructure/base/outputs.tf deleted file mode 100644 index 78e771f8..00000000 --- a/infrastructure/base/outputs.tf +++ /dev/null @@ -1,15 +0,0 @@ -output "staging_postgresql_host" { - value = module.staging.postgresql_host -} - -output "staging_postgresql_port" { - value = module.staging.postgresql_port -} - -output "staging_app_url" { - value = module.staging.app_url -} - -output "staging_app_domain" { - value = module.staging.app_domain -} diff --git a/infrastructure/base/variables.tf b/infrastructure/base/variables.tf deleted file mode 100644 index 0843716d..00000000 --- a/infrastructure/base/variables.tf +++ /dev/null @@ -1,91 +0,0 @@ -variable "do_region" { - type = string - description = "DigitalOcean Region" -} - -variable "do_token" { - type = string - description = "DigitalOcean Token" -} - -variable "do_spaces_client_id" { - type = string - description = "DigitalOcean Spaces Client ID" -} - -variable "do_spaces_secret_key" { - type = string - description = "DigitalOcean Spaces Secret Key" -} - -variable "do_project_name" { - type = string - description = "Project name at DO" -} - -variable "project_name" { - type = string - description = "Short name of the project, will be used to prefix created resources" -} - -variable "container_registry_name" { - type = string - description = "Name of DO container registry" -} - -variable "postgres_size" { - type = string - description = "DigitalOcean PostgreSQL size" -} - -variable "postgres_db_name" { - type = string - description = "Name of PostgreSQL database" -} - -variable "do_app_instance" { - type = string - description = "DigitalOcean Droplet size" - default = "basic-xs" -} - -variable "do_app_instance_count" { - type = number - description = "Number of instances to create" - default = 1 -} - -variable "do_app_image_tag" { - type = string - description = "Tag of image from DO container registry" - default = "latest" -} - -variable "repo_name" { - type = string - description = "Name of the Github repository where the code is hosted" -} - -variable "ga_tracking_id" { - type = string - default = "" - description = "Google Analytics tracking id" -} - -variable "mapbox_api_token" { - type = string - default = "" - description = "MAPBOX api token" -} - -variable "mapbox_username" { - type = string - default = "" - description = "MAPBOX username" -} - -variable "mapbox_style_id" { - type = string - default = "" - description = "MAPBOX style ID" -} diff --git a/infrastructure/base/vars/terraform.tfvars b/infrastructure/base/vars/terraform.tfvars deleted file mode 100644 index 4d932f7d..00000000 --- a/infrastructure/base/vars/terraform.tfvars +++ /dev/null @@ -1,12 +0,0 @@ -do_region = "fra1" -do_token = "DO TOKEN" -do_spaces_client_id = "DO Spaces Client ID" -do_spaces_secret_key = "DO Spaces Secret Key" -do_project_name = "ESA COMMS project" -project_name = "esa-gda-comms" -container_registry_name = "esa-gda-comms" -postgres_size = "db-s-1vcpu-1gb" -postgres_db_name = "esa" -do_app_instance = "basic-xs" -do_app_instance_count = 1 -repo_name = "esa" diff --git a/infrastructure/base/versions.tf b/infrastructure/base/versions.tf deleted file mode 100644 index e7b9e21f..00000000 --- a/infrastructure/base/versions.tf +++ /dev/null @@ -1,38 +0,0 @@ -## -# Terraform configuration for creating environment of this project on DO infrastructure -# This configuration should be run at following steps: -# 1. Check if you already have existing container registry at your DO account --> if it exists, skip container_registry module -# 2. If container registry does not exist or it does not have any images uncomment FIRST RUN blocks at app module --> this is require to successfully create DO APP -# 3. Apply terraform code -# 4. Run CI/CD pipeline to build and push docker images to container registry -# 5. If you have uncommented FIRST RUN blocks at app module, comment them again and apply terraform code --> set docker image tags variable to correct values -## - -terraform { - backend "s3" { - endpoint = "fra1.digitaloceanspaces.com" - bucket = "esa-gda-comms-terraform-state" - region = "us-west-1" - key = "state" - skip_credentials_validation = true - skip_metadata_api_check = true - } - - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = "~> 2.30" - } - } -} - -provider "digitalocean" { - token = var.do_token - spaces_access_id = var.do_spaces_client_id - spaces_secret_key = var.do_spaces_secret_key -} - -# https://github.com/integrations/terraform-provider-github/issues/667#issuecomment-1182340862 -provider "github" { - # owner = "Project" -} diff --git a/infrastructure/state/.terraform.lock.hcl b/infrastructure/state/.terraform.lock.hcl deleted file mode 100644 index 138c4342..00000000 --- a/infrastructure/state/.terraform.lock.hcl +++ /dev/null @@ -1,26 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/digitalocean/digitalocean" { - version = "2.29.0" - constraints = "~> 2.0" - hashes = [ - "h1:mJrr4YaOsB7bWfCSJZneiXB6JMnVNnFxYRmQ8vKaOSQ=", - "zh:0af0a1a2de818c5dc8ee7ad4dc4731452848e84cfa0c1ce514af1c7aad15c53c", - "zh:27229f3162b4142be48554f56227265982f3b74e4c79fa5d2528c8a3912d1e19", - "zh:31d6e73bfe12231fa0ab3bbeef0e4aa9822a2008ae2a1a8b22557bdada4af7a3", - "zh:6e7417413e96b87a11d47e9acbc88e6d707a6ab23a7de6b584fc600d9d3cbf00", - "zh:9faf40798a698b80e8d56e502c220856d2d5f55d5137b9cf5371f2fdaeadd70a", - "zh:b9ab9caf21b3f928fdd891e749fd8d33f6d441b39a08d725edf58cf8027a9b7b", - "zh:be32b3a35474f8acbab4d0ad8676810fa05a87918cc1874b53672159005016c0", - "zh:c2e8f7c08cad44b46e2e5580183e1ef2a4f1803347de136d1a35f333973a25f0", - "zh:cf0aba5b5042c762da489050716815652f809f3ef0ededb0f981f11691dbef03", - "zh:d1c0874c0ae0aa1eae86dbd131978796303599709c35b5dee926887d375f4cc8", - "zh:d4eecb61e763950a5a0f40cddc7a58345419a522b783aae7b0703309a354bb0c", - "zh:d866df86dd78eb2a9e54ebff637301522766710bb6dc7f8e330f1146822b62ee", - "zh:da51541ef96d0a5745740dc623bff3ccfb6b098b548d78cf5e9d95a15c69963a", - "zh:ede343be1528b468feae3a1cbf781e223f63ce33446a008a42f2fb799a23b436", - "zh:f20a60e2cecd29bbcc73d59e95aca368e2c55b7648f1923df2c0f7578026b048", - "zh:fccaf963f2db1e271e9d28276172910ca6b95471b8e0dfac758daf0495ce17f5", - ] -} diff --git a/infrastructure/state/main.tf b/infrastructure/state/main.tf deleted file mode 100644 index ea89b8d1..00000000 --- a/infrastructure/state/main.tf +++ /dev/null @@ -1,34 +0,0 @@ -terraform { - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = "~> 2.0" - } - } -} - -provider "digitalocean" { - token = var.do_token - spaces_access_id = var.do_spaces_client_id - spaces_secret_key = var.do_spaces_secret_key -} - -# Project was created manually beforehand -# resource "digitalocean_project" "terraform_state" { -# name = var.project_name -# } - -data "digitalocean_project" "terraform_state" { - name = var.do_project_name -} - -resource "digitalocean_spaces_bucket" "terraform_state" { - name = "${var.project_name}-terraform-state" - region = var.do_region - acl = "private" -} - -resource "digitalocean_project_resources" "terraform_state" { - project = data.digitalocean_project.terraform_state.id - resources = [digitalocean_spaces_bucket.terraform_state.urn] -} diff --git a/infrastructure/state/outputs.tf b/infrastructure/state/outputs.tf deleted file mode 100644 index 38792f80..00000000 --- a/infrastructure/state/outputs.tf +++ /dev/null @@ -1,3 +0,0 @@ -output "state_bucket" { - value = digitalocean_spaces_bucket.terraform_state.id -} diff --git a/infrastructure/state/variables.tf b/infrastructure/state/variables.tf deleted file mode 100644 index 49f57ad2..00000000 --- a/infrastructure/state/variables.tf +++ /dev/null @@ -1,29 +0,0 @@ -variable "do_region" { - type = string - description = "DigitalOcean Region" -} - -variable "do_token" { - type = string - description = "DigitalOcean Token" -} - -variable "do_spaces_client_id" { - type = string - description = "DigitalOcean Spaces Client ID" -} - -variable "do_spaces_secret_key" { - type = string - description = "DigitalOcean Spaces Secret Key" -} - -variable "do_project_name" { - type = string - description = "Project name at DO" -} - -variable "project_name" { - type = string - description = "Short name of the project, will be used to prefix created resources" -} diff --git a/infrastructure/state/vars/terraform.tfvars b/infrastructure/state/vars/terraform.tfvars deleted file mode 100644 index 22a9adca..00000000 --- a/infrastructure/state/vars/terraform.tfvars +++ /dev/null @@ -1,8 +0,0 @@ -do_region = "fra1" -do_token = "DO Token" -do_spaces_client_id = "DO Spaces Client ID" -do_spaces_secret_key = "DO Spaces Secret Key" -do_project_name = "ESA COMMS project" -project_name = "esa-gda-comms" - - diff --git a/infrastructure/terraform/.gitignore b/infrastructure/terraform/.gitignore new file mode 100644 index 00000000..78e35b0e --- /dev/null +++ b/infrastructure/terraform/.gitignore @@ -0,0 +1,30 @@ +# Local .terraform directory +.terraform/ + +# .tfstate files (state is stored in S3) +*.tfstate +*.tfstate.* +terraform.tfstate +terraform.tfstate.backup + +# Plan files (contain sensitive data) +*.tfplan +tfplan +*.plan + +# Crash log files +crash.log +crash.*.log + +# Override files +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Local variables file (contains sensitive data) +vars/local.tfvars + +# CLI configuration files +.terraformrc +terraform.rc \ No newline at end of file diff --git a/infrastructure/terraform/.opentofu-version b/infrastructure/terraform/.opentofu-version new file mode 100644 index 00000000..27f9cd32 --- /dev/null +++ b/infrastructure/terraform/.opentofu-version @@ -0,0 +1 @@ +1.8.0 diff --git a/infrastructure/terraform/.terraform.lock.hcl b/infrastructure/terraform/.terraform.lock.hcl new file mode 100644 index 00000000..9cb2385e --- /dev/null +++ b/infrastructure/terraform/.terraform.lock.hcl @@ -0,0 +1,59 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.14" + hashes = [ + "h1:BrNG7eFOdRrRRbHdvrTjMJ8X8Oh/tiegURiKf7J2db8=", + "zh:1a41f3ee26720fee7a9a0a361890632a1701b5dc1cf5355dc651ddbe115682ff", + "zh:30457f36690c19307921885cc5e72b9dbeba369445815903acd5c39ac0e41e7a", + "zh:42c22674d5f23f6309eaf3ac3a4f1f8b66b566c1efe1dcb0dd2fb30c17ce1f78", + "zh:4cc271c795ff8ce6479ec2d11a8ba65a0a9ed6331def6693f4b9dccb6e662838", + "zh:60932aa376bb8c87cd1971240063d9d38ba6a55502c867fdbb9f5361dc93d003", + "zh:864e42784bde77b18393ebfcc0104cea9123da5f4392e8a059789e296952eefa", + "zh:9750423138bb01ecaa5cec1a6691664f7783d301fb1628d3b64a231b6b564e0e", + "zh:e5d30c4dec271ef9d6fe09f48237ec6cfea1036848f835b4e47f274b48bda5a7", + "zh:e62bd314ae97b43d782e0841b13e68a3f8ec85cc762004f973ce5ce7b6cdbfd0", + "zh:ea851a3c072528a4445ac6236ba2ce58ffc99ec466019b0bd0e4adde63a248e4", + ] +} + +provider "registry.opentofu.org/hashicorp/random" { + version = "3.7.2" + hashes = [ + "h1:cFGCdxTlsrteTiaOV/iOQdql7eJkD3F/vtJxenkj9IE=", + "zh:2ffeb1058bd7b21a9e15a5301abb863053a2d42dffa3f6cf654a1667e10f4727", + "zh:519319ed8f4312ed76519652ad6cd9f98bc75cf4ec7990a5684c072cf5dd0a5d", + "zh:7371c2cc28c94deb9dba62fbac2685f7dde47f93019273a758dd5a2794f72919", + "zh:9b0ac4c1d8e36a86b59ced94fa517ae9b015b1d044b3455465cc6f0eab70915d", + "zh:c6336d7196f1318e1cbb120b3de8426ce43d4cacd2c75f45dba2dbdba666ce00", + "zh:c71f18b0cb5d55a103ea81e346fb56db15b144459123f1be1b0209cffc1deb4e", + "zh:d2dc49a6cac2d156e91b0506d6d756809e36bf390844a187f305094336d3e8d8", + "zh:d5b5fc881ccc41b268f952dae303501d6ec9f9d24ee11fe2fa56eed7478e15d0", + "zh:db9723eaca26d58c930e13fde221d93501529a5cd036b1f167ef8cff6f1a03cc", + "zh:fe3359f733f3ab518c6f85f3a9cd89322a7143463263f30321de0973a52d4ad8", + ] +} + +provider "registry.opentofu.org/integrations/github" { + version = "5.45.0" + constraints = "~> 5.17" + hashes = [ + "h1:sP/Er9osOsz4vhKZAul+GeV0c5XdvMblJBMiP+T5tWc=", + "zh:2afb8ee5b847071e51d5a39bcad5cf466c4d22452450d37c44a5f9d2eb9879e5", + "zh:38d087b88c86ddd63b60d14d613f86a5885d154048098c0484266a9a69018b16", + "zh:3e6a787e3e40f1535d85f8dc5f2e8c90242ab8237feebd027f696fa154261394", + "zh:55dac5a813b3774b48ca45b8a797c32e6d787d4f282b43b622155cad3daac46a", + "zh:563f2782f3c4c584b249c5fa0628951a57b4593f3c5805a4efb6d494f8686716", + "zh:677180ec9376d5f926286592998e2864c85f06d6b416c1d89031d817a285c72e", + "zh:80eec141fa47131e8f60a6478e51b3a5920efe803444e684f9605fca09a24e34", + "zh:8b9f1e1f4b42b51e53767f4f927eabdcefe55fb0369e996ac2a0063148b5e48d", + "zh:95627f75848561830f8c20949f024f902a2100a022c68aa8d84320f43e75cc46", + "zh:95ac41b99dfca3ce556092e036bb04dc03367d0779071112e59d4bf11259a89d", + "zh:9e966482729ba8214b480bdd786aff9a15234e9c093c5406b56ce89ccb07dcab", + "zh:b7a9d563613f1b9a233f8f285848cc9d8c08c556aad7ea57cd63e0abb19b10cf", + "zh:ce56bb7ca876f47f5beee01de3ab84d27964b972c9adceb8e2f7824891e05c27", + "zh:f73e063ad5b84f1943eafb8a52a26dd805d06ac11d6c951175ac76c07187f553", + ] +} diff --git a/infrastructure/terraform/main.tf b/infrastructure/terraform/main.tf new file mode 100644 index 00000000..723d8611 --- /dev/null +++ b/infrastructure/terraform/main.tf @@ -0,0 +1,106 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.14" + } + } + + // TF does not allow vars here. Use main vars or module outputs for other variables + backend "s3" { + bucket = "esa-tofu-state" + key = "state" + region = "eu-west-3" + profile = "esa" + } + + required_version = "~> 1.8.0" +} + +module "state" { + source = "./modules/state" + state_project_name = var.project_name + state_aws_region = var.aws_region + state_aws_profile = var.aws_profile +} + +module "frontend_ecr" { + source = "./modules/ecr" + project_name = var.project_name + ecr_repo_name = "frontend" +} + +module "cms_ecr" { + source = "./modules/ecr" + project_name = var.project_name + ecr_repo_name = "cms" +} + +module "iam" { + source = "./modules/iam" +} + +module "vpc" { + source = "./modules/vpc" + project = var.project_name + aws_region = var.aws_region +} + +module "dev" { + providers = { + aws = aws.dev + } + + source = "./modules/env" + domain = "esa-gda.dev-vizzuality.com" + base_path = "/impact-sphere" + + project = var.project_name + environment = "dev" + + aws_region = var.aws_region + + vpc = module.vpc.vpc + subnet_ids = module.vpc.public_subnet_ids + availability_zones = module.vpc.availability_zones + + beanstalk_platform = "64bit Amazon Linux 2023 v4.7.3 running Docker" + beanstalk_tier = "WebServer" + ec2_instance_type = "t3.medium" + + elasticbeanstalk_iam_service_linked_role_name = module.iam.elasticbeanstalk_service_linked_role + cname_prefix = "${var.project_name}-dev-environment" + + rds_instance_class = "db.t4g.medium" + rds_engine_version = "15.12" + rds_backup_retention_period = 3 + + repo_name = var.github_repo_name + github_owner = var.github_owner + github_token = var.github_token + + github_additional_environment_variables = { + NEXT_PUBLIC_MATOMO_URL = "https://gda.esa.int/eopsua/" + NEXT_PUBLIC_MATOMO_SITE_ID = "3" + } +} + +module "github" { + source = "./modules/github" + repo_name = var.github_repo_name + github_owner = var.github_owner + github_token = var.github_token + global_secret_map = { + PIPELINE_USER_ACCESS_KEY_ID = module.iam.pipeline_user_access_key_id + PIPELINE_USER_SECRET_ACCESS_KEY = module.iam.pipeline_user_access_key_secret + } + global_variable_map = { + PROJECT_NAME = var.project_name + NEXT_PUBLIC_MAPBOX_API_TOKEN = var.mapbox_api_token + NEXT_PUBLIC_MAPBOX_USERNAME = var.mapbox_username + NEXT_PUBLIC_MAPBOX_STYLE_ID = var.mapbox_style_id + AWS_REGION = var.aws_region + CLIENT_REPOSITORY_NAME = module.frontend_ecr.repository_name + CMS_REPOSITORY_NAME = module.cms_ecr.repository_name + } +} diff --git a/infrastructure/terraform/modules/beanstalk/iam.tf b/infrastructure/terraform/modules/beanstalk/iam.tf new file mode 100644 index 00000000..b1dbb376 --- /dev/null +++ b/infrastructure/terraform/modules/beanstalk/iam.tf @@ -0,0 +1,50 @@ +# most of this is from https://gist.github.com/tomfa/6fc429af5d598a85e723b3f56f681237 + +# the role and profile for the EC2 + +resource "aws_iam_instance_profile" "beanstalk_ec2" { + name = "${var.application_name}-beanstalk-ec2-user" + role = aws_iam_role.beanstalk_ec2.name +} + + +resource "aws_iam_role" "beanstalk_ec2" { + name = "${var.application_name}-beanstalk-ec2-role" + assume_role_policy = < { + name = v.secret_name + created_at = v.created_at + updated_at = v.updated_at + } + } +} + +output "global_variables" { + description = "Map of global GitHub Actions variables" + value = { + for k, v in github_actions_variable.global_github_variable : k => { + name = v.variable_name + value = v.value + created_at = v.created_at + updated_at = v.updated_at + } + } +} + +output "environment_secrets" { + description = "Map of GitHub Actions environment secrets" + value = { + for k, v in github_actions_environment_secret.environment_secret : k => { + name = v.secret_name + environment = v.environment + created_at = v.created_at + updated_at = v.updated_at + } + } +} + +output "environment_variables" { + description = "Map of GitHub Actions environment variables" + value = { + for k, v in github_actions_environment_variable.environment_variable : k => { + name = v.variable_name + value = v.value + environment = v.environment + created_at = v.created_at + updated_at = v.updated_at + } + } +} + +output "environment_name" { + description = "GitHub environment name if created" + value = var.github_environment +} diff --git a/infrastructure/terraform/modules/github/variables.tf b/infrastructure/terraform/modules/github/variables.tf new file mode 100644 index 00000000..6e44e025 --- /dev/null +++ b/infrastructure/terraform/modules/github/variables.tf @@ -0,0 +1,39 @@ +variable "repo_name" { + type = string +} + +variable "global_secret_map" { + type = map(string) + default = {} +} + +variable "global_variable_map" { + type = map(string) + default = {} +} + +variable "environment_variable_map" { + type = map(string) + default = {} +} + +variable "environment_secret_map" { + type = map(string) + default = {} +} + +variable "github_owner" { + type = string + description = "Owner of the Github repository where the code is hosted" +} + +variable "github_token" { + type = string + description = "Github token to access the repository" +} + +variable "github_environment" { + type = string + description = "Environment to create in the Github repository" + default = null +} diff --git a/infrastructure/terraform/modules/iam/main.tf b/infrastructure/terraform/modules/iam/main.tf new file mode 100644 index 00000000..0c3896ae --- /dev/null +++ b/infrastructure/terraform/modules/iam/main.tf @@ -0,0 +1,66 @@ +# This user's access key & secret access key will be needed in GH Secrets +resource "aws_iam_user" "pipeline_user" { + name = "CatalyseProjectPipelineUser" +} + +resource "aws_iam_access_key" "pipeline_user_access_key" { + user = aws_iam_user.pipeline_user.name +} + +resource "aws_iam_user_policy_attachment" "eb_web_tier_user_policy" { + user = aws_iam_user.pipeline_user.name + policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier" +} + +resource "aws_iam_user_policy_attachment" "eb_managed_updates_customer_user_policy" { + user = aws_iam_user.pipeline_user.name + policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy" +} + +## Below policies are needed to login through GitHub Actions + +resource "aws_iam_user_policy" "get_ecr_token_policy" { + name = "get_ecr_token_policy" + user = aws_iam_user.pipeline_user.name + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "ecr:GetAuthorizationToken" + ] + Effect = "Allow" + Resource = "*" + }, + ] + }) +} + +resource "aws_iam_user_policy" "ecr_push_pull_policy" { + name = "ecr_push_pull_policy" + user = aws_iam_user.pipeline_user.name + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability", + "ecr:CompleteLayerUpload", + "ecr:GetDownloadUrlForLayer", + "ecr:InitiateLayerUpload", + "ecr:PutImage", + "ecr:UploadLayerPart" + ] + Effect = "Allow" + Resource = "*" + }, + ] + }) +} + +resource "aws_iam_service_linked_role" "elasticbeanstalk" { + aws_service_name = "elasticbeanstalk.amazonaws.com" +} \ No newline at end of file diff --git a/infrastructure/terraform/modules/iam/outputs.tf b/infrastructure/terraform/modules/iam/outputs.tf new file mode 100644 index 00000000..790e9385 --- /dev/null +++ b/infrastructure/terraform/modules/iam/outputs.tf @@ -0,0 +1,19 @@ +output "pipeline_user_arn" { + description = "The Amazon Resource Name (ARN) assigned to the pipeline user" + value = try(aws_iam_user.pipeline_user.arn, "") +} + +output "pipeline_user_access_key_id" { + description = "The access key id for the pipeline user" + value = try(aws_iam_access_key.pipeline_user_access_key.id, "") +} + +output "pipeline_user_access_key_secret" { + description = "The access key secret for the pipeline user" + value = try(aws_iam_access_key.pipeline_user_access_key.secret, "") +} + +output "elasticbeanstalk_service_linked_role" { + description = "The ARN of the AWS Service Linked Role for Elastic Beanstalk" + value = try(aws_iam_service_linked_role.elasticbeanstalk.arn, "") +} \ No newline at end of file diff --git a/infrastructure/terraform/modules/rds/iam_policy_secrets_read.json.tpl b/infrastructure/terraform/modules/rds/iam_policy_secrets_read.json.tpl new file mode 100644 index 00000000..9520fd3d --- /dev/null +++ b/infrastructure/terraform/modules/rds/iam_policy_secrets_read.json.tpl @@ -0,0 +1,17 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "secretsmanager:GetResourcePolicy", + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + "secretsmanager:ListSecretVersionIds" + ], + "Resource": [ + "${secret_arn}" + ] + } + ] +} \ No newline at end of file diff --git a/infrastructure/terraform/modules/rds/main.tf b/infrastructure/terraform/modules/rds/main.tf new file mode 100644 index 00000000..802e5a06 --- /dev/null +++ b/infrastructure/terraform/modules/rds/main.tf @@ -0,0 +1,106 @@ +######################## +## Cluster +######################## + +resource "aws_db_instance" "postgresql" { + identifier = "db-${var.project}-${var.environment}" + engine = "postgres" + engine_version = var.rds_engine_version + instance_class = var.rds_instance_class + availability_zone = var.availability_zones[0] + db_name = replace(var.database_name, "-", "_") + username = var.rds_user_name + password = random_password.postgresql_superuser.result + backup_retention_period = 5 + allocated_storage = 5 + skip_final_snapshot = true + + maintenance_window = "Mon:00:00-Mon:03:00" + backup_window = "03:00-06:00" + + deletion_protection = true + + vpc_security_group_ids = [ + aws_security_group.postgresql.id + ] + db_subnet_group_name = aws_db_subnet_group.postgresql.name +} + +resource "random_password" "postgresql_superuser" { + length = 16 + special = false +} + + +##################### +# Security Groups +##################### + + +# Allow access to aurora to all resources which are in the same security group +resource "aws_security_group" "postgresql" { + vpc_id = var.vpc_id + description = "Security Group for PostgreSQL DB" + name = "${var.project}-${var.environment}-PostgreSQL-ingress" + revoke_rules_on_delete = true + tags = merge( + { + Name = "${var.project} ${var.environment} RDS SG" + }, + var.tags + ) +} + +resource "aws_security_group_rule" "postgresql_ingress" { + type = "ingress" + from_port = var.rds_port + to_port = var.rds_port + protocol = "tcp" + cidr_blocks = [var.vpc_cidr_block] + security_group_id = aws_security_group.postgresql.id +} + +resource "aws_db_subnet_group" "postgresql" { + name = "${var.project}-${var.environment}-db-subnet-group" + subnet_ids = var.subnet_ids + + tags = merge( + { + Name = "${var.project}-${var.environment}-db-subnet-group" + }, + var.tags + ) +} + +#################### +# Secret Manager +#################### + +resource "aws_secretsmanager_secret" "postgresql-admin" { + description = "Connection string for PostgreSQL cluster" + name = "${var.project}-${var.environment}-postgresql-admin-password" + tags = var.tags +} + +resource "aws_secretsmanager_secret_version" "postgresql-admin" { + + secret_id = aws_secretsmanager_secret.postgresql-admin.id + secret_string = jsonencode({ + "username" = var.rds_user_name, + "engine" = "postgresql", + "host" = aws_db_instance.postgresql.endpoint + "password" = random_password.postgresql_superuser.result, + "port" = var.rds_port, + }) +} + +locals { + secrets_postgresql_writer = templatefile("${path.module}/iam_policy_secrets_read.json.tpl", { + secret_arn = aws_secretsmanager_secret.postgresql-admin.arn + }) +} + +resource "aws_iam_policy" "secrets_postgresql-writer" { + name = "${var.project}-${var.environment}-secrets-postgresql-writer" + policy = local.secrets_postgresql_writer +} diff --git a/infrastructure/terraform/modules/rds/outputs.tf b/infrastructure/terraform/modules/rds/outputs.tf new file mode 100644 index 00000000..a10d7bfd --- /dev/null +++ b/infrastructure/terraform/modules/rds/outputs.tf @@ -0,0 +1,34 @@ +output "security_group_id" { + value = aws_security_group.postgresql.id + description = "Security group ID to access postgresql database" +} + +output "postgresql-password-secret-arn" { + value = aws_secretsmanager_secret.postgresql-admin.arn +} + +output "postgresql-password-secret-version-arn" { + value = aws_secretsmanager_secret_version.postgresql-admin.arn +} + +output "username" { + value = aws_db_instance.postgresql.username + sensitive = true +} + +output "password" { + value = aws_db_instance.postgresql.password + sensitive = true +} + +output "host" { + value = aws_db_instance.postgresql.address +} + +output "port" { + value = aws_db_instance.postgresql.port +} + +output "db_name" { + value = aws_db_instance.postgresql.db_name +} diff --git a/infrastructure/terraform/modules/rds/variables.tf b/infrastructure/terraform/modules/rds/variables.tf new file mode 100644 index 00000000..f5de7ed6 --- /dev/null +++ b/infrastructure/terraform/modules/rds/variables.tf @@ -0,0 +1,76 @@ +######################## +## Variables +######################## + +variable "project" { + type = string + description = "Project name, used for naming resources" +} + +variable "environment" { + type = string + description = "The name of the environment this server hosts" +} + +variable "vpc_id" { + type = string + description = "The ID of the VPC that the RDS cluster will be created in" +} + +variable "subnet_ids" { + type = list(string) + description = "The ID's of the VPC subnets that the RDS cluster instances will be created in" +} + +variable "rds_user_name" { + type = string + description = "RDS master user name" +} + +variable "rds_backup_retention_period" { + type = number + description = "Retention period for backup files, in days" +} + +variable "tags" { + type = map(string) + description = "Tags to add to resources" +} + +variable "log_retention_period" { + type = number + description = "Number of days to retain logs in Cloud Watch" +} + +variable "rds_engine_version" { + type = string + description = "RDS Database engine version" +} + +variable "rds_instance_count" { + type = number + description = "Number of permanent RDS instances" +} + +variable "rds_instance_class" { + type = string + description = "RDS instance type class" +} + +variable "rds_port" { + type = string + description = "Port to access RDS database" +} + +variable "vpc_cidr_block" { + type = string + description = "CIDR block of VPC" +} + +variable "availability_zones" { + type = list(any) +} + +variable "database_name" { + type = string +} diff --git a/infrastructure/terraform/modules/s3-public/main.tf b/infrastructure/terraform/modules/s3-public/main.tf new file mode 100644 index 00000000..d9a4a0a4 --- /dev/null +++ b/infrastructure/terraform/modules/s3-public/main.tf @@ -0,0 +1,111 @@ +## Resource: S3 Bucket for public uploads (CMS media and map layers) +resource "aws_s3_bucket" "this" { + bucket = "${var.project}-${var.environment}-public" + ## Beware: This allows TF to destroy the resource even if the bucket is not empty + force_destroy = true + + tags = merge({ + Name = "${var.project}-${var.environment}-public" + }) +} + +## Ownership Controls - Disable ACLs (recommended by AWS) +resource "aws_s3_bucket_ownership_controls" "this" { + bucket = aws_s3_bucket.this.id + + rule { + object_ownership = "BucketOwnerEnforced" + } +} + +## Public Access Block - Allow public access +resource "aws_s3_bucket_public_access_block" "app_bucket_block" { + bucket = aws_s3_bucket.this.id + + block_public_acls = false + block_public_policy = false + ignore_public_acls = false + restrict_public_buckets = false + + depends_on = [aws_s3_bucket_ownership_controls.this] +} + +## Bucket Policy - Allow public read access +resource "aws_s3_bucket_policy" "public_read" { + bucket = aws_s3_bucket.this.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "PublicReadGetObject" + Effect = "Allow" + Principal = "*" + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.this.arn}/*" + } + ] + }) + + depends_on = [aws_s3_bucket_public_access_block.app_bucket_block] +} + +## CORS Configuration +resource "aws_s3_bucket_cors_configuration" "this" { + bucket = aws_s3_bucket.this.id + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["GET", "PUT", "POST", "DELETE", "HEAD"] + allowed_origins = ["*"] + expose_headers = ["ETag"] + max_age_seconds = 3000 + } +} + +## IAM User for S3 bucket access (for CMS uploads) +resource "aws_iam_user" "s3_upload_user" { + name = "${var.project}-${var.environment}-s3-upload-user" + + tags = merge({ + Name = "${var.project}-${var.environment}-s3-upload-user" + }) +} + +## IAM Access Key for S3 upload user +resource "aws_iam_access_key" "s3_upload_user_access_key" { + user = aws_iam_user.s3_upload_user.name +} + +## IAM Policy for S3 bucket read/write access +resource "aws_iam_user_policy" "s3_upload_policy" { + name = "${var.project}-${var.environment}-s3-upload-policy" + user = aws_iam_user.s3_upload_user.name + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:ListAllMyBuckets" + ] + Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "s3:PutObject", + "s3:GetObject", + "s3:DeleteObject", + "s3:ListBucket", + "s3:GetBucketLocation" + ] + Resource = [ + aws_s3_bucket.this.arn, + "${aws_s3_bucket.this.arn}/*" + ] + } + ] + }) +} diff --git a/infrastructure/terraform/modules/s3-public/outputs.tf b/infrastructure/terraform/modules/s3-public/outputs.tf new file mode 100644 index 00000000..148e1af8 --- /dev/null +++ b/infrastructure/terraform/modules/s3-public/outputs.tf @@ -0,0 +1,36 @@ +output "bucket_name" { + description = "S3 bucket name" + value = aws_s3_bucket.this.bucket +} + +output "bucket_arn" { + description = "S3 bucket ARN" + value = aws_s3_bucket.this.arn +} + +output "bucket_region" { + description = "S3 bucket region" + value = aws_s3_bucket.this.region +} + +output "bucket_domain_name" { + description = "S3 bucket domain name" + value = aws_s3_bucket.this.bucket_domain_name +} + +output "bucket_regional_domain_name" { + description = "S3 bucket regional domain name" + value = aws_s3_bucket.this.bucket_regional_domain_name +} + +output "s3_access_key_id" { + description = "Access key ID for S3 upload user" + value = aws_iam_access_key.s3_upload_user_access_key.id + sensitive = true +} + +output "s3_secret_access_key" { + description = "Secret access key for S3 upload user" + value = aws_iam_access_key.s3_upload_user_access_key.secret + sensitive = true +} diff --git a/infrastructure/terraform/modules/s3-public/variables.tf b/infrastructure/terraform/modules/s3-public/variables.tf new file mode 100644 index 00000000..5d867fbc --- /dev/null +++ b/infrastructure/terraform/modules/s3-public/variables.tf @@ -0,0 +1,14 @@ +variable "project" { + description = "Project name" + type = string +} + +variable "environment" { + description = "Environment name (e.g., dev, staging, production)" + type = string +} + +variable "aws_region" { + description = "AWS region" + type = string +} diff --git a/infrastructure/terraform/modules/s3/main.tf b/infrastructure/terraform/modules/s3/main.tf new file mode 100644 index 00000000..4a2c8135 --- /dev/null +++ b/infrastructure/terraform/modules/s3/main.tf @@ -0,0 +1,20 @@ +## Resource: S3 Bucket +resource "aws_s3_bucket" "this" { + bucket = "${var.project}-${var.environment}-bucket" + ## Beware: This allows TF to destroy the resource even if the bucket is not empty + force_destroy = true + + tags = merge({ + Name = "${var.project}-${var.environment}-bucket" + }) +} + +## Public Access Block +resource "aws_s3_bucket_public_access_block" "app_bucket_block" { + bucket = aws_s3_bucket.this.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} diff --git a/infrastructure/terraform/modules/s3/outputs.tf b/infrastructure/terraform/modules/s3/outputs.tf new file mode 100644 index 00000000..19fca4d8 --- /dev/null +++ b/infrastructure/terraform/modules/s3/outputs.tf @@ -0,0 +1,7 @@ +output "s3_outputs" { + description = "S3 Bucket Outputs" + value = { + name = aws_s3_bucket.this.bucket + arn = aws_s3_bucket.this.arn + } +} diff --git a/infrastructure/terraform/modules/s3/variables.tf b/infrastructure/terraform/modules/s3/variables.tf new file mode 100644 index 00000000..cede895a --- /dev/null +++ b/infrastructure/terraform/modules/s3/variables.tf @@ -0,0 +1,9 @@ +variable "project" { + description = "Project name" + type = string +} + +variable "environment" { + description = "Environment name (e.g., staging, production)" + type = string +} \ No newline at end of file diff --git a/infrastructure/terraform/modules/state/main.tf b/infrastructure/terraform/modules/state/main.tf new file mode 100644 index 00000000..462ffa6d --- /dev/null +++ b/infrastructure/terraform/modules/state/main.tf @@ -0,0 +1,39 @@ + + +provider "aws" { + region = var.state_aws_region + profile = var.state_aws_profile +} + +resource "aws_s3_bucket" "state" { + bucket = "${var.state_project_name}-tofu-state" + + tags = { + Project = var.state_project_name + } +} + +resource "aws_s3_bucket_ownership_controls" "state" { + bucket = aws_s3_bucket.state.id + + rule { + object_ownership = "BucketOwnerEnforced" + } +} + +resource "aws_s3_bucket_versioning" "state" { + bucket = aws_s3_bucket.state.id + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "state" { + bucket = aws_s3_bucket.state.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} diff --git a/infrastructure/terraform/modules/state/outputs.tf b/infrastructure/terraform/modules/state/outputs.tf new file mode 100644 index 00000000..053d32a2 --- /dev/null +++ b/infrastructure/terraform/modules/state/outputs.tf @@ -0,0 +1,4 @@ +output "state_bucket_name" { + value = aws_s3_bucket.state.bucket + description = "Name of the S3 bucket used to store the Terraform state" +} \ No newline at end of file diff --git a/infrastructure/terraform/modules/state/permissions.json b/infrastructure/terraform/modules/state/permissions.json new file mode 100644 index 00000000..437ca159 --- /dev/null +++ b/infrastructure/terraform/modules/state/permissions.json @@ -0,0 +1,45 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "s3:GetLifecycleConfiguration", + "s3:GetBucketTagging", + "dynamodb:DeleteItem", + "s3:GetBucketLogging", + "dynamodb:ListTagsOfResource", + "s3:CreateBucket", + "s3:ListBucket", + "s3:GetAccelerateConfiguration", + "dynamodb:DeleteTable", + "s3:GetBucketPolicy", + "s3:PutEncryptionConfiguration", + "s3:GetBucketObjectLockConfiguration", + "s3:GetEncryptionConfiguration", + "dynamodb:DescribeTable", + "s3:GetBucketRequestPayment", + "dynamodb:GetItem", + "dynamodb:DescribeContinuousBackups", + "s3:PutBucketAcl", + "s3:GetBucketOwnershipControls", + "s3:DeleteBucket", + "s3:PutBucketVersioning", + "dynamodb:PutItem", + "s3:GetBucketWebsite", + "s3:PutBucketOwnershipControls", + "s3:GetBucketVersioning", + "dynamodb:DescribeTimeToLive", + "s3:GetBucketAcl", + "s3:GetReplicationConfiguration", + "dynamodb:CreateTable", + "s3:GetBucketCORS" + ], + "Resource": [ + "arn:aws:dynamodb:*:{accountId}:table/*", + "arn:aws:s3:::*" + ] + } + ] +} diff --git a/infrastructure/terraform/modules/state/variables.tf b/infrastructure/terraform/modules/state/variables.tf new file mode 100644 index 00000000..0ff80b5e --- /dev/null +++ b/infrastructure/terraform/modules/state/variables.tf @@ -0,0 +1,16 @@ +variable "state_aws_region" { + type = string + description = "AWS region" + default = "eu-west-3" +} + +variable "state_project_name" { + type = string + description = "Short name of the project, will be used to prefix created resources" +} + +variable "state_aws_profile" { + type = string + description = "AWS profile to use to perform TF operations" +} + diff --git a/infrastructure/terraform/modules/vpc/main.tf b/infrastructure/terraform/modules/vpc/main.tf new file mode 100644 index 00000000..47b3d271 --- /dev/null +++ b/infrastructure/terraform/modules/vpc/main.tf @@ -0,0 +1,63 @@ +resource "aws_vpc" "this" { + cidr_block = "10.0.0.0/16" + enable_dns_support = true + enable_dns_hostnames = true + + tags = { + Name = "${var.project}-vpc" + } +} + +resource "aws_internet_gateway" "this" { + vpc_id = aws_vpc.this.id + + tags = { + Name = "${var.project}-igw" + } +} + +resource "aws_subnet" "public_a" { + vpc_id = aws_vpc.this.id + cidr_block = "10.0.1.0/24" + availability_zone = "${var.aws_region}a" + map_public_ip_on_launch = true + + tags = { + Name = "${var.project}-public-a" + } +} + +resource "aws_subnet" "public_b" { + vpc_id = aws_vpc.this.id + cidr_block = "10.0.2.0/24" + availability_zone = "${var.aws_region}b" + map_public_ip_on_launch = true + + tags = { + Name = "${var.project}-public-b" + } +} + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.this.id + + tags = { + Name = "${var.project}-public-rt" + } +} + +resource "aws_route" "public_internet_access" { + route_table_id = aws_route_table.public.id + destination_cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.this.id +} + +resource "aws_route_table_association" "public_a" { + subnet_id = aws_subnet.public_a.id + route_table_id = aws_route_table.public.id +} + +resource "aws_route_table_association" "public_b" { + subnet_id = aws_subnet.public_b.id + route_table_id = aws_route_table.public.id +} \ No newline at end of file diff --git a/infrastructure/terraform/modules/vpc/outputs.tf b/infrastructure/terraform/modules/vpc/outputs.tf new file mode 100644 index 00000000..fa8e87b4 --- /dev/null +++ b/infrastructure/terraform/modules/vpc/outputs.tf @@ -0,0 +1,32 @@ +output "vpc_id" { + description = "The ID of the created VPC" + value = aws_vpc.this.id +} + +output "public_subnet_ids" { + description = "List of public subnet IDs" + value = [aws_subnet.public_a.id, aws_subnet.public_b.id] +} + +output "vpc_cidr_block" { + description = "CIDR block of the VPC" + value = aws_vpc.this.cidr_block +} + +output "internet_gateway_id" { + description = "ID of the Internet Gateway" + value = aws_internet_gateway.this.id +} + +output "vpc" { + description = "VPC object including id, cidr_block" + value = { + id = aws_vpc.this.id + cidr_block = aws_vpc.this.cidr_block + } +} + +output "availability_zones" { + description = "Availability zones used for the public subnets" + value = [aws_subnet.public_a.availability_zone, aws_subnet.public_b.availability_zone] +} \ No newline at end of file diff --git a/infrastructure/terraform/modules/vpc/variables.tf b/infrastructure/terraform/modules/vpc/variables.tf new file mode 100644 index 00000000..33d0a054 --- /dev/null +++ b/infrastructure/terraform/modules/vpc/variables.tf @@ -0,0 +1,9 @@ +variable "project" { + type = string + description = "Project name used in tagging and naming AWS resources." +} + +variable "aws_region" { + description = "The AWS region to deploy resources in" + type = string +} \ No newline at end of file diff --git a/infrastructure/terraform/outputs.tf b/infrastructure/terraform/outputs.tf new file mode 100644 index 00000000..9dc3620d --- /dev/null +++ b/infrastructure/terraform/outputs.tf @@ -0,0 +1,46 @@ +# GitHub Actions - Global Secrets +output "github_global_secrets" { + description = "Map of global GitHub Actions secrets (values not shown)" + value = module.github.global_secrets +} + +# GitHub Actions - Global Variables +output "github_global_variables" { + description = "Map of global GitHub Actions variables with values" + value = module.github.global_variables +} + +# GitHub Actions - Dev Environment Secrets +output "github_dev_environment_secrets" { + description = "Map of GitHub Actions dev environment secrets (values not shown)" + value = module.dev.github_environment_secrets +} + +# GitHub Actions - Dev Environment Variables +output "github_dev_environment_variables" { + description = "Map of GitHub Actions dev environment variables with values" + value = module.dev.github_environment_variables +} + +# GitHub - Dev Environment Name +output "github_dev_environment_name" { + description = "GitHub dev environment name" + value = module.dev.github_environment_name +} + +# Dev S3 Public Bucket +output "dev_s3_public_bucket_name" { + description = "Dev environment public S3 bucket name for uploads" + value = module.dev.s3_public_bucket_name +} + +output "dev_s3_public_bucket_region" { + description = "Dev environment public S3 bucket region" + value = module.dev.s3_public_bucket_region +} + +output "dev_s3_public_bucket_url" { + description = "Dev environment public S3 bucket URL" + value = module.dev.s3_public_bucket_url +} + diff --git a/infrastructure/terraform/providers.tf b/infrastructure/terraform/providers.tf new file mode 100644 index 00000000..5badfb8d --- /dev/null +++ b/infrastructure/terraform/providers.tf @@ -0,0 +1,23 @@ +provider "aws" { + region = var.aws_region + profile = var.aws_profile + + default_tags { + tags = { + Project = var.project_name + } + } +} + +provider "aws" { + alias = "dev" + region = var.aws_region + profile = var.aws_profile + + default_tags { + tags = { + Project = var.project_name + Environment = "dev" + } + } +} \ No newline at end of file diff --git a/infrastructure/terraform/source_bundle/.ebextensions/authorized_keys.config b/infrastructure/terraform/source_bundle/.ebextensions/authorized_keys.config new file mode 100644 index 00000000..5ba8d652 --- /dev/null +++ b/infrastructure/terraform/source_bundle/.ebextensions/authorized_keys.config @@ -0,0 +1,17 @@ +files: + /home/ec2-user/.ssh/extra_authorized_keys: + mode: "000400" + owner: ec2-user + group: ec2-user + content: | + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCyeDY64NwHA4hELV92xOVjZP2+10AK9m7oTQF7T3sgh2LxcWoS7iFVM22Sl4Xbo2SEXdTebmK5ORVd8WqhdVNN7eZQPAACfGSfGk/JYQu5nRP6s3n3hFWEpsuLFth/um+IRw6guSbn7cjIDxL5mMx+o3rfN93S/Nqpe6dqL7YmA2G16SPcQgrFANgER3CXKG7za7XWXsrTtAWdAF0jXe/JS7N9R5RsOwZJD9bbfUC8oTMV+v3ZO2PLWxek1LaG7pbEdYvdrk/B1TiLrL7PIgThcK9QFxwDVReOhASBf56GJDwKaKVhJT6omzrQcpnJkYhxL8RLLawBSVVGCKEVvlWumoiZwNa5qV20+2RSHSS22uzef+gWfjaJynRKbCx+sEogCV9l6WWYQR9mRzakq0wBvD3mOqNM58nZcpY5nPE+A1nwY2Dfc78UyPG1VIiuQd4ryKyucdbkHhoVxmuz2yIPudAFY7bUWyWmllNWBHBHuZPImBfXVIAhhx12ouWJa7v0LfuYrT0Q4NzNjGkdzixod/SDa2x06z1tSK9zg6X+jzLRAFKfY36y1ZU9f3QzR2v2sdoafB0vtx6FsApBTkTP0Uw9g7ulDjxumJjLVaBuUMfORJXNoG200d7m7cKLNFcLd3RzT3ZK6PzlDObFzHXfiefhQq91tlpWUHLTBNOf4Q== alejandro.peralta@vizzuality.com +commands: + 01_touch_keys_file: + cwd: /home/ec2-user/.ssh/ + command: touch authorized_keys + 02_append_keys: + cwd: /home/ec2-user/.ssh/ + command: sort -u extra_authorized_keys authorized_keys -o authorized_keys + 99_rm_extra_keys: + cwd: /home/ec2-user/.ssh/ + command: rm extra_authorized_keys diff --git a/infrastructure/terraform/source_bundle/proxy/conf.d/application.conf b/infrastructure/terraform/source_bundle/proxy/conf.d/application.conf new file mode 100644 index 00000000..3e51936a --- /dev/null +++ b/infrastructure/terraform/source_bundle/proxy/conf.d/application.conf @@ -0,0 +1,53 @@ +upstream frontend { + server client:3000; +} + +upstream cms { + server cms:1337; +} + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + server_name _; + + # Strapi CMS/API - MUST come before /impact-sphere to match correctly + # Using regex location for exact prefix matching + location ~ ^/impact-sphere/cms(/.*)?$ { + # Rewrite to remove /impact-sphere/cms prefix + rewrite ^/impact-sphere/cms(/.*)?$ $1 break; + + proxy_pass http://cms; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_cache_bypass $http_upgrade; + proxy_pass_request_headers on; + client_max_body_size 10m; + } + + # Next.js frontend (basePath = /impact-sphere) + location /impact-sphere { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_cache_bypass $http_upgrade; + } +} \ No newline at end of file diff --git a/infrastructure/terraform/variables.tf b/infrastructure/terraform/variables.tf new file mode 100644 index 00000000..92ef34f0 --- /dev/null +++ b/infrastructure/terraform/variables.tf @@ -0,0 +1,44 @@ +variable "aws_profile" { + type = string + description = "AWS profile to use to perform TF operations" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "eu-west-3" +} +variable "project_name" { + type = string + description = "Short name of the project, will be used to prefix created resources" +} + +variable "github_owner" { + type = string + description = "Owner of the Github repository where the code is hosted" +} + +variable "github_repo_name" { + type = string + description = "Name of the Github repository where the code is hosted" +} + +variable "github_token" { + type = string + description = "Github token to access the repository" +} + +variable "mapbox_api_token" { + type = string + description = "Mapbox API token" +} + +variable "mapbox_username" { + type = string + description = "Mapbox username" +} + +variable "mapbox_style_id" { + type = string + description = "Mapbox style ID" +} diff --git a/infrastructure/terraform/vars/terraform.tfvars b/infrastructure/terraform/vars/terraform.tfvars new file mode 100644 index 00000000..724b0227 --- /dev/null +++ b/infrastructure/terraform/vars/terraform.tfvars @@ -0,0 +1,10 @@ +aws_profile="" +aws_region="eu-west-3" +project_name="esa" + +github_owner="Vizzuality" +github_repo_name="esa" +github_token="" +mapbox_api_token="" +mapbox_username="" +mapbox_style_id="" \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 449398dd..31beebf9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2664,6 +2664,7 @@ __metadata: "@radix-ui/react-toast": ^1.1.4 "@radix-ui/react-tooltip": ^1.0.6 "@recoiljs/refine": ^0.1.1 + "@socialgouv/matomo-next": ^1.10.0 "@svgr/webpack": ^8.1.0 "@t3-oss/env-nextjs": 0.4.1 "@tailwindcss/typography": 0.5.9 @@ -6841,6 +6842,15 @@ __metadata: languageName: node linkType: hard +"@socialgouv/matomo-next@npm:^1.10.0": + version: 1.10.0 + resolution: "@socialgouv/matomo-next@npm:1.10.0" + peerDependencies: + next: ">= 9.5.5" + checksum: bf3c5c3008677d42d775a6be996c485af65981c53dadd372a60111eaa7faad9b31d5ca71d0b419d7283d304b531c49ac12ca38ad97b28e6b037707bd6098896a + languageName: node + linkType: hard + "@stoplight/better-ajv-errors@npm:1.0.3": version: 1.0.3 resolution: "@stoplight/better-ajv-errors@npm:1.0.3"