diff --git a/.editorconfig b/.editorconfig index 71cd14e9..aa5b9729 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,7 @@ indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true +end_of_line = lf [*.md] max_line_length = off diff --git a/.github/ISSUE_TEMPLATE/1-issue.md b/.github/ISSUE_TEMPLATE/1-issue.md new file mode 100644 index 00000000..b22f1e61 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-issue.md @@ -0,0 +1,23 @@ +--- +name: Issue +about: Beschreibe eine neue Aufgabe +title: '' +labels: '' +assignees: '' + +--- + +**Beschreibung** + +Eine klare und präzise Beschreibung der Ausgangssituation, der Problematik sowie bereits bekannter Lösungsansätze. + + + +**Abgrenzung** + +Nicht beachtet in dieser Aufgabe wird die Suche nach der Frage. + +**Acceptance Criteria** + + - [ ] Die Antwort muss 42 sein. + - [ ] ... diff --git a/.github/ISSUE_TEMPLATE/2-bug.md b/.github/ISSUE_TEMPLATE/2-bug.md new file mode 100644 index 00000000..21936a17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-bug.md @@ -0,0 +1,33 @@ +--- +name: Bug +about: Melde einen neuen Bug +title: 'Bug: ' +labels: bug +assignees: '' + +--- + +**Beschreibung** +Eine klare und präzise Beschreibung des Problems. + +**Schritte zum Reproduzieren** +1. Gehe zur Seite '....' +2. Klicke auf '....' +3. Scrolle zu '....' +4. Ein Fehler erscheint + +**Beobachtetes Verhalten** +Eine Beschreibung des Fehlverhaltens. + +**Erwartetes Verhalten** +Eine Beschreibung des eigentlich erwarteten Verhaltens + +**Screenshots** +Zeige Screenshots, wenn vorhanden und hilfreich. + +**Plattform** +Falls relevant: + - Device: [e.g. Desktop, iPhone6] + - OS: [e.g. Windows, iOS8.1] + - Browser [e.g. firefox, edge] + - Mobile Version [e.g. 22] diff --git a/.github/actions/create-image/action.yaml b/.github/actions/create-image/action.yaml new file mode 100644 index 00000000..62955900 --- /dev/null +++ b/.github/actions/create-image/action.yaml @@ -0,0 +1,57 @@ +name: 'create docker image' +description: 'Builds a docker image and tags it' +inputs: + IMAGE_NAME: + description: 'The image name' + required: true + TAG: + description: 'The version of the image' + required: true + DOCKERFILE: + description: 'The path to the Dockerfile' + required: true + GITHUB_TOKEN: + description: 'The github token' + required: true + +runs: + using: 'composite' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set environment variables + shell: bash + run: | + echo COMMITED_AT=$(git show -s --format=%cI ${{ github.sha }}) >> $GITHUB_ENV + echo REVISION=$(git rev-parse --short HEAD) >> $GITHUB_ENV + + - name: Collect docker image metadata + id: meta-data + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.IMAGE_NAME }} + labels: | + org.opencontainers.image.created=${{ env.COMMITED_AT }} + org.opencontainers.image.maintainer=EBP Schweiz AG + flavor: | + latest=false + tags: | + ${{ inputs.TAG }} + + - name: Log in to the GitHub container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ inputs.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./ + file: ${{ inputs.DOCKERFILE }} + push: true + tags: ${{ steps.meta-data.outputs.tags }} + labels: ${{ steps.meta-data.outputs.labels }} + no-cache: true diff --git a/.github/actions/deploy/action.yaml b/.github/actions/deploy/action.yaml deleted file mode 100644 index 30d4f921..00000000 --- a/.github/actions/deploy/action.yaml +++ /dev/null @@ -1,84 +0,0 @@ -name: "Pulumi deployment to cluster" -description: "Starts a pulumi deployment to the cluster" -inputs: - # secrets for login etc - pulumiConfigSecret: - description: "Pulumi config secret" - required: true - npmPkgToken: - description: "token for npm pkg access" - required: true - awsAccessKeyId: - description: "AWS access key id" - required: true - awsSecretAccessKey: - description: "AWS secret access key" - required: true - eksClusterName: - description: "AWS EKS cluster name" - required: true - eksRoleArn: - description: "AWS EKS Role ARN" - required: true - # variables for pulumi setup - pulumiConfigPassphrase: - description: "Pulumi config passphrase" - required: true - stack: - description: "Pulumi stack name" - required: true - command: - description: "Pulumi command to run" - required: false - default: "preview" - region: - description: "AWS region" - required: false - default: "eu-central-1" - s3Bucket: - description: "S3 bucket for pulumi state" - required: false - default: "swissgeol-assets-swisstopo" - version: - description: "App version for assets" - required: false - default: "dev" -runs: - using: "composite" - steps: - # https://github.com/marketplace/actions/configure-aws-credentials-for-github-actions - - name: configure aws credentials - uses: aws-actions/configure-aws-credentials@v2 - with: - aws-access-key-id: ${{ inputs.awsAccessKeyId }} - aws-secret-access-key: ${{ inputs.awsSecretAccessKey }} - aws-region: ${{ inputs.region }} - - - name: Create kubeconfig - shell: bash - working-directory: ./deployment - run: | - aws --region ${{ inputs.region }} eks update-kubeconfig --name ${{ inputs.eksClusterName }} --alias swissgeol-${{ inputs.eksClusterName }} --role-arn ${{ inputs.eksRoleArn }} - kubectl get namespaces - - - name: install pulumi deps - shell: bash - working-directory: ./deployment - run: | - echo "//npm.pkg.github.com/:_authToken=${{ inputs.npmPkgToken }}" >> .npmrc - npm ci - - # https://github.com/marketplace/actions/pulumi-cli-action - - name: execute pulumi - uses: pulumi/actions@v4 - with: - command: ${{ inputs.command }} - stack-name: ${{ inputs.stack }} - cloud-url: "s3://${{ inputs.s3Bucket }}?region=${{ inputs.region }}&awssdk=v2" - work-dir: ./deployment - config-map: "{ geoadmin-swissgeol-asset:version: {value: ${{ inputs.version }}, secret: false }}" - env: - AWS_ACCESS_KEY_ID: ${{ inputs.awsAccessKeyId }} - AWS_SECRET_ACCESS_KEY: ${{ inputs.awsSecretAccessKey }} - AWS_REGION: ${{ inputs.region }} - PULUMI_CONFIG_PASSPHRASE: ${{ inputs.pulumiConfigPassphrase }} diff --git a/.github/actions/generate-version/action.yaml b/.github/actions/generate-version/action.yaml deleted file mode 100644 index 952f52dc..00000000 --- a/.github/actions/generate-version/action.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: "Generate Version for Asset" -description: "Generates a new version from git sha" -inputs: - path: - description: "path for version.json" - required: true -runs: - using: "composite" - steps: - - name: generate version.json - shell: bash - run: | - VERSION="{\"tag\": \"generated_by_build\",\"build\": \"${{github.run_number}}\",\"commit\": \"${{github.sha}}\"}" - echo $VERSION > ${{ inputs.path }}/version.json - cat ${{ inputs.path }}/version.json diff --git a/.github/actions/registry-push/action.yaml b/.github/actions/registry-push/action.yaml deleted file mode 100644 index 4ea0ab86..00000000 --- a/.github/actions/registry-push/action.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: "Container regsistry login & push" -description: "Logins to the registry and pushes the given images" -inputs: - image: - description: "Container image name" - required: true - username: - description: "Username for the registry" - required: true - password: - description: "Password for the registry" - required: true -runs: - using: "composite" - steps: - # https://github.com/marketplace/actions/docker-login - - name: Login to container registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ inputs.username }} - password: ${{ inputs.password }} - - - name: Push container image - shell: bash - run: docker push ${{ inputs.image }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..58c2d110 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,252 @@ +name: build + +on: + push: + branches: + - '**' + workflow_dispatch: + +env: + NODE_VERSION: '20.x' + +jobs: + install: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Cache node modules + uses: actions/cache@v4 + with: + path: ./node_modules + key: "${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}" + restore-keys: | + ${{ runner.os }}-npm- + - name: Install node dependencies + run: npm ci + - name: Run postinstall + run: npm run postinstall + - name: Generate prisma types + run: | + cd apps/server-asset-sg/ + npx ng gen-prisma-client + + test: + runs-on: ubuntu-latest + needs: install + env: + DB_USERNAME: postgres-test + DB_PASSWORD: postgres-test + DB_DATABASE: postgres-test + DATABASE_URL: postgres://postgres-test:postgres-test@localhost:5432/postgres-test?schema=public + services: + db: + image: postgis/postgis + ports: + - '5432:5432' + env: + POSTGRES_USER: ${{ env.DB_USERNAME }} + POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }} + POSTGRES_DB: ${{ env.DB_DATABASE }} + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.12.1 + ports: + - '9200:9200' + env: + ES_JAVA_OPTS: -Xms512m -Xmx512m + xpack.security.enabled: false + discovery.type: single-node + cluster.routing.allocation.disk.threshold_enabled: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Restore cached node modules + uses: actions/cache@v4 + with: + path: ./node_modules + key: "${{ runner.os }}-npm-${{ steps.cache-node-modules.outputs.cache-key }}" + - name: Migrate database + run: | + cd apps/server-asset-sg/ + npx prisma migrate deploy --schema src/app/prisma/schema.prisma + - name: Run tests + run: npm run test + + lint: + runs-on: ubuntu-latest + needs: install + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Restore cached node modules + uses: actions/cache@v4 + with: + path: ./node_modules + key: "${{ runner.os }}-npm-${{ steps.cache-node-modules.outputs.cache-key }}" + - name: Run lint + run: npm run lint + + # It would be cleaner and probably more performant to replace this build step + # with either a non-emitting build or a simple type check. + # We only have `build` available for now, + # since the project is currently split across a multitude of small packages, + # all of which have to specify their own commands. + # (Daniel von Atzigen, 2024-04-12) + build: + runs-on: ubuntu-latest + needs: + - test + - lint + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Restore cached node modules + uses: actions/cache@v4 + with: + path: ./node_modules + key: "${{ runner.os }}-npm-${{ steps.cache-node-modules.outputs.cache-key }}" + - name: Reset nx + run: npx nx reset + - name: Run build + run: npm run build + + build_and_push_app: + name: 'build and push app' + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + needs: + - build + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Create image + uses: ./.github/actions/create-image + with: + IMAGE_NAME: ${{ vars.BASE_IMAGE_NAME }}-app + TAG: edge + DOCKERFILE: ./apps/client-asset-sg/docker/Dockerfile + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build_and_push_api: + name: 'build and push api' + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + needs: + - build + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Create image + uses: ./.github/actions/create-image + with: + IMAGE_NAME: ${{ vars.BASE_IMAGE_NAME }}-api + TAG: edge + DOCKERFILE: ./apps/server-asset-sg/docker/Dockerfile + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + tag_edge_commit: + name: 'tag edge commit' + needs: + - build_and_push_app + - build_and_push_api + runs-on: ubuntu-latest + steps: + - name: Create/update tag + uses: actions/github-script@v7 + with: + script: | + github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'refs/tags/edge', + sha: context.sha + }).catch(err => { + if (err.status !== 422) throw err; + github.rest.git.updateRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'tags/edge', + sha: context.sha + }); + }) + + tag_rc_image_app: + name: tag rc image app + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: + - build + runs-on: ubuntu-latest + steps: + - name: Login to GitHub Packages + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin + + - name: Pull docker image + run: docker pull ${{ vars.BASE_IMAGE_NAME }}-app:edge + + - name: Tag docker image + run: docker tag ${{ vars.BASE_IMAGE_NAME }}-app:edge ${{ vars.BASE_IMAGE_NAME }}-app:release-candidate + + - name: Push docker image + run: docker push ${{ vars.BASE_IMAGE_NAME }}-app:release-candidate + + tag_rc_image_api: + name: tag rc image api + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: + - build + runs-on: ubuntu-latest + steps: + - name: Login to GitHub Packages + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin + + - name: Pull docker image + run: docker pull ${{ vars.BASE_IMAGE_NAME }}-api:edge + + - name: Tag docker image + run: docker tag ${{ vars.BASE_IMAGE_NAME }}-api:edge ${{ vars.BASE_IMAGE_NAME }}-api:release-candidate + + - name: Push docker image + run: docker push ${{ vars.BASE_IMAGE_NAME }}-api:release-candidate + + tag_rc_commit: + name: 'tag rc commit' + needs: + - tag_rc_image_app + - tag_rc_image_api + runs-on: ubuntu-latest + steps: + - name: Create/update tag + uses: actions/github-script@v7 + with: + script: | + github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'refs/tags/release-candidate', + sha: context.sha + }).catch(err => { + if (err.status !== 422) throw err; + github.rest.git.updateRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'tags/release-candidate', + sha: context.sha + }); + }) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..6440b54c --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,38 @@ +name: code-ql + +on: + push: + branches: + - develop + workflow_dispatch: + schedule: + - cron: '35 4 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..1667602b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,115 @@ +name: release + +on: + workflow_dispatch: + inputs: + version: + description: "Version number (e.g. 1.12)" + required: true +jobs: + release_app: + name: release app + needs: + - read_version + runs-on: ubuntu-latest + steps: + - name: Login to GitHub Packages + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin + + - name: Pull docker image + run: docker pull ${{ vars.BASE_IMAGE_NAME }}-app:release-candidate + + - name: Tag docker image + run: | + docker tag ${{ vars.BASE_IMAGE_NAME }}-app:release-candidate ${{ vars.BASE_IMAGE_NAME }}-app:${{ inputs.VERSION }} + docker tag ${{ vars.BASE_IMAGE_NAME }}-app:release-candidate ${{ vars.BASE_IMAGE_NAME }}-app:latest + + - name: Push docker image + run: | + docker push ${{ vars.BASE_IMAGE_NAME }}-app:${{ inputs.VERSION }} + docker push ${{ vars.BASE_IMAGE_NAME }}-app:latest + + release_api: + name: release api + needs: + - read_version + runs-on: ubuntu-latest + steps: + - name: Login to GitHub Packages + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin + + - name: Pull docker image + run: docker pull ${{ vars.BASE_IMAGE_NAME }}-api:release-candidate + + - name: Tag docker image + run: | + docker tag ${{ vars.BASE_IMAGE_NAME }}-api:release-candidate ${{ vars.BASE_IMAGE_NAME }}-api:${{ inputs.VERSION }} + docker tag ${{ vars.BASE_IMAGE_NAME }}-api:release-candidate ${{ vars.BASE_IMAGE_NAME }}-api:latest + + - name: Push docker image + run: | + docker push ${{ vars.BASE_IMAGE_NAME }}-api:${{ inputs.VERSION }} + docker push ${{ vars.BASE_IMAGE_NAME }}-api:latest + + + tag_commit: + name: 'tag commit' + needs: + - release_app + - release_api + runs-on: ubuntu-latest + steps: + - name: Create/update latest tag + uses: actions/github-script@v7 + with: + script: | + github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'refs/tags/latest', + sha: context.sha + }).catch(err => { + if (err.status !== 422) throw err; + github.rest.git.updateRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'tags/latest', + sha: context.sha + }); + }) + + - name: Create/update version tag + uses: actions/github-script@v7 + with: + script: | + github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'refs/tags/${{ inputs.VERSION }}', + sha: context.sha + }).catch(err => { + if (err.status !== 422) throw err; + github.rest.git.updateRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'tags/${{ inputs.VERSION }}', + sha: context.sha + }); + }) + + create_release: + name: 'create release' + needs: + - release_app + - release_api + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Create release + uses: softprops/action-gh-release@v2 + with: + tag_name: '${{ inputs.VERSION }}' + name: 'swissgeol-assets v${{ inputs.VERSION }}' + generate_release_notes: true + make_latest: true diff --git a/.github/workflows/swissgeol-dev.yml b/.github/workflows/swissgeol-dev.yml deleted file mode 100644 index 6133b3b0..00000000 --- a/.github/workflows/swissgeol-dev.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Swissgeol Asset Dev Deploy - -on: - push: - branches: ["main"] - workflow_dispatch: -env: - APP_IMAGE: ghcr.io/geoadmin/swissgeol-asset-app:dev - API_IMAGE: ghcr.io/geoadmin/swissgeol-asset-api:dev - -jobs: - # ------------------ - build-web-app: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: generate version.json - id: generate-version - uses: ./.github/actions/generate-version - with: - path: ./apps/client-asset-sg/src/assets - - - name: Build app Docker image - run: docker build . -f ./apps/client-asset-sg/docker/Dockerfile -t ${{ env.APP_IMAGE }} - - - name: Push container image - id: registry-push - uses: ./.github/actions/registry-push - with: - image: ${{ env.APP_IMAGE }} - username: ${{ vars.SWISSGEOL_BUILD_USERNAME }} - password: ${{ secrets.SWISSGEOL_BUILD_PAT }} - - # ------------------ - build-web-api: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: generate version.json - id: generate-version - uses: ./.github/actions/generate-version - with: - path: ./apps/server-asset-sg/src/assets - - - name: Run NPM build - run: | - npm ci - npm run build -- server-asset-sg - - name: Retag Image - working-directory: ./apps/server-asset-sg/docker - run: docker tag registry.lambda-it.ch/asset-swissgeol/api:latest ${{ env.API_IMAGE }} - - - name: Push container image - id: registry-push - uses: ./.github/actions/registry-push - with: - image: ${{ env.API_IMAGE }} - username: ${{ vars.SWISSGEOL_BUILD_USERNAME }} - password: ${{ secrets.SWISSGEOL_BUILD_PAT }} diff --git a/.github/workflows/swissgeol-int.yml b/.github/workflows/swissgeol-int.yml deleted file mode 100644 index 4294496b..00000000 --- a/.github/workflows/swissgeol-int.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: Swissgeol Asset Int Deploy - -on: - release: - types: [published] - workflow_dispatch: - inputs: - tag: - description: "Container Tag" -env: - APP_IMAGE: ghcr.io/geoadmin/swissgeol-asset-app:${{ github.event.inputs.tag || github.event.release.tag_name }} - API_IMAGE: ghcr.io/geoadmin/swissgeol-asset-api:${{ github.event.inputs.tag || github.event.release.tag_name }} - -jobs: - # ------------------ - build-web-app: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.release.tag_name }} - - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: generate version.json - id: generate-version - uses: ./.github/actions/generate-version - with: - path: ./apps/client-asset-sg/src/assets - - - name: Build app Docker image - run: docker build . -f ./apps/client-asset-sg/docker/Dockerfile -t ${{ env.APP_IMAGE }} - - - name: Push container image - id: registry-push - uses: ./.github/actions/registry-push - with: - image: ${{ env.APP_IMAGE }} - username: ${{ vars.SWISSGEOL_BUILD_USERNAME }} - password: ${{ secrets.SWISSGEOL_BUILD_PAT }} - - # ------------------ - build-web-api: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.release.tag_name }} - - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: generate version.json - id: generate-version - uses: ./.github/actions/generate-version - with: - path: ./apps/client-asset-sg/src/assets - - - name: Run NPM build - run: | - npm ci - npm run build -- server-asset-sg - - name: Retag Image - working-directory: ./apps/server-asset-sg/docker - run: docker tag registry.lambda-it.ch/asset-swissgeol/api:latest ${{ env.API_IMAGE }} - - - name: Push container image - id: registry-push - uses: ./.github/actions/registry-push - with: - image: ${{ env.API_IMAGE }} - username: ${{ vars.SWISSGEOL_BUILD_USERNAME }} - password: ${{ secrets.SWISSGEOL_BUILD_PAT }} - - # ------------------ - deployment: - runs-on: ubuntu-latest - needs: [build-web-app, build-web-api] - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.release.tag_name }} - - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: Pulumi deployment to cluster - uses: ./.github/actions/deploy - with: - pulumiConfigSecret: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }} - awsAccessKeyId: ${{ secrets.AWS_ACCESS_KEY_ID }} - awsSecretAccessKey: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - eksClusterName: int - eksRoleArn: arn:aws:iam::779726271945:role/kubernetes-admins-int - pulumiConfigPassphrase: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }} - stack: swissgeol-asset-int - npmPkgToken: ${{ secrets.SWISSGEOL_BUILD_PAT }} - command: up - version: ${{ github.event.release.tag_name }} diff --git a/.github/workflows/swissgeol-prod.yml b/.github/workflows/swissgeol-prod.yml deleted file mode 100644 index 4528d5fc..00000000 --- a/.github/workflows/swissgeol-prod.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: Swissgeol Asset Prod Deploy - -on: - release: - types: [published] - workflow_dispatch: - inputs: - tag: - description: "Container Tag" -env: - APP_IMAGE: ghcr.io/geoadmin/swissgeol-asset-app:${{ github.event.inputs.tag || github.event.release.tag_name }} - API_IMAGE: ghcr.io/geoadmin/swissgeol-asset-api:${{ github.event.inputs.tag || github.event.release.tag_name }} - -jobs: - # ------------------ - build-web-app: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.release.tag_name }} - - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: generate version.json - id: generate-version - uses: ./.github/actions/generate-version - with: - path: ./apps/client-asset-sg/src/assets - - - name: Build app Docker image - run: docker build . -f ./apps/client-asset-sg/docker/Dockerfile -t ${{ env.APP_IMAGE }} - - - name: Push container image - id: registry-push - uses: ./.github/actions/registry-push - with: - image: ${{ env.APP_IMAGE }} - username: ${{ vars.SWISSGEOL_BUILD_USERNAME }} - password: ${{ secrets.SWISSGEOL_BUILD_PAT }} - - # ------------------ - build-web-api: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.release.tag_name }} - - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: generate version.json - id: generate-version - uses: ./.github/actions/generate-version - with: - path: ./apps/client-asset-sg/src/assets - - - name: Run NPM build - run: | - npm ci - npm run build -- server-asset-sg - - name: Retag Image - working-directory: ./apps/server-asset-sg/docker - run: docker tag registry.lambda-it.ch/asset-swissgeol/api:latest ${{ env.API_IMAGE }} - - - name: Push container image - id: registry-push - uses: ./.github/actions/registry-push - with: - image: ${{ env.API_IMAGE }} - username: ${{ vars.SWISSGEOL_BUILD_USERNAME }} - password: ${{ secrets.SWISSGEOL_BUILD_PAT }} - - # ------------------ - deployment: - runs-on: ubuntu-latest - needs: [build-web-app, build-web-api] - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.release.tag_name }} - - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: Pulumi deployment to cluster - uses: ./.github/actions/deploy - with: - pulumiConfigSecret: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }} - awsAccessKeyId: ${{ secrets.AWS_ACCESS_KEY_ID }} - awsSecretAccessKey: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - eksClusterName: prod - eksRoleArn: arn:aws:iam::779726271945:role/kubernetes-admins-prod - pulumiConfigPassphrase: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }} - stack: swissgeol-asset-prod - npmPkgToken: ${{ secrets.SWISSGEOL_BUILD_PAT }} - command: up - version: ${{ github.event.release.tag_name }} diff --git a/.gitignore b/.gitignore index c04fa8be..1c03120f 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ Thumbs.db __pycache__ +.env.local .env.staging .env.prod diff --git a/.vscode/settings.json b/.vscode/settings.json index 34d8f2e1..db0ada5d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "editor.codeActionsOnSave": { - "source.organizeImports": false, - "source.fixAll.eslint": true + "source.organizeImports": "never", + "source.fixAll.eslint": "explicit" }, "peacock.color": "#0b7285", "peacock.remoteColor": "#000", diff --git a/README.md b/README.md index 7582a80f..17303b39 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,185 @@ -# asset-swissgeol-ch +# SwissGeol Asset -More docs for asset-swissgeol-ch: https://github.com/Lambda-IT/asset-swissgeol-ch/wiki +## Development +The following components must be installed on the development computer: -## Instances +✔️ Git +✔️ Docker +✔️ Node.js 20 LTS -- assets.swissgeol.ch - - assets.swissgeol.ch/kibana -- int-assets.swissgeol.ch - - int-assets.swissgeol.ch/kibana +### Setting Up the Development Environment +Follow these steps to set up the development environment on your local machine: +* [1. Configure Local Systems](#1-Configure-Local-Systems) +* [2. Configure the Asset Server](#2-Configure-the-Asset-Server) +* [3. Install Dependencies](#3-Install-Dependencies) +* [4. Build Local Systems](#4-Build-Local-Systems) +* [5. Initialize MinIO](#5-Initialize-MinIO) + +#### 1. Configure Local Systems +Configure `development/.env` according to the [development services configuration](#Development-Services-Configuration). + +#### 2. Configure the Asset Server +Create an empty copy of the [web server configuration](#Asset-Server-Configuration) as [`apps/server-asset-sg/.env.local`](apps/server-asset-sg/.env.local). +Configure the following variables: +* Set `AUTH_URL=http://localhost:8866`. +* Set `FRONTEND_URL=http://localhost:4200`. +* Set `DATABASE_URL=postgres://asset-swissgeol:asset-swissgeol@localhost:5432/postgres?schema=public`. +* Set `GOTRUE_JWT_SECRET` to the same value as in [`development/.env`](development/.env). +* Leave `OCR_URL` empty. +* Leave `OCR_CALLBACK_URL` empty. + +#### 3. Install Dependencies +Install node modules: +```bash +npm run install +``` + +Decorate the Angular CLI with the Nx CLI: +```bash +npm run postinstall +``` + +#### 4. Build Local Services +Generate prisma-client for database-access: +```bash +cd apps/server-asset-sg/ +ng gen-prisma-client +``` + +Build postgis-gotrue docker image: +```bash +cd development/images/db +docker build -t postgis-gotrue . +``` + +#### 5. Initialize MinIO +* [Start the development services](#Starting-the-Development-Environment). +* Open http://localhost:9001 +* Sign in using the `STORAGE_USER` and `STORAGE_PASSWORD` of your development environment. +* Navigate to [Buckets](http://localhost:9001/buckets) and create a new bucket with the name `asset-sg`. +* Navigate to [the new bucket's browser](http://localhost:9001/browser/asset-sg) and create an empty folder with the name `asset-sg`. +* Navigate to [Configuration](http://localhost:9001/settings/configurations/region) and change the server region to `local`. +* Navigate to [Access Keys](http://localhost:9001/access-keys) and create a new access key. +* Open your Asset Server Configuration at [`apps/server-asset-sg/.env.local`](apps/server-asset-sg/.env.local) and make the following changes: + * `S3_REGION=local` + * `S3_ENDPOINT=http://localhost:9000` + * `S3_BUCKET_NAME=asset-sg` + * `S3_ASSET_FOLDER=asset-sg` + * `S3_ACCESS_KEY_ID` as your newly generated access key. + * `S3_SECRET_ACCESS_KEY` as your newly generated access key's secret. + +### Starting the Development Environment +Start development services: +```bash +cd development +docker compose up +``` +Start the application: +```bash +npm run start +``` + +### Local Services and Applications +| 🔖App/Service | 🔗Link | 🧞User | 🔐Password | +|:-------------------------|:-------------------------------------------------|:-------------------------|:-------------------------| +| Assets (client) | [localhost:4200](http://localhost:4200/) | `admin@swissgeol.assets` | `swissgeol_assets` | +| Assets REST API (server) | [localhost:3333/api/](http://localhost:3333/api) | n/a | n/a | +| postgreSQL (docker) | localhost:5432 | .env `$DB_USER` | .env `$DB_PASSWORD` | +| Elasticsearch (docker) | [localhost:9200](http://localhost:9200) | n/a | n/a | +| Kibana (docker) | [localhost:5601](http://localhost:5601) | n/a | n/a | +| pgAdmin (docker) | [localhost:5051](http://localhost:5051/) | .env `$PGADMIN_EMAIL` | .env `$PGADMIN_PASSWORD` | +| MinIO (docker) | [localhost:9001](http://localhost:9001/) | .env `$STORAGE_USER` | .env `$STORAGE_PASSWORD` | +| smtp4dev (docker) | [localhost:5000](http://localhost:5000/) | n/a | n/a | +| oidc-server (docker) | [localhost:4011](http://localhost:4011/) | n/a | n/a | + +### Importing Example Data +You can dump data from a remote environment into a local file so you can initialize your development database with it. +To do so, use the following commands. +Be aware that you need to manually insert the `{DB_*}` values beforehand. +```bash +cd development +docker compose exec db sh -c 'pg_dump --dbname=postgresql://{DB_USERNAME}:{DB_PASSWORD}@{DB_HOST}:5432/{DB_DATABASE} --data-only --exclude-table asset_user -n public > /dump.sql' +``` +> The export will output warnings related to circular foreign-key constraints. +> These can be safely ignored. + +> The export will only contain the database's data, not its structure. +> Data related to the authentication process is also excluded, +> so we don't run into conflicts when using a different eIAM provider. + +To import the dumped data, run the following commands. +Ensure to start your database service beforehand. +```bash +# Reset the database: +npm run prisma -- migrate reset -f +npm run prisma -- migrate deploy + +# Import example data: +cd development +docker compose exec db sh -c 'psql --dbname=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB} -f /dump.sql' +``` +> You will need to manually sync the data to Elasticsearch via the admin panel in the web UI. + +## Testing +> Tests execute automatically on every push to the Git repository. + +The local tests require a running instance of both _postgreSQL_ and _Elasticsearch_. +Make sure that your local development environment is fully shutdown and then run the test services: +```bash +cd development +docker compose down +docker compose -f docker-compose.test.yml up +``` +Then run all tests: +```bash +npm run test +``` +It is also possible to run only specific tests: +```bash +# Run only the server tests: +nx run server-asset-sg:test + +# Run only a specific test suite: +nx run server-asset-sg:test -t 'AssetRepo' + +# Run only a specific, nested test suite: +nx run server-asset-sg:test -t 'AssetRepo create' +``` + +## Configuration +### Asset Server Configuration +The file `apps/server-asset-sg/.env.local` configures secrets for the SwissGeol Asset server. +An empty template for the file can be found in [`apps/server-asset-sg/.env.template`](apps/server-asset-sg/.env.template). + +| Variable | Example | Description | +|----------------------|--------------------------------------------------------------------------------------------|------------------------------------------------------------| +| AUTH_URL | http://my.gotrue.example:8866 | URL of the GoTrue auth service. | +| FRONTEND_URL | http://assets.geo.admin.ch | Public URL of the SwissGeol Asset web client. | +| S3_REGION | euw-3 | Region of the S3 instance. | +| S3_ENDPOINT | http://compute-1.amazonaws.com | URL to the S3 instance. | +| S3_ACCESS_KEY_ID | AP6wpeXraSc0IH4d42IN | Access Key for the S3 instance. | +| S3_SECRET_ACCESS_KEY | fSx5Bfib0OeAyG1mwtslKA04Qj6oPStLcpnkACmF | Secret Key for the S3 instance. | +| S3_BUCKET_NAME | asset-sg | S3 bucket name. | +| S3_ASSET_FOLDER | asset-sg | Folder within the S3 bucket into which objects are stored. | +| DATABASE_URL | postgres://asset-swissgeol:asset-swissgeol@my.postgres.example:5432/postgres?schema=public | PostgreSQL access URL. | +| GOTRUE_JWT_SECRET | 18af41574b30be7539d8c3e45ccdeea9431cff6419cdce5cabc5f28cfb73e15c | JWT secret key for the GoTrue server. | +| OCR_URL | | Leave empty. | +| OCR_CALLBACK_URL | | Leave empty. | + + +### Development Services Configuration +The file `development/.env` configures secrets for the services used in local development. +An empty template for the file can be found in [`development/.env.template`](development/.env.template). + +> Make sure that your passwords have a minimal length of 8 and contain at combination of +> upper, lower and special characters. Some of the passwords will be checked for validity during startup. + +| Variable | Wert | Beschreibung | +|-------------------|----------|------------------------------------------| +| STORAGE_USER | _custom_ | Username for the MinIO container. | +| STORAGE_PASSWORD | _custom_ | Password for the MinIO container. | +| DB_USER | postgres | Username for the PostgreSQL container. | +| DB_PASSWORD | _custom_ | Password for the PostgreSQL container. | +| PGADMIN_EMAIL | _custom_ | Email for the PgAdmin container. | +| PGADMIN_PASSWORD | _custom_ | Password for the PgAdmin container. | +| GOTRUE_JWT_SECRET | _custom_ | JWT Secret Key for the GoTrue container. | diff --git a/apps/client-asset-sg-e2e/cypress.config.ts b/apps/client-asset-sg-e2e/cypress.config.ts index ec4ed885..39337d99 100644 --- a/apps/client-asset-sg-e2e/cypress.config.ts +++ b/apps/client-asset-sg-e2e/cypress.config.ts @@ -1,5 +1,5 @@ -import { defineConfig } from 'cypress'; import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; export default defineConfig({ e2e: nxE2EPreset(__dirname), diff --git a/apps/client-asset-sg-e2e/src/support/commands.ts b/apps/client-asset-sg-e2e/src/support/commands.ts index 4200179b..384d6715 100644 --- a/apps/client-asset-sg-e2e/src/support/commands.ts +++ b/apps/client-asset-sg-e2e/src/support/commands.ts @@ -18,7 +18,7 @@ declare namespace Cypress { // // -- This is a parent command -- Cypress.Commands.add('login', (email, password) => { - console.log('Custom command example: Login', email, password); + console.log('Custom command example: Login'); }); // // -- This is a child command -- diff --git a/apps/client-asset-sg/docker/Dockerfile b/apps/client-asset-sg/docker/Dockerfile index ae87bd02..5842e09c 100644 --- a/apps/client-asset-sg/docker/Dockerfile +++ b/apps/client-asset-sg/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16-alpine as ui-builder +FROM node:20-alpine as app-builder WORKDIR /app COPY . . @@ -11,10 +11,9 @@ RUN npx nx build client-asset-sg # final image build FROM nginx:mainline-alpine -LABEL maintainer=support@lambda-it.ch WORKDIR /usr/share/nginx/html -COPY --from=ui-builder /app/dist/apps/client-asset-sg . +COPY --from=app-builder /app/dist/apps/client-asset-sg . # this nginx base image will parse the template and will move it to # /etc/nginx/conf.d/default.conf before it starts nginx process diff --git a/apps/client-asset-sg/project.json b/apps/client-asset-sg/project.json index dceecf5a..b99fba3a 100644 --- a/apps/client-asset-sg/project.json +++ b/apps/client-asset-sg/project.json @@ -1,93 +1,120 @@ { - "name": "client-asset-sg", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "projectType": "application", - "sourceRoot": "apps/client-asset-sg/src", - "prefix": "asset-sg", - "targets": { - "build": { - "executor": "@angular-devkit/build-angular:browser", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/apps/client-asset-sg", - "index": "apps/client-asset-sg/src/index.html", - "main": "apps/client-asset-sg/src/main.ts", - "polyfills": ["zone.js"], - "tsConfig": "apps/client-asset-sg/tsconfig.app.json", - "inlineStyleLanguage": "scss", - "assets": ["apps/client-asset-sg/src/favicon.ico", "apps/client-asset-sg/src/assets"], - "styles": ["apps/client-asset-sg/src/styles.scss"], - "scripts": [] + "name": "client-asset-sg", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "sourceRoot": "apps/client-asset-sg/src", + "prefix": "asset-sg", + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/apps/client-asset-sg", + "index": "apps/client-asset-sg/src/index.html", + "main": "apps/client-asset-sg/src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "apps/client-asset-sg/tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "apps/client-asset-sg/src/favicon.ico", + "apps/client-asset-sg/src/assets" + ], + "styles": [ + "apps/client-asset-sg/src/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1.2mb", + "maximumError": "1.3mb" }, - "configurations": { - "production": { - "budgets": [ - { - "type": "initial", - "maximumWarning": "1.2mb", - "maximumError": "1.3mb" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "2kb", - "maximumError": "4kb" - } - ], - "outputHashing": "all", - "fileReplacements": [ - { - "replace": "apps/client-asset-sg/src/environments/environment.ts", - "with": "apps/client-asset-sg/src/environments/environment.prod.ts" - } - ] - }, - "development": { - "buildOptimizer": false, - "optimization": false, - "vendorChunk": true, - "extractLicenses": false, - "sourceMap": true, - "namedChunks": true - } - }, - "defaultConfiguration": "production" - }, - "serve": { - "executor": "@angular-devkit/build-angular:dev-server", - "configurations": { - "production": { - "browserTarget": "client-asset-sg:build:production" - }, - "development": { - "browserTarget": "client-asset-sg:build:development" - } - }, - "defaultConfiguration": "development", - "options": { - "proxyConfig": "apps/client-asset-sg/proxy.conf.json" + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" } - }, - "extract-i18n": { - "executor": "@angular-devkit/build-angular:extract-i18n", - "options": { - "browserTarget": "client-asset-sg:build" + ], + "outputHashing": "all", + "fileReplacements": [ + { + "replace": "apps/client-asset-sg/src/environments/environment.ts", + "with": "apps/client-asset-sg/src/environments/environment.prod.ts" } + ] }, - "lint": { - "executor": "@nrwl/linter:eslint", - "outputs": ["{options.outputFile}"], - "options": { - "lintFilePatterns": ["apps/client-asset-sg/**/*.ts", "apps/client-asset-sg/**/*.html"] + "int": { + "fileReplacements": [ + { + "replace": "apps/client-asset-sg/src/environments/environment.ts", + "with": "apps/client-asset-sg/src/environments/environment.int.ts" } + ] }, - "test": { - "executor": "@nrwl/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], - "options": { - "jestConfig": "apps/client-asset-sg/jest.config.ts", - "passWithNoTests": true - } + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "client-asset-sg:build:production" + }, + "int": { + "browserTarget": "client-asset-sg:build:int" + }, + "development": { + "browserTarget": "client-asset-sg:build:development" + } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "apps/client-asset-sg/proxy.conf.json" + } + }, + "extract-i18n": { + "executor": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "client-asset-sg:build" + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": [ + "{options.outputFile}" + ], + "options": { + "lintFilePatterns": [ + "apps/client-asset-sg/**/*.ts", + "apps/client-asset-sg/**/*.html" + ] + } }, - "tags": [] + "test": { + "executor": "@nrwl/jest:jest", + "outputs": [ + "{workspaceRoot}/coverage/{projectRoot}" + ], + "options": { + "jestConfig": "apps/client-asset-sg/jest.config.ts", + "passWithNoTests": true + } + } + }, + "tags": [] } diff --git a/apps/client-asset-sg/src/app/app.component.html b/apps/client-asset-sg/src/app/app.component.html index 51447d42..52c102ab 100644 --- a/apps/client-asset-sg/src/app/app.component.html +++ b/apps/client-asset-sg/src/app/app.component.html @@ -1,10 +1,10 @@ - + - +
- +
-
- +
+
diff --git a/apps/client-asset-sg/src/app/app.component.spec.ts b/apps/client-asset-sg/src/app/app.component.spec.ts deleted file mode 100644 index 5c44f3d5..00000000 --- a/apps/client-asset-sg/src/app/app.component.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { AppComponent } from './app.component'; -import { NxWelcomeComponent } from './nx-welcome.component'; - -describe('AppComponent', () => { - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [AppComponent, NxWelcomeComponent], - }).compileComponents(); - }); - - it('should create the app', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); - }); - - it(`should have as title 'client-asset-sg'`, () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app.title).toEqual('client-asset-sg'); - }); - - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Welcome client-asset-sg'); - }); -}); diff --git a/apps/client-asset-sg/src/app/app.component.ts b/apps/client-asset-sg/src/app/app.component.ts index 7ec1d29a..f6cd7e3a 100644 --- a/apps/client-asset-sg/src/app/app.component.ts +++ b/apps/client-asset-sg/src/app/app.component.ts @@ -1,43 +1,69 @@ +import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; +import { Router } from '@angular/router'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { Store } from '@ngrx/store'; import { WINDOW } from 'ngx-window-token'; -import { debounceTime, fromEvent, startWith } from 'rxjs'; +import { debounceTime, fromEvent, map, startWith } from 'rxjs'; import { assert } from 'tsafe'; -import { AppPortalService, setCssCustomProperties } from '@asset-sg/client-shared'; -import { FavouriteService } from '@asset-sg/favourite'; +import { AuthService } from '@asset-sg/auth'; +import { AppPortalService, appSharedStateActions, setCssCustomProperties } from '@asset-sg/client-shared'; + +import { AppState } from './state/app-state'; const fullHdWidth = 1920; @UntilDestroy() @Component({ - selector: 'asset-sg-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'], + selector: 'asset-sg-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], }) export class AppComponent { - private _wndw = inject(WINDOW); - - public appPortalService = inject(AppPortalService); - private _favouriteService = inject(FavouriteService); - - constructor() { - const wndw = this._wndw; - assert(wndw != null); - - fromEvent(wndw, 'resize') - .pipe(debounceTime(50), startWith(null), untilDestroyed(this)) - .subscribe(() => { - let fontSize = '1rem'; - const width = window.innerWidth; - if (width >= fullHdWidth) { - fontSize = '1rem'; - } else if (width >= 0.8 * fullHdWidth) { - fontSize = `${width / fullHdWidth}rem`; - } else { - fontSize = '0.8rem'; - } - setCssCustomProperties(wndw.document.documentElement, ['font-size', fontSize]); - }); - } + private _wndw = inject(WINDOW); + private _httpClient = inject(HttpClient); + public appPortalService = inject(AppPortalService); + private store = inject(Store); + public readonly router: Router = inject(Router); + + constructor(private readonly _authService: AuthService) { + this._httpClient + .get('api/oauth-config/config') + .pipe( + map((response: any) => { + return response; + }), + ) + .subscribe(async oAuthConfig => { + await this._authService.configureOAuth( + oAuthConfig.oauth_issuer, + oAuthConfig.oauth_clientId, + oAuthConfig.oauth_scope, + oAuthConfig.oauth_showDebugInformation, + oAuthConfig.oauth_tokenEndpoint, + ); + + this.store.dispatch(appSharedStateActions.loadUserProfile()); + this.store.dispatch(appSharedStateActions.loadReferenceData()); + }); + + const wndw = this._wndw; + assert(wndw != null); + + fromEvent(wndw, 'resize') + .pipe(debounceTime(50), startWith(null), untilDestroyed(this)) + .subscribe(() => { + let fontSize; + const width = window.innerWidth; + if (width >= fullHdWidth) { + fontSize = '1rem'; + } else if (width >= 0.8 * fullHdWidth) { + fontSize = `${width / fullHdWidth}rem`; + } else { + fontSize = '0.8rem'; + } + setCssCustomProperties(wndw.document.documentElement, ['font-size', fontSize]); + }); + } } diff --git a/apps/client-asset-sg/src/app/app.module.ts b/apps/client-asset-sg/src/app/app.module.ts index 2cfdf690..abfd15f4 100644 --- a/apps/client-asset-sg/src/app/app.module.ts +++ b/apps/client-asset-sg/src/app/app.module.ts @@ -19,14 +19,14 @@ import { PushModule } from '@rx-angular/template/push'; import * as O from 'fp-ts/Option'; import * as C from 'io-ts/Codec'; -import { AuthInterceptor } from '@asset-sg/auth'; +import { AuthInterceptor, AuthModule } from '@asset-sg/auth'; import { - AnchorComponent, - ButtonComponent, - CURRENT_LANG, - TranslateTsLoader, - currentLangFactory, - icons, + AnchorComponent, + ButtonComponent, + CURRENT_LANG, + TranslateTsLoader, + currentLangFactory, + icons, } from '@asset-sg/client-shared'; import { storeLogger } from '@asset-sg/core'; @@ -36,6 +36,7 @@ import { adminGuard, editorGuard } from './app-guards'; import { assetsPageMatcher } from './app-matchers'; import { AppComponent } from './app.component'; import { AppBarComponent, MenuBarComponent, NotFoundComponent, RedirectToLangComponent } from './components'; +import { ErrorComponent } from './components/error/error.component'; import { appTranslations } from './i18n'; import { AppSharedStateEffects } from './state'; import { appSharedStateReducer } from './state/app-shared.reducer'; @@ -43,89 +44,104 @@ import { appSharedStateReducer } from './state/app-shared.reducer'; registerLocaleData(locale_deCH, 'de-CH'); @NgModule({ - declarations: [AppComponent, RedirectToLangComponent, NotFoundComponent, AppBarComponent, MenuBarComponent], - imports: [ - BrowserModule, - BrowserAnimationsModule, - HttpClientModule, - RouterModule.forRoot([ - { - path: ':lang/a', - loadChildren: () => import('@asset-sg/auth').then(m => m.AuthModule), - }, - { - path: ':lang/profile', - loadChildren: () => import('@asset-sg/profile').then(m => m.ProfileModule), - }, - { - path: ':lang/admin', - loadChildren: () => import('@asset-sg/admin').then(m => m.AdminModule), - canActivate: [adminGuard], - }, - { - path: ':lang/asset-admin', - loadChildren: () => import('@asset-sg/asset-editor').then(m => m.AssetEditorModule), - canActivate: [editorGuard], - }, - { - matcher: assetsPageMatcher, - loadChildren: () => import('@asset-sg/asset-viewer').then(m => m.AssetViewerModule), - }, - { - path: 'not-found', - component: NotFoundComponent, - }, - { - path: '**', - component: RedirectToLangComponent, - }, - ]), - TranslateModule.forRoot({ - loader: { provide: TranslateLoader, useFactory: () => new TranslateTsLoader(appTranslations) }, - }), - StoreRouterConnectingModule.forRoot({ serializer: FullRouterStateSerializer, stateKey: 'router' }), - StoreModule.forRoot( - { router: routerReducer, shared: appSharedStateReducer }, - { - metaReducers: environment.ngrxStoreLoggerEnabled ? [storeLogger()] : [], - runtimeChecks: { - strictStateImmutability: false, - }, - }, - ), - EffectsModule.forRoot([AppSharedStateEffects]), - ForModule, - LetModule, - PushModule, + declarations: [ + AppComponent, + RedirectToLangComponent, + NotFoundComponent, + AppBarComponent, + MenuBarComponent, + ErrorComponent, + ], + imports: [ + BrowserModule, + BrowserAnimationsModule, + HttpClientModule, + RouterModule.forRoot([ + { + path: ':lang/a', + loadChildren: () => import('@asset-sg/auth').then(m => m.AuthModule), + }, + { + path: ':lang/profile', + loadChildren: () => import('@asset-sg/profile').then(m => m.ProfileModule), + }, + { + path: ':lang/admin', + loadChildren: () => import('@asset-sg/admin').then(m => m.AdminModule), + canActivate: [adminGuard], + }, + { + path: ':lang/asset-admin', + loadChildren: () => import('@asset-sg/asset-editor').then(m => m.AssetEditorModule), + canActivate: [editorGuard], + }, + { + path: ':lang/error', + component: ErrorComponent, + }, + { + matcher: assetsPageMatcher, + loadChildren: () => import('@asset-sg/asset-viewer').then(m => m.AssetViewerModule), + }, + { + path: 'not-found', + component: NotFoundComponent, + }, + { + path: '**', + component: RedirectToLangComponent, + }, + ]), + TranslateModule.forRoot({ + loader: { provide: TranslateLoader, useFactory: () => new TranslateTsLoader(appTranslations) }, + }), + StoreRouterConnectingModule.forRoot({ serializer: FullRouterStateSerializer, stateKey: 'router' }), + StoreModule.forRoot( + { router: routerReducer, shared: appSharedStateReducer }, + { + metaReducers: environment.ngrxStoreLoggerEnabled ? [storeLogger()] : [], + runtimeChecks: { + strictStateImmutability: false, + }, + }, + ), + EffectsModule.forRoot([AppSharedStateEffects]), + ForModule, + LetModule, + PushModule, - SvgIconComponent, + SvgIconComponent, - AnchorComponent, - ButtonComponent, - DialogModule, - A11yModule, - ], - providers: [ - provideSvgIcons(icons), - { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, - { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'fill', floatLabel: 'auto' } }, - { provide: CURRENT_LANG, useFactory: currentLangFactory }, - ], - bootstrap: [AppComponent], + AnchorComponent, + ButtonComponent, + DialogModule, + A11yModule, + AuthModule, + ], + providers: [ + provideSvgIcons(icons), + { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, + { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'fill', floatLabel: 'auto' } }, + { provide: CURRENT_LANG, useFactory: currentLangFactory }, + ], + bootstrap: [AppComponent], }) export class AppModule { - private _translateService = inject(TranslateService); - constructor() { - this._translateService.setDefaultLang('de'); - } + private _translateService = inject(TranslateService); + + constructor() { + this._translateService.setDefaultLang('de'); + } } export interface Encoder { - readonly encode: (a: A) => O; + readonly encode: (a: A) => O; } + function optionFromNullable(encoder: Encoder): Encoder> { - return { - encode: O.fold(() => null, encoder.encode), - }; + return { + encode: O.fold(() => null, encoder.encode), + }; } + const foooobar = optionFromNullable(C.string); diff --git a/apps/client-asset-sg/src/app/components/app-bar/app-bar.component.html b/apps/client-asset-sg/src/app/components/app-bar/app-bar.component.html index eda685f8..8eb2d1a1 100644 --- a/apps/client-asset-sg/src/app/components/app-bar/app-bar.component.html +++ b/apps/client-asset-sg/src/app/components/app-bar/app-bar.component.html @@ -1,5 +1,5 @@ - +