diff --git a/.github/actions/build-and-deploy-api/action.yml b/.github/actions/build-and-deploy-api/action.yml index 7d1efcf9..9a3a8520 100644 --- a/.github/actions/build-and-deploy-api/action.yml +++ b/.github/actions/build-and-deploy-api/action.yml @@ -1,19 +1,13 @@ name: 'build-and-deploy-api' -description: 'Builds API Project for Production and Deploys it to a given WA Slot' +description: 'Builds API project for production and deploys it to a given environment' inputs: releaseVersion: required: true description: "Release Version (Commit or Tag)" - slot: + deploymentEnv: required: true - description: "Slot Identifier" - mongoUri: - required: true - description: "Mongo Connection URI" - sentryKey: - required: true - description: "Sentry DSN Key" + description: "Deployment Environment" sentryAuthToken: required: true description: "Sentry Auth Token" @@ -26,6 +20,9 @@ inputs: containerRegistryPassword: required: true description: "Container registry password" + containerTag: + required: true + description: "Container tag" outputs: url: description: "API URL" @@ -42,6 +39,11 @@ runs: registry: ${{ inputs.containerRegistryUrl }} username: ${{ inputs.containerRegistryUsername }} password: ${{ inputs.containerRegistryPassword }} + - name: Create Environment + run: envsubst < apps/api/src/app/environment.template > apps/api/src/app/environment.ts + shell: bash + env: + RELEASE_VERSION: ${{ inputs.releaseVersion }} - name: Build app run: | npx nx build api --prod @@ -55,20 +57,12 @@ runs: context: ./ file: ./apps/api/Dockerfile build-args: | - NODE_VERSION=${{ steps.node-version-check.outputs.node-version}} + NODE_VERSION=${{ steps.node-version-check.outputs.node-version }} push: true tags: | - ghcr.io/kordis-leitstelle/kordis-api:${{ inputs.releaseVersion}} + ${{ inputs.containerTag }} cache-from: type=gha cache-to: type=gha,mode=max - - name: Set environment for deployment - run: envsubst < apps/api/src/.env.template > dist/apps/api/.env - env: - MONGODB_URI: ${{ inputs.mongoUri }} - ENVIRONMENT_NAME: ${{ inputs.slot }} - RELEASE_VERSION: ${{ inputs.releaseVersion }} - SENTRY_KEY: ${{ inputs.sentryKey }} - shell: bash - name: Deploy API id: wa-deployment run: echo "url=placeholder" >> $GITHUB_OUTPUT @@ -80,6 +74,6 @@ runs: SENTRY_ORG: kordis-leitstelle SENTRY_PROJECT: kordis-api with: - environment: ${{ inputs.slot }} + environment: ${{ inputs.deploymentEnv }} version: ${{ inputs.releaseVersion }} sourcemaps: ./dist/apps/api diff --git a/.github/actions/build-and-deploy-spa/action.yml b/.github/actions/build-and-deploy-spa/action.yml index 5d69b9b8..858485e1 100644 --- a/.github/actions/build-and-deploy-spa/action.yml +++ b/.github/actions/build-and-deploy-spa/action.yml @@ -1,25 +1,28 @@ name: 'build-and-deploy-spa' -description: 'Builds SPA Project for Production and Deploys it to a given SWA Environment' +description: 'Builds SPA project for production and deploys it to a given environment' inputs: - apiUrl: - required: true - description: "Base URL of the API" - oauthConfig: - required: true - description: "OAuthConfig from the angular-oauth2-oidc package" releaseVersion: required: true description: "Release Version (Commit or Tag)" deploymentEnv: required: true description: "Deployment Environment" - sentryKey: - required: true - description: "Sentry DSN Key" sentryAuthToken: required: true description: "Sentry Auth Token" + containerRegistryUrl: + required: true + description: "Container registry url" + containerRegistryUsername: + required: true + description: "Container registry username" + containerRegistryPassword: + required: true + description: "Container registry password" + containerTag: + required: true + description: "Container tag" outputs: url: description: "SPA URL" @@ -28,25 +31,38 @@ outputs: runs: using: "composite" steps: - - name: Generate Third-Party Licenses - run: npx --yes generate-license-file@3.0.0-beta.1 --input package.json --output apps/spa/src/assets/third-party-licenses.txt --ci - shell: bash - - run: | - envsubst < apps/spa/src/environments/environment.template > apps/spa/src/environments/environment.prod.ts - npx nx build spa --prod + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ inputs.containerRegistryUrl }} + username: ${{ inputs.containerRegistryUsername }} + password: ${{ inputs.containerRegistryPassword }} + - run: envsubst < apps/spa/src/environments/environment.template > apps/spa/src/environments/environment.ts shell: bash env: IS_PRODUCTION: true - ENVIRONMENT_NAME: ${{ inputs.deploymentEnv }} - API_URL: ${{ inputs.apiUrl }} - OAUTH_CONFIG: ${{ inputs.oauthConfig }} RELEASE_VERSION: ${{ inputs.releaseVersion }} - SENTRY_KEY: ${{ inputs.sentryKey }} + - run: | + npx nx build spa --prod + npx --yes generate-license-file@3.0.1 --input package.json --output dist/apps/spa/browser/assets/third-party-licenses.txt --ci + shell: bash + - name: Build and push image + uses: docker/build-push-action@v5 + with: + context: ./ + file: ./apps/spa/docker/Dockerfile + push: true + tags: | + ${{ inputs.containerTag }} + cache-from: type=gha + cache-to: type=gha,mode=max - name: Deploy SPA id: spa-deployment shell: bash run: echo "url=placeholder" >> $GITHUB_OUTPUT - - name: Build SPA with source maps + - name: Build SPA with source maps for sentry run: npx nx build spa --prod --source-map=true shell: bash - name: Create Sentry release diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 080ec578..9a5baf04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: with: fetch-depth: 0 - name: Ensure Conventional Commits - uses: webiny/action-conventional-commits@v1.2.0 + uses: webiny/action-conventional-commits@v1.3.0 - name: Setup node uses: actions/setup-node@v4 with: @@ -34,33 +34,42 @@ jobs: - name: Lint run: npx nx affected --target=lint --parallel=3 - - name: Run Tests with Code Coverage - run: npx nx affected --target=test --parallel=3 --ci --coverage --coverageReporters=lcov + - name: Run all tests + if: github.event_name == 'push' + run: npx nx run-many --all --target=test --parallel --ci --coverage --coverageReporters=lcov + - name: Run affected tests + if: github.event_name == 'pull_request' + run: npx nx affected --target=test --parallel --ci --coverage --coverageReporters=lcov - name: Merge Coverage files run: '[ -d "./coverage/" ] && ./node_modules/.bin/lcov-result-merger ./coverage/**/lcov.info ./coverage/lcov.info || exit 0' - - name: Create SPA Environment File - run: envsubst < apps/spa/src/environments/environment.template > apps/spa/src/environments/environment.prod.ts + - name: Create Environments + run: | + envsubst < apps/spa/src/environments/environment.template > apps/spa/src/environments/environment.ts + envsubst < apps/api/src/app/environment.template > apps/api/src/app/environment.ts env: IS_PRODUCTION: true ENVIRONMENT_NAME: 'ci' RELEASE_VERSION: ${{ github.sha }} - API_URL: http://localhost:3000/ - OAUTH_CONFIG: undefined - name: Build - run: npx nx run-many -t build --all --parallel=3 + run: | + npx nx run-many -t build --all --parallel=3 + docker build -t kordis-api:${{ github.sha }} -f ./apps/api/Dockerfile --build-arg NODE_VERSION=$(cat .nvmrc | tr -cd '[:digit:].') . & + docker build -t kordis-spa:${{ github.sha }} -f ./apps/spa/docker/Dockerfile . & + wait + - name: Install Chromium for E2Es + run: npx -y playwright install chromium - name: Start and prepare MongoDB for E2Es run: ./tools/db/kordis-db.sh init e2edb + - name: Start API and SPA containers + run: | + docker run -d -p 3000:3333 -e MONGODB_URI=mongodb://host.docker.internal:27017/e2edb kordis-api:${{ github.sha }} + docker run -d -p 4200:8080 -e API_URL=http://localhost:3000 kordis-spa:${{ github.sha }} - name: Run E2Es - run: npm run serve:all:prod & (npx wait-on tcp:3000 && npx wait-on http://localhost:4200 && npx nx e2e spa-e2e) + run: npx wait-on -t 30s tcp:3000 && npx wait-on -t 30s http://localhost:4200 && npx nx e2e spa-e2e --skipInstall env: E2E_BASE_URL: http://localhost:4200/ - MONGODB_URI: mongodb://127.0.0.1:27017/e2edb - ENVIRONMENT_NAME: 'ci' - RELEASE_VERSION: ${{ github.sha }} - SENTRY_KEY: ${{ secrets.SENTRY_KEY }} - PORT: 3000 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: e2e-test-results diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ddcd631c..4df4f77d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,11 +27,11 @@ jobs: uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/next-deployment.yml b/.github/workflows/next-deployment.yml index ea89e308..b2719f8a 100644 --- a/.github/workflows/next-deployment.yml +++ b/.github/workflows/next-deployment.yml @@ -29,14 +29,13 @@ jobs: id: api-deployment uses: ./.github/actions/build-and-deploy-api with: - slot: "next" + deploymentEnv: "next" releaseVersion: ${{ github.sha }} - mongoUri: ${{ secrets.DEV_MONGODB_URI }} - sentryKey: ${{ secrets.API_SENTRY_KEY }} sentryAuthToken: ${{ secrets.SENTRY_AUTH_TOKEN }} containerRegistryUrl: ghcr.io containerRegistryUsername: ${{ github.actor }} containerRegistryPassword: ${{ secrets.GITHUB_TOKEN }} + containerTag: ghcr.io/kordis-leitstelle/kordis-api:${{ github.sha }} - name: Apply Database Migrations run: echo "add again once infrastructure is set up" # ./tools/db/kordis-db.sh apply-pending-migrations env: @@ -47,12 +46,13 @@ jobs: id: spa-deployment uses: ./.github/actions/build-and-deploy-spa with: - apiUrl: ${{ steps.api-deployment.outputs.url }} - oauthConfig: ${{ secrets.DEV_OAUTH_CONFIG }} releaseVersion: ${{ github.sha }} deploymentEnv: "next" - sentryKey: ${{ secrets.SPA_SENTRY_KEY }} sentryAuthToken: ${{ secrets.SENTRY_AUTH_TOKEN }} + containerRegistryUrl: ghcr.io + containerRegistryUsername: ${{ github.actor }} + containerRegistryPassword: ${{ secrets.GITHUB_TOKEN }} + containerTag: ghcr.io/kordis-leitstelle/kordis-spa:${{ github.sha }} e2e: needs: deployment @@ -69,7 +69,7 @@ jobs: env: E2E_BASE_URL: ${{ needs.deployment.outputs.spaUrl }} AADB2C_TEST_USERS: ${{ secrets.E2E_TEST_USERS }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: e2e-test-results diff --git a/.github/workflows/nx-migration-checker.yml b/.github/workflows/nx-migration-checker.yml index fe4dfcbd..6ab56567 100644 --- a/.github/workflows/nx-migration-checker.yml +++ b/.github/workflows/nx-migration-checker.yml @@ -17,7 +17,7 @@ jobs: node-version-file: '.nvmrc' cache: 'npm' - run: npm ci - - uses: timonmasberg/nx-migration-gh-action@v1.1.8 + - uses: timonmasberg/nx-migration-gh-action@v1.1.12 with: repoToken: ${{ secrets.WORKFLOW_PAT }} prTitle: 'chore(deps): migrate nx to $VERSION' diff --git a/.github/workflows/preview-deployment.yml b/.github/workflows/preview-deployment.yml index c9d27501..2f6bbef8 100644 --- a/.github/workflows/preview-deployment.yml +++ b/.github/workflows/preview-deployment.yml @@ -69,7 +69,7 @@ jobs: cache: 'npm' - run: npm ci --ignore-scripts - name: Initial Deployment Preview Comment - uses: peter-evans/create-or-update-comment@v3.1.0 + uses: peter-evans/create-or-update-comment@v4.0.0 id: pr-preview-comment with: issue-number: ${{ github.event.issue.number }} @@ -81,25 +81,27 @@ jobs: id: api-deployment uses: ./.github/actions/build-and-deploy-api with: - slot: "pr${{ github.event.issue.number }}" + deploymentEnv: "pr${{ github.event.issue.number }}" releaseVersion: ${{ steps.set-pr-sha.outputs.head_sha }} - sentryKey: ${{ secrets.API_SENTRY_KEY }} sentryAuthToken: ${{ secrets.SENTRY_AUTH_TOKEN }} containerRegistryUrl: ghcr.io containerRegistryUsername: ${{ github.actor }} containerRegistryPassword: ${{ secrets.GITHUB_TOKEN }} + containerTag: ghcr.io/kordis-leitstelle/kordis-api:${{ steps.set-pr-sha.outputs.head_sha }} + - name: Build and Deploy SPA id: spa-deployment uses: ./.github/actions/build-and-deploy-spa with: - apiUrl: ${{ steps.api-deployment.outputs.url }} - oauthConfig: ${{ secrets.DEV_OAUTH_CONFIG }} releaseVersion: ${{ steps.set-pr-sha.outputs.head_sha }} deploymentEnv: "pr${{ github.event.issue.number }}" - sentryKey: ${{ secrets.SPA_SENTRY_KEY }} sentryAuthToken: ${{ secrets.SENTRY_AUTH_TOKEN }} + containerRegistryUrl: ghcr.io + containerRegistryUsername: ${{ github.actor }} + containerRegistryPassword: ${{ secrets.GITHUB_TOKEN }} + containerTag: ghcr.io/kordis-leitstelle/kordis-spa:${{ steps.set-pr-sha.outputs.head_sha }} - name: Update PR Preview Comment - uses: peter-evans/create-or-update-comment@v3.1.0 + uses: peter-evans/create-or-update-comment@v4.0.0 with: comment-id: ${{ steps.pr-preview-comment.outputs.comment-id }} edit-mode: replace @@ -110,7 +112,7 @@ jobs: Commit SHA: ${{ steps.set-pr-sha.outputs.head_sha }} reactions: "rocket" - name: AZ B2C Tenant Login - uses: azure/login@v1.5.0 + uses: azure/login@v1.6.1 with: creds: '${{ secrets.AZURE_AADB2C_CREDENTIALS }}' allow-no-subscriptions: true @@ -143,7 +145,7 @@ jobs: cache: 'npm' - run: npm ci --ignore-scripts - name: Find PR Preview Comment - uses: peter-evans/find-comment@v2 + uses: peter-evans/find-comment@v3 id: deploy-preview-comment with: issue-number: ${{ github.event.pull_request.number }} @@ -151,7 +153,7 @@ jobs: body-includes: Deployment Preview - name: Update PR Preview Comment if: steps.deploy-preview-comment.outputs.comment-id != '' - uses: peter-evans/create-or-update-comment@v3.1.0 + uses: peter-evans/create-or-update-comment@v4.0.0 with: comment-id: ${{ steps.deploy-preview-comment.outputs.comment-id }} edit-mode: replace @@ -163,25 +165,27 @@ jobs: id: api-deployment uses: ./.github/actions/build-and-deploy-api with: - slot: "pr${{ github.event.pull_request.number }}" + deploymentEnv: "pr${{ github.event.pull_request.number }}" releaseVersion: ${{ github.event.pull_request.head.sha }} - sentryKey: ${{ secrets.API_SENTRY_KEY }} sentryAuthToken: ${{ secrets.SENTRY_AUTH_TOKEN }} containerRegistryUrl: ghcr.io containerRegistryUsername: ${{ github.actor }} containerRegistryPassword: ${{ secrets.GITHUB_TOKEN }} + containerTag: ghcr.io/kordis-leitstelle/kordis-api:${{ github.event.pull_request.head.sha }} - name: Build and Deploy SPA id: spa-deployment uses: ./.github/actions/build-and-deploy-spa with: - apiUrl: ${{ steps.api-deployment.outputs.url }} - oauthConfig: ${{ secrets.DEV_OAUTH_CONFIG }} - releaseVersion: ${{ github.event.pull_request.head.sha }} + releaseVersion: ${{ github.event.pull_request.head.sha }} deploymentEnv: "pr${{ github.event.pull_request.number }}" - sentryKey: ${{ secrets.SPA_SENTRY_KEY }} sentryAuthToken: ${{ secrets.SENTRY_AUTH_TOKEN }} + containerRegistryUrl: ghcr.io + containerRegistryUsername: ${{ github.actor }} + containerRegistryPassword: ${{ secrets.GITHUB_TOKEN }} + containerTag: ghcr.io/kordis-leitstelle/kordis-spa:${{ github.event.pull_request.head.sha }} + - name: Update PR Preview Comment - uses: peter-evans/create-or-update-comment@v3.1.0 + uses: peter-evans/create-or-update-comment@v4.0.0 with: comment-id: ${{ steps.deploy-preview-comment.outputs.comment-id }} edit-mode: replace @@ -200,7 +204,7 @@ jobs: (needs.has-deployment.outputs.has-swa == 'true' || needs.has-deployment.outputs.has-wa == 'true') && false steps: - name: Find PR Preview Comment - uses: peter-evans/find-comment@v2 + uses: peter-evans/find-comment@v3 id: deploy-preview-comment with: issue-number: ${{ github.event.pull_request.number }} @@ -208,7 +212,7 @@ jobs: body-includes: Deployment Preview - name: Update PR Preview Comment if: steps.deploy-preview-comment.outputs.comment-id != '' - uses: peter-evans/create-or-update-comment@v3.1.0 + uses: peter-evans/create-or-update-comment@v4.0.0 with: comment-id: ${{ steps.deploy-preview-comment.outputs.comment-id }} edit-mode: replace @@ -217,7 +221,7 @@ jobs: 🏁 This PR has been closed. No deployment preview is available. reactions: "hooray" - name: AZ B2C Tenant Login - uses: azure/login@v1.5.0 + uses: azure/login@v1.6.1 with: creds: '${{ secrets.AZURE_AADB2C_CREDENTIALS }}' allow-no-subscriptions: true diff --git a/.gitignore b/.gitignore index 419a1d55..923e59f4 100644 --- a/.gitignore +++ b/.gitignore @@ -45,8 +45,7 @@ Thumbs.db playwright/.auth # Environments -apps/api/src/.env +apps/api/.env apps/spa-e2e/.env -apps/spa/src/environments/environment.prod.ts -.nx/cache \ No newline at end of file +.nx/cache diff --git a/.prettierignore b/.prettierignore index 41bf6f4e..e2388480 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,7 +8,9 @@ package-lock.json .angular apps/api/src/main.ts +apps/spa/src/assets/config.template.json +libs/spa/observability/src/lib/services/sentry-observability.service.spec.ts /.nx/cache -migrations.json \ No newline at end of file +migrations.json diff --git a/.prettierrc b/.prettierrc index 99afd194..091d14c8 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,15 +6,10 @@ "proseWrap": "always", "overrides": [ { - "files": [ - "*.ts" - ], + "files": ["*.ts"], "options": { "parser": "typescript", - "importOrder": [ - "^@kordis/(.*)$", - "^[./]" - ], + "importOrder": ["^@kordis/(.*)$", "^[./]"], "importOrderSeparation": true, "importOrderSortSpecifiers": true, "importOrderParserPlugins": [ @@ -25,9 +20,5 @@ } } ], - "plugins": [ - "prettier-plugin-tailwindcss", - "@trivago/prettier-plugin-sort-imports" - ], - "tailwindConfig": "./apps/spa/tailwind.config.js" + "plugins": ["@trivago/prettier-plugin-sort-imports"] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd942c16..ec7b0a2b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,6 +73,11 @@ Please try to stick to this as much as possible. You can serve the SPA with you will get redirected to a login page where you can choose a persona. This will set a JWT in the local storage. +We agreed on supporting only Chromium based browsers with their 2 latest major +versions. This allows us to use features that are available for these browsers +regardless of their support in other browsers. Therefore, please use Chrome or +Edge for development. + ### API The API is a NestJS application serving a GraphQL API. Please use the NX CLI to @@ -84,7 +89,7 @@ information about the folder and code structure please also read the [architecture documentation](docs/architecture.md). Before you can run the API application, you need to create a .env file from the -[.env.example](apps/api/src/.env.template) file. There you have to specify the +[.env.example](apps/api/.env.template) file. There you have to specify the MongoDB connection URI and more configurations. You can ignore some values as they are only used in production (they have a comment to clarify this). If you want to test the API directly without the SPA, you can use one of the diff --git a/README.md b/README.md index d15a8af0..92d7da40 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
-

Kordis

+

Kordis

Koordinierungssoftware für Einsatzleitstellen/ Software for rescue operations control centres @@ -9,6 +9,7 @@ control centres [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=kordis-leitstelle&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=kordis-leitstelle) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=kordis-leitstelle&metric=coverage)](https://sonarcloud.io/summary/new_code?id=kordis-leitstelle) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=kordis-leitstelle&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=kordis-leitstelle) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fkordis-leitstelle%2Fkordis.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fkordis-leitstelle%2Fkordis?ref=badge_shield)
diff --git a/apps/api/.env.template b/apps/api/.env.template new file mode 100644 index 00000000..8d29b197 --- /dev/null +++ b/apps/api/.env.template @@ -0,0 +1,7 @@ +MONGODB_URI=$MONGODB_URI +ENVIRONMENT_NAME=$ENVIRONMENT_NAME +SENTRY_KEY=$SENTRY_KEY# Prod +AADB2C_TENANT_NAME=$AADB2C_TENANT_NAME# Prod +AADB2C_SIGN_IN_POLICY=$AADB2C_SIGN_IN_POLICY# Prod +AADB2C_CLIENT_ID=$AADB2C_CLIENT_ID# Prod +AADB2C_ISSUER=$AADB2C_ISSUER# Prod diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 5df6f546..0ec9427f 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -4,20 +4,23 @@ FROM docker.io/node:${NODE_VERSION}-alpine AS builder WORKDIR /app +ENV NODE_ENV=production + # Install dependencies separately for caching COPY ./dist/apps/api/package.json ./dist/apps/api/package-lock.json ./ -RUN npm --omit=dev -f install +RUN npm --omit=dev ci COPY ./dist/apps/api ./ # Use distroless for maximum security: https://github.com/GoogleContainerTools/distroless -FROM gcr.io/distroless/nodejs${NODE_VERSION}-debian11 +FROM gcr.io/distroless/nodejs${NODE_VERSION}-debian12:nonroot -COPY --from=builder /app /app +COPY --chown=root:root --chmod=655 --from=builder /app /app WORKDIR /app ENV PORT=3333 -EXPOSE ${PORT} +ENV NODE_ENV=production +EXPOSE ${PORT} -CMD ["./main.js"] \ No newline at end of file +CMD ["./main.js"] diff --git a/apps/api/project.json b/apps/api/project.json index 4d011b58..9aa015c5 100644 --- a/apps/api/project.json +++ b/apps/api/project.json @@ -40,10 +40,7 @@ }, "lint": { "executor": "@nx/eslint:lint", - "outputs": ["{options.outputFile}"], - "options": { - "lintFilePatterns": ["apps/api/**/*.ts"] - } + "outputs": ["{options.outputFile}"] }, "test": { "executor": "@nx/jest:jest", diff --git a/apps/api/src/.env.template b/apps/api/src/.env.template deleted file mode 100644 index 09149cc2..00000000 --- a/apps/api/src/.env.template +++ /dev/null @@ -1,4 +0,0 @@ -MONGODB_URI=$MONGODB_URI -ENVIRONMENT_NAME=$ENVIRONMENT_NAME# Prod -RELEASE_VERSION=$RELEASE_VERSION# Prod -SENTRY_KEY=$SENTRY_KEY# Prod diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 2b8a5053..006b4e4e 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -1,31 +1,32 @@ import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import { MongooseModule } from '@nestjs/mongoose'; -import { AutomapperModule } from '@timonmasberg/automapper-nestjs'; import * as path from 'path'; import { AuthModule } from '@kordis/api/auth'; -import { - DevObservabilityModule, - SentryObservabilityModule, -} from '@kordis/api/observability'; +import { ObservabilityModule } from '@kordis/api/observability'; import { OrganizationModule } from '@kordis/api/organization'; import { SharedKernel, errorFormatterFactory } from '@kordis/api/shared'; import { AppResolver } from './app.resolver'; import { AppService } from './app.service'; import { GraphqlSubscriptionsController } from './controllers/graphql-subscriptions.controller'; +import { HealthCheckController } from './controllers/health-check.controller'; +import environment from './environment'; + +const isNextOrProdEnv = ['next', 'prod'].includes( + process.env.ENVIRONMENT_NAME ?? '', +); const FEATURE_MODULES = [OrganizationModule]; const UTILITY_MODULES = [ SharedKernel, - AuthModule, - ...(process.env.NODE_ENV === 'production' && !process.env.GITHUB_ACTIONS - ? [SentryObservabilityModule] - : [DevObservabilityModule]), + AuthModule.forRoot(isNextOrProdEnv ? 'aadb2c' : 'dev'), + ObservabilityModule.forRoot(isNextOrProdEnv ? 'sentry' : 'dev'), ]; @Module({ @@ -34,6 +35,7 @@ const UTILITY_MODULES = [ isGlobal: true, cache: true, envFilePath: path.resolve(__dirname, '.env'), + load: [environment], }), GraphQLModule.forRootAsync({ imports: [ConfigModule], @@ -67,6 +69,6 @@ const UTILITY_MODULES = [ ...FEATURE_MODULES, ], providers: [AppService, AppResolver], - controllers: [GraphqlSubscriptionsController], + controllers: [GraphqlSubscriptionsController, HealthCheckController], }) export class AppModule {} diff --git a/apps/api/src/app/controllers/health-check.controller.spec.ts b/apps/api/src/app/controllers/health-check.controller.spec.ts new file mode 100644 index 00000000..ac838137 --- /dev/null +++ b/apps/api/src/app/controllers/health-check.controller.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { HealthCheckController } from './health-check.controller'; + +describe('HealthCheckController', () => { + let controller: HealthCheckController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [HealthCheckController], + }).compile(); + + controller = module.get(HealthCheckController); + }); + + it('should return a resolved promise for the healthCheck method', async () => { + await expect(controller.healthCheck()).resolves.toBeUndefined(); + }); +}); diff --git a/apps/api/src/app/controllers/health-check.controller.ts b/apps/api/src/app/controllers/health-check.controller.ts new file mode 100644 index 00000000..0c710b0d --- /dev/null +++ b/apps/api/src/app/controllers/health-check.controller.ts @@ -0,0 +1,10 @@ +import { All, Controller, HttpCode } from '@nestjs/common'; + +@Controller('health-check') +export class HealthCheckController { + @All() + @HttpCode(200) + healthCheck(): Promise { + return Promise.resolve(); + } +} diff --git a/apps/api/src/app/environment.template b/apps/api/src/app/environment.template new file mode 100644 index 00000000..e2802f44 --- /dev/null +++ b/apps/api/src/app/environment.template @@ -0,0 +1,3 @@ +export default () => ({ + RELEASE_VERSION: '$RELEASE_VERSION', +}); diff --git a/apps/api/src/app/environment.ts b/apps/api/src/app/environment.ts new file mode 100644 index 00000000..db951de3 --- /dev/null +++ b/apps/api/src/app/environment.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export default () => ({ + RELEASE_VERSION: '0.0.0-development', +}); diff --git a/apps/spa-e2e/project.json b/apps/spa-e2e/project.json index a6577b88..fc11a922 100644 --- a/apps/spa-e2e/project.json +++ b/apps/spa-e2e/project.json @@ -23,10 +23,7 @@ }, "lint": { "executor": "@nx/eslint:lint", - "outputs": ["{options.outputFile}"], - "options": { - "lintFilePatterns": ["apps/spa-e2e/**/*.{ts,tsx,js,jsx}"] - } + "outputs": ["{options.outputFile}"] } }, "tags": [], diff --git a/apps/spa/.browserslistrc b/apps/spa/.browserslistrc new file mode 100644 index 00000000..1fff758b --- /dev/null +++ b/apps/spa/.browserslistrc @@ -0,0 +1,2 @@ +last 2 Chrome major version +last 2 Edge major versions diff --git a/apps/spa/.eslintrc.json b/apps/spa/.eslintrc.json index 9aa37450..5017aebf 100644 --- a/apps/spa/.eslintrc.json +++ b/apps/spa/.eslintrc.json @@ -31,6 +31,12 @@ "files": ["*.html"], "extends": ["plugin:@nx/angular-template"], "rules": {} + }, + { + "files": ["*.d.ts"], + "rules": { + "no-var": "off" + } } ] } diff --git a/apps/spa/docker/Dockerfile b/apps/spa/docker/Dockerfile new file mode 100644 index 00000000..697135e2 --- /dev/null +++ b/apps/spa/docker/Dockerfile @@ -0,0 +1,17 @@ +FROM nginxinc/nginx-unprivileged:1.24.0 + +WORKDIR /usr/share/nginx/html + +COPY ./apps/spa/docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY ./dist/apps/spa/browser ./ +# COPY needed, since native permissions altering wont work due to user restrictions by image +COPY --chown=nginx:nginx ./dist/apps/spa/browser/assets/config.template.json ./assets/config.template.json +COPY --chown=nginx:nginx ./dist/apps/spa/browser/assets/config.json ./assets/config.json +COPY --chmod=704 ./apps/spa/docker/docker-entrypoint.sh /usr/local/bin/docker-entrypoint + +EXPOSE 8080 + +ENTRYPOINT ["docker-entrypoint"] + +CMD ["nginx", "-g", "daemon off;"] + diff --git a/apps/spa/docker/docker-entrypoint.sh b/apps/spa/docker/docker-entrypoint.sh new file mode 100644 index 00000000..5ed473b3 --- /dev/null +++ b/apps/spa/docker/docker-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +set -euo + +echo "Setting runtime environment from env vars" + +# Check if OAuth is set, otherwise set it to null +export OAUTH_CONFIG="${OAUTH_CONFIG:-null}" + +envsubst assets/config.json + +exec "$@" diff --git a/apps/spa/docker/nginx.conf b/apps/spa/docker/nginx.conf new file mode 100644 index 00000000..a0e91fc8 --- /dev/null +++ b/apps/spa/docker/nginx.conf @@ -0,0 +1,46 @@ +server { + listen 8080; + + server_name _; + server_tokens off; + + index index.html; + root /usr/share/nginx/html; + + # change nonce in the apps index.html + sub_filter_once off; + sub_filter csp_nonce $request_id; + + # add CSP and further security headers + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'; object-src 'none'; base-uri 'self'; connect-src 'self'; img-src 'self'; style-src 'self' 'nonce-$request_id'; font-src 'self'; frame-ancestors 'self'; trusted-types angular angular#bundler dompurify default; require-trusted-types-for 'script';" always; + add_header Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # enable gzip compression + gzip on; + gzip_comp_level 5; + gzip_min_length 1100; + gzip_buffers 4 32k; + gzip_proxied any; + gzip_types + application/javascript + application/json + application/x-javascript + application/xml + image/svg+xml + text/css + text/javascript + text/js + text/plain + text/xml; + gzip_vary on; + + location / { + real_ip_header X-Forwarded-For; + set_real_ip_from 10.0.0.0/8; + + try_files $uri $uri/ /index.html; + } +} diff --git a/apps/spa/jest.config.ts b/apps/spa/jest.config.ts index a25dc2c8..0c985741 100644 --- a/apps/spa/jest.config.ts +++ b/apps/spa/jest.config.ts @@ -1,4 +1,5 @@ /* eslint-disable */ + export default { displayName: 'spa', preset: '../../jest.preset.js', diff --git a/apps/spa/project.json b/apps/spa/project.json index 1e9fa75c..40abd385 100644 --- a/apps/spa/project.json +++ b/apps/spa/project.json @@ -6,38 +6,35 @@ "prefix": "kordis", "targets": { "build": { - "executor": "@angular-devkit/build-angular:browser", + "executor": "@angular-devkit/build-angular:application", "outputs": ["{options.outputPath}"], "options": { "outputPath": "dist/apps/spa", "index": "apps/spa/src/index.html", - "main": "apps/spa/src/main.ts", + "browser": "apps/spa/src/main.ts", "polyfills": ["zone.js"], "tsConfig": "apps/spa/tsconfig.app.json", "inlineStyleLanguage": "css", "assets": [ "apps/spa/src/favicon.ico", - "apps/spa/src/assets", - "apps/spa/src/manifest.webmanifest" + "apps/spa/src/manifest.webmanifest", + "apps/spa/src/assets" + ], + "styles": [ + "./node_modules/ng-zorro-antd/ng-zorro-antd.min.css", + "apps/spa/src/styles.css" ], - "styles": ["apps/spa/src/styles.css"], "scripts": [], "serviceWorker": true, "ngswConfigPath": "apps/spa/ngsw-config.json" }, "configurations": { "production": { - "fileReplacements": [ - { - "replace": "apps/spa/src/environments/environment.ts", - "with": "apps/spa/src/environments/environment.prod.ts" - } - ], "budgets": [ { "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumWarning": "1.5mb", + "maximumError": "2mb" }, { "type": "anyComponentStyle", @@ -48,12 +45,9 @@ "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production" @@ -65,7 +59,10 @@ "buildTarget": "spa:build:production" }, "development": { - "buildTarget": "spa:build:development" + "buildTarget": "spa:build:development", + "headers": { + "Content-Security-Policy": "default-src 'self'; script-src 'self' 'nonce-csp_nonce'; object-src 'none'; base-uri 'self'; connect-src 'self'; img-src 'self'; style-src 'self' 'nonce-csp_nonce'; font-src 'self'; frame-ancestors 'self'; trusted-types angular angular#bundler dompurify default; require-trusted-types-for 'script';" + } } }, "defaultConfiguration": "development" @@ -78,10 +75,7 @@ }, "lint": { "executor": "@nx/eslint:lint", - "outputs": ["{options.outputFile}"], - "options": { - "lintFilePatterns": ["apps/spa/**/*.ts", "apps/spa/**/*.html"] - } + "outputs": ["{options.outputFile}"] }, "test": { "executor": "@nx/jest:jest", diff --git a/apps/spa/src/app/app.module.ts b/apps/spa/src/app/app.module.ts index a5774ede..24e309b7 100644 --- a/apps/spa/src/app/app.module.ts +++ b/apps/spa/src/app/app.module.ts @@ -1,8 +1,12 @@ +import { registerLocaleData } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; -import { NgModule, isDevMode } from '@angular/core'; +import de from '@angular/common/locales/de'; +import { APP_INITIALIZER, NgModule, isDevMode } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { ServiceWorkerModule } from '@angular/service-worker'; +import DOMPurify from 'dompurify'; +import { NZ_I18N, de_DE } from 'ng-zorro-antd/i18n'; import { AuthModule, DevAuthModule } from '@kordis/spa/auth'; import { @@ -15,17 +19,19 @@ import { AppComponent } from './component/app.component'; import { ProtectedComponent } from './component/protected.component'; import routes from './routes'; +registerLocaleData(de); + @NgModule({ declarations: [AppComponent, ProtectedComponent], imports: [ BrowserModule, - HttpClientModule, RouterModule.forRoot(routes), + HttpClientModule, environment.oauth ? AuthModule.forRoot( environment.oauth.config, environment.oauth.discoveryDocumentUrl, - ) + ) : DevAuthModule.forRoot(), // for now, we accept that we have the sentry module and dependencies in our dev bundle as well environment.sentryKey @@ -33,7 +39,7 @@ import routes from './routes'; environment.sentryKey, environment.environmentName, environment.releaseVersion, - ) + ) : NoopObservabilityModule.forRoot(), ServiceWorkerModule.register('ngsw-worker.js', { enabled: !isDevMode(), @@ -42,7 +48,29 @@ import routes from './routes'; registrationStrategy: 'registerWhenStable:30000', }), ], - providers: [], + providers: [ + { + provide: APP_INITIALIZER, + // we need to use a global tt policy here mainly for the ant design icons + useFactory: () => () => { + globalThis.trustedTypes.createPolicy('default', { + // https://github.com/angular/angular/issues/31329 can't use Angular DomSanitizer here + createHTML: (s) => { + return DOMPurify.sanitize( + s + // hack so chrome won't complain about inline styles (mainly from svg icons) + .replace('