diff --git a/.github/workflows/check-documentation.yml b/.github/workflows/check-documentation.yml index 27e761fa5..6b2cddeac 100644 --- a/.github/workflows/check-documentation.yml +++ b/.github/workflows/check-documentation.yml @@ -54,7 +54,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7 - name: Setup Node 20 - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.0.3 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.0.3 with: node-version: '20' diff --git a/.github/workflows/deploy-documentation.yml b/.github/workflows/deploy-documentation.yml index d1816bddd..13e0fb963 100644 --- a/.github/workflows/deploy-documentation.yml +++ b/.github/workflows/deploy-documentation.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7 - name: Setup Node 20 - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.0.3 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.0.3 with: node-version: 20 diff --git a/.github/workflows/docker-push.yml b/.github/workflows/docker-push.yml index 406d8304b..cc84c6e3e 100644 --- a/.github/workflows/docker-push.yml +++ b/.github/workflows/docker-push.yml @@ -81,7 +81,7 @@ jobs: type=sha - name: Build and push Docker images id: push - uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 + uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 with: context: ${{ matrix.app.context }} target: ${{ matrix.app.target }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b66413f22..425da269b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -64,7 +64,7 @@ jobs: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV #- name: Repository Dispatch - # uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0 + # uses: peter-evans/repository-dispatch@44966f0098fd4ab26380bb099e1edf6d57eb2c90 # v3.0.0 # with: # token: ${{ github.token }} # event-type: trigger-ocelot-build-success @@ -72,7 +72,7 @@ jobs: # client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}' - name: Repository Dispatch stage.ocelot.social - uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0 + uses: peter-evans/repository-dispatch@44966f0098fd4ab26380bb099e1edf6d57eb2c90 # v3.0.0 with: token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository event-type: trigger-ocelot-build-success @@ -80,7 +80,7 @@ jobs: client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "GITHUB_RUN_NUMBER": "${{ env.GITHUB_RUN_NUMBER }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}' - name: Repository Dispatch stage.yunite.me - uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0 + uses: peter-evans/repository-dispatch@44966f0098fd4ab26380bb099e1edf6d57eb2c90 # v3.0.0 with: token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository event-type: trigger-ocelot-build-success diff --git a/.gitignore b/.gitignore index d9d081e31..6f71f582b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ yarn-error.log* kubeconfig.yaml backup-cron-job.log .vscode +.nuxt node_modules/ cypress/videos diff --git a/CHANGELOG.md b/CHANGELOG.md index 361c5df32..e3b1a955a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,80 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [3.4.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.3.0...3.4.0) + +- fix(webapp): fix badge focus [`#8452`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8452) +- feat(backend): branding middlewares [`#8429`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8429) +- refactor(webapp): make login, registration, password-reset layout brandable [`#8440`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8440) +- fix(backend): fixes for branding [`#8449`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8449) +- Replace edit link by pencil button [`#8453`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8453) +- fix(webapp): refine little things [`#8382`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8382) +- fix(webapp): fix admin badges settings [`#8438`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8438) +- build(deps): bump peter-evans/repository-dispatch [`#8443`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8443) +- build(deps-dev): bump nodemon from 3.1.9 to 3.1.10 in /backend [`#8447`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8447) +- build(deps-dev): bump @types/node from 22.14.1 to 22.15.2 in /backend [`#8446`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8446) +- build(deps): bump docker/build-push-action from 6.15.0 to 6.16.0 [`#8444`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8444) +- build(deps-dev): bump cypress from 14.3.1 to 14.3.2 in the cypress group [`#8442`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8442) +- build(deps-dev): bump eslint-import-resolver-typescript in /backend [`#8445`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8445) +- build(deps-dev): bump eslint-config-prettier in /backend [`#8370`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8370) +- revokeBadge also removes selection [`#8437`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8437) +- feat(webapp): badges UI [`#8426`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8426) +- feat(backend): lint - detect unused typescript disables [`#8425`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8425) +- fix(docu): remove required but missing `frontend/.env` [`#8431`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8431) +- refactor(backend): types for neo4j & neode [`#8409`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8409) +- lint everything, disable some setup steps for jest [`#8423`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8423) +- lint n/no-sync [`#8405`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8405) +- fix(backend): fix notification emails with different name [`#8419`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8419) +- refactor(backend): default badges, always return a badge [`#8430`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8430) +- refactor(backend): allow to set selected badge-slot to null [`#8421`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8421) +- chore(frontend): run npm install [`#8432`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8432) +- refactor(webapp): refactor branding diverse v2 [`#8427`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8427) +- feat(webapp): badges admin settings [`#8401`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8401) +- move graphql types into graphql folder [`#8420`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8420) +- fix faker image seed [`#8422`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8422) +- build(deps-dev): bump @faker-js/faker from 9.6.0 to 9.7.0 in /webapp [`#8411`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8411) +- build(deps-dev): bump @faker-js/faker from 9.6.0 to 9.7.0 [`#8414`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8414) +- build(deps): bump sanitize-html from 2.15.0 to 2.16.0 in /backend [`#8418`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8418) +- build(deps-dev): bump cypress from 14.3.0 to 14.3.1 in the cypress group [`#8413`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8413) +- build(deps): bump actions/setup-node from 4.3.0 to 4.4.0 [`#8412`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8412) +- refactor(backend): separate queries [`#8358`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8358) +- refactor(backend): lint @typescript-eslint/strict [`#8408`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8408) +- refactor(backend): lint @typescript-eslint/recommended-requiring-type-checking [`#8407`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8407) +- lint @typescript-eslint/recommended [`#8406`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8406) +- build(deps): bump nodemailer from 6.10.0 to 6.10.1 in /backend [`#8417`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8417) +- build(deps-dev): bump @eslint-community/eslint-plugin-eslint-comments [`#8415`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8415) +- feat(backend): badges [`#8391`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8391) +- feat(backend): do not notify blocked or muted users [`#8403`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8403) +- feat(backend): only one email is sent although more notifications are triggered [`#8400`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8400) +- fix(backend): flaky notifications on mention in group unit test [`#8404`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8404) +- refactor(webapp): refactor branding of post ribbons and chat etc. [`#8395`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8395) +- downgrade sass to 1.77.6 [`#8399`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8399) +- mentiioned users in posts and comments of groups [`#8392`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8392) +- feat(backend): no notification mails to users online [`#8397`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8397) +- Add .nuxt to gitignore [`#8393`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8393) +- fix migrations [`#8390`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8390) +- chore(frontend): add '.nvmrc' file to new frontend [`#7112`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7112) +- refactor(backend): fix is muted by me query [`#8365`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8365) +- fix(backend): block/mute chat [`#8364`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8364) +- build(deps): bump graphql-upload from 11.0.0 to 13.0.0 in /backend [`#8375`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8375) +- build(deps-dev): bump the typescript group across 1 directory with 2 updates [`#8383`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8383) +- Bump graphql from 14.7.0 to 15.10.1 in /webapp [`#8157`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8157) +- fix(webapp): better settings ux [`#8347`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8347) +- Bump bcryptjs from 2.4.3 to 3.0.2 [`#8218`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8218) +- Bump bcryptjs from 2.4.3 to 3.0.2 in /backend [`#8224`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8224) +- build(deps-dev): bump cypress from 14.2.1 to 14.3.0 in the cypress group [`#8366`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8366) +- build(deps-dev): bump eslint-import-resolver-typescript in /backend [`#8369`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8369) +- build(deps-dev): bump dotenv from 16.4.7 to 16.5.0 [`#8367`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8367) +- build(deps): bump ioredis from 4.16.1 to 5.6.1 in /backend [`#8371`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8371) +- build(deps): bump dotenv from 16.4.7 to 16.5.0 in /backend [`#8372`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8372) +- build(deps-dev): bump eslint-config-prettier in /webapp [`#8377`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8377) +- build(deps-dev): bump @types/node from 22.14.0 to 22.14.1 in /backend [`#8374`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8374) + #### [3.3.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.2.1...3.3.0) +> 12 April 2025 + +- v3.3.0 [`#8380`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8380) - fix(webapp): refine group muting locales [`#8378`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8378) - chore(backend): add e-mail setting for our new 'mailserver' to our backend .env.template [`#8359`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8359) - refactor(backend): set up smtp pooling for nodemailer [`#8167`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8167) diff --git a/README.md b/README.md index b1fe0ea14..910eae5a4 100644 --- a/README.md +++ b/README.md @@ -187,10 +187,6 @@ $ cp .env.template .env # in folder backend/ $ cp .env.template .env -# in folder frontend/ -$ cp .env.template .env -``` - For Development: ```bash diff --git a/backend/.eslintignore b/backend/.eslintignore new file mode 100644 index 000000000..e19e2338d --- /dev/null +++ b/backend/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ +build/ +coverage/ \ No newline at end of file diff --git a/backend/.eslintrc.cjs b/backend/.eslintrc.cjs index cff4c1de1..1fe6b8779 100644 --- a/backend/.eslintrc.cjs +++ b/backend/.eslintrc.cjs @@ -16,6 +16,7 @@ module.exports = { 'plugin:promise/recommended', 'plugin:security/recommended-legacy', 'plugin:@eslint-community/eslint-comments/recommended', + 'prettier', ], settings: { 'import/parsers': { @@ -114,7 +115,7 @@ module.exports = { 'n/no-callback-literal': 'error', // 'n/no-deprecated-api': 'error', // part of n/recommended // 'n/no-exports-assign': 'error', // part of n/recommended - 'n/no-extraneous-import': 'off', // TODO // part of n/recommended + 'n/no-extraneous-import': 'off', // duplicate of import/no-extraneous-dependencies // part of n/recommended // 'n/no-extraneous-require': 'error', // part of n/recommended 'n/no-hide-core-modules': 'error', 'n/no-missing-import': 'off', // not compatible with typescript // part of n/recommended @@ -126,7 +127,7 @@ module.exports = { // 'n/no-process-exit': 'error', // part of n/recommended 'n/no-restricted-import': 'error', 'n/no-restricted-require': 'error', - // 'n/no-sync': 'error', + 'n/no-sync': 'error', // 'n/no-unpublished-bin': 'error', // part of n/recommended 'n/no-unpublished-import': [ 'error', @@ -178,9 +179,10 @@ module.exports = { { files: ['*.ts', '*.tsx'], extends: [ - // 'plugin:@typescript-eslint/recommended', - // 'plugin:@typescript-eslint/recommended-requiring-type-checking', - // 'plugin:@typescript-eslint/strict', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:@typescript-eslint/strict', + 'prettier', ], rules: { // allow explicitly defined dangling promises @@ -192,6 +194,11 @@ module.exports = { 'import/unambiguous': 'off', // this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable '@typescript-eslint/no-unnecessary-condition': 'off', + // respect underscore as acceptable unused variable + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], }, parserOptions: { tsconfigRootDir: __dirname, diff --git a/backend/Dockerfile b/backend/Dockerfile index 40b78225a..f89fa2d3d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -23,11 +23,14 @@ COPY . . ONBUILD COPY ./branding/constants/ src/config/tmp ONBUILD RUN tools/replace-constants.sh ONBUILD COPY ./branding/email/ src/middleware/helpers/email/ +ONBUILD COPY ./branding/middlewares/ src/middleware/branding/ +ONBUILD COPY ./branding/data/ src/db/data +ONBUILD COPY ./branding/public/ public/ ONBUILD RUN yarn install --production=false --frozen-lockfile --non-interactive ONBUILD RUN yarn run build ONBUILD RUN mkdir /build ONBUILD RUN cp -r ./build /build -ONBUILD RUN cp -r ./public /build/build +ONBUILD RUN cp -r ./public /build ONBUILD RUN cp -r ./package.json yarn.lock /build ONBUILD RUN cd /build && yarn install --production=true --frozen-lockfile --non-interactive diff --git a/backend/README.md b/backend/README.md index bfc875d95..7d8bbfb15 100644 --- a/backend/README.md +++ b/backend/README.md @@ -120,6 +120,20 @@ When using `CATEGORIES_ACTIVE=true` you also want to seed the categories with: yarn db:data:categories ``` +### Branding Data + +You might need to seed some branding specific data into the database. + +To do so, run: + +```sh +# in backend with database running (In docker or local) +yarn db:data:branding + +# for docker +docker exec backend yarn db:data:branding +``` + ### Seed Data For a predefined set of test data you can seed the database with: diff --git a/backend/branding/data/.gitkeep b/backend/branding/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/branding/middlewares/.gitkeep b/backend/branding/middlewares/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/branding/public/.gitkeep b/backend/branding/public/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/package.json b/backend/package.json index b6cadf0b9..6d12190ed 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social-backend", - "version": "3.3.0", + "version": "3.4.0", "description": "GraphQL Backend for ocelot.social", "repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social", "author": "ocelot.social Community", @@ -12,16 +12,19 @@ "build": "tsc && tsc-alias && ./scripts/build.copy.files.sh", "dev": "nodemon --exec ts-node --require tsconfig-paths/register src/ -e js,ts,gql", "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,ts,gql", - "lint": "eslint --max-warnings=0 --ext .js,.ts ./src", + "lint": "eslint --max-warnings=0 --report-unused-disable-directives --ext .js,.ts .", "test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --runInBand --coverage --forceExit --detectOpenHandles", "db:reset": "ts-node --require tsconfig-paths/register src/db/reset.ts", "db:reset:withmigrations": "ts-node --require tsconfig-paths/register src/db/reset-with-migrations.ts", "db:seed": "ts-node --require tsconfig-paths/register src/db/seed.ts", "db:data:admin": "ts-node --require tsconfig-paths/register src/db/admin.ts", + "db:data:badges": "ts-node --require tsconfig-paths/register src/db/badges.ts", + "db:data:branding": "ts-node --require tsconfig-paths/register src/db/data-branding.ts", "db:data:categories": "ts-node --require tsconfig-paths/register src/db/categories.ts", "db:migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --store ./src/db/migrate/store.ts", "db:migrate:create": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create", - "prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js" + "prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js", + "prod:db:data:branding": "node build/src/db/data-branding.js" }, "dependencies": { "@babel/cli": "~7.27.0", @@ -87,10 +90,10 @@ "neo4j-graphql-js": "^2.11.5", "neode": "^0.4.9", "node-fetch": "^2.7.0", - "nodemailer": "^6.10.0", + "nodemailer": "^6.10.1", "nodemailer-html-to-text": "^3.2.0", "request": "~2.88.2", - "sanitize-html": "~2.15.0", + "sanitize-html": "~2.16.0", "slug": "~9.1.0", "subscriptions-transport-ws": "^0.9.19", "trunc-html": "~1.1.2", @@ -99,17 +102,19 @@ "xregexp": "^5.1.2" }, "devDependencies": { - "@eslint-community/eslint-plugin-eslint-comments": "^4.4.1", - "@faker-js/faker": "9.6.0", + "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", + "@faker-js/faker": "9.7.0", "@types/jest": "^29.5.14", - "@types/node": "^22.14.1", + "@types/lodash": "^4.17.16", + "@types/node": "^22.15.2", + "@types/uuid": "~9.0.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "apollo-server-testing": "~2.11.0", "eslint": "^8.57.1", - "eslint-config-prettier": "^10.1.1", + "eslint-config-prettier": "^10.1.2", "eslint-config-standard": "^17.1.0", - "eslint-import-resolver-typescript": "^4.3.2", + "eslint-import-resolver-typescript": "^4.3.4", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.11.0", "eslint-plugin-n": "^17.17.0", @@ -118,7 +123,7 @@ "eslint-plugin-promise": "^7.2.1", "eslint-plugin-security": "^3.0.1", "jest": "^29.7.0", - "nodemon": "~3.1.9", + "nodemon": "~3.1.10", "prettier": "^3.5.3", "require-json5": "^1.3.0", "rosie": "^2.1.1", diff --git a/backend/public/img/badges/default_trophy.svg b/backend/public/img/badges/default_trophy.svg new file mode 100644 index 000000000..b203cdfc6 --- /dev/null +++ b/backend/public/img/badges/default_trophy.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/backend/public/img/badges/default_verification.svg b/backend/public/img/badges/default_verification.svg new file mode 100644 index 000000000..7bde29f35 --- /dev/null +++ b/backend/public/img/badges/default_verification.svg @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/backend/public/img/badges/indiegogo_en_bear.svg b/backend/public/img/badges/trophy_blue_bear.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_bear.svg rename to backend/public/img/badges/trophy_blue_bear.svg diff --git a/backend/public/img/badges/indiegogo_en_panda.svg b/backend/public/img/badges/trophy_blue_panda.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_panda.svg rename to backend/public/img/badges/trophy_blue_panda.svg diff --git a/backend/public/img/badges/indiegogo_en_rabbit.svg b/backend/public/img/badges/trophy_blue_rabbit.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_rabbit.svg rename to backend/public/img/badges/trophy_blue_rabbit.svg diff --git a/backend/public/img/badges/indiegogo_en_racoon.svg b/backend/public/img/badges/trophy_blue_racoon.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_racoon.svg rename to backend/public/img/badges/trophy_blue_racoon.svg diff --git a/backend/public/img/badges/indiegogo_en_rhino.svg b/backend/public/img/badges/trophy_blue_rhino.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_rhino.svg rename to backend/public/img/badges/trophy_blue_rhino.svg diff --git a/backend/public/img/badges/indiegogo_en_tiger.svg b/backend/public/img/badges/trophy_blue_tiger.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_tiger.svg rename to backend/public/img/badges/trophy_blue_tiger.svg diff --git a/backend/public/img/badges/indiegogo_en_turtle.svg b/backend/public/img/badges/trophy_blue_turtle.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_turtle.svg rename to backend/public/img/badges/trophy_blue_turtle.svg diff --git a/backend/public/img/badges/indiegogo_en_whale.svg b/backend/public/img/badges/trophy_blue_whale.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_whale.svg rename to backend/public/img/badges/trophy_blue_whale.svg diff --git a/backend/public/img/badges/indiegogo_en_wolf.svg b/backend/public/img/badges/trophy_blue_wolf.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_wolf.svg rename to backend/public/img/badges/trophy_blue_wolf.svg diff --git a/backend/public/img/badges/fundraisingbox_de_airship.svg b/backend/public/img/badges/trophy_green_airship.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_airship.svg rename to backend/public/img/badges/trophy_green_airship.svg diff --git a/backend/public/img/badges/fundraisingbox_de_alienship.svg b/backend/public/img/badges/trophy_green_alienship.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_alienship.svg rename to backend/public/img/badges/trophy_green_alienship.svg diff --git a/backend/public/img/badges/fundraisingbox_de_balloon.svg b/backend/public/img/badges/trophy_green_balloon.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_balloon.svg rename to backend/public/img/badges/trophy_green_balloon.svg diff --git a/backend/public/img/badges/wooold_de_bee.svg b/backend/public/img/badges/trophy_green_bee.svg similarity index 100% rename from backend/public/img/badges/wooold_de_bee.svg rename to backend/public/img/badges/trophy_green_bee.svg diff --git a/backend/public/img/badges/fundraisingbox_de_bigballoon.svg b/backend/public/img/badges/trophy_green_bigballoon.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_bigballoon.svg rename to backend/public/img/badges/trophy_green_bigballoon.svg diff --git a/backend/public/img/badges/wooold_de_butterfly.svg b/backend/public/img/badges/trophy_green_butterfly.svg similarity index 100% rename from backend/public/img/badges/wooold_de_butterfly.svg rename to backend/public/img/badges/trophy_green_butterfly.svg diff --git a/backend/public/img/badges/fundraisingbox_de_crane.svg b/backend/public/img/badges/trophy_green_crane.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_crane.svg rename to backend/public/img/badges/trophy_green_crane.svg diff --git a/backend/public/img/badges/wooold_de_double_rainbow.svg b/backend/public/img/badges/trophy_green_doublerainbow.svg similarity index 100% rename from backend/public/img/badges/wooold_de_double_rainbow.svg rename to backend/public/img/badges/trophy_green_doublerainbow.svg diff --git a/backend/public/img/badges/wooold_de_end_of_rainbow.svg b/backend/public/img/badges/trophy_green_endrainbow.svg similarity index 100% rename from backend/public/img/badges/wooold_de_end_of_rainbow.svg rename to backend/public/img/badges/trophy_green_endrainbow.svg diff --git a/backend/public/img/badges/wooold_de_flower.svg b/backend/public/img/badges/trophy_green_flower.svg similarity index 100% rename from backend/public/img/badges/wooold_de_flower.svg rename to backend/public/img/badges/trophy_green_flower.svg diff --git a/backend/public/img/badges/fundraisingbox_de_glider.svg b/backend/public/img/badges/trophy_green_glider.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_glider.svg rename to backend/public/img/badges/trophy_green_glider.svg diff --git a/backend/public/img/badges/fundraisingbox_de_helicopter.svg b/backend/public/img/badges/trophy_green_helicopter.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_helicopter.svg rename to backend/public/img/badges/trophy_green_helicopter.svg diff --git a/backend/public/img/badges/wooold_de_lifetree.svg b/backend/public/img/badges/trophy_green_lifetree.svg similarity index 100% rename from backend/public/img/badges/wooold_de_lifetree.svg rename to backend/public/img/badges/trophy_green_lifetree.svg diff --git a/backend/public/img/badges/wooold_de_magic_rainbow.svg b/backend/public/img/badges/trophy_green_magicrainbow.svg similarity index 100% rename from backend/public/img/badges/wooold_de_magic_rainbow.svg rename to backend/public/img/badges/trophy_green_magicrainbow.svg diff --git a/backend/public/img/badges/fundraisingbox_de_starter.svg b/backend/public/img/badges/trophy_green_starter.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_starter.svg rename to backend/public/img/badges/trophy_green_starter.svg diff --git a/backend/public/img/badges/wooold_de_super_founder.svg b/backend/public/img/badges/trophy_green_superfounder.svg similarity index 100% rename from backend/public/img/badges/wooold_de_super_founder.svg rename to backend/public/img/badges/trophy_green_superfounder.svg diff --git a/backend/public/img/badges/user_role_admin.svg b/backend/public/img/badges/verification_red_admin.svg similarity index 100% rename from backend/public/img/badges/user_role_admin.svg rename to backend/public/img/badges/verification_red_admin.svg diff --git a/backend/public/img/badges/user_role_developer.svg b/backend/public/img/badges/verification_red_developer.svg similarity index 100% rename from backend/public/img/badges/user_role_developer.svg rename to backend/public/img/badges/verification_red_developer.svg diff --git a/backend/public/img/badges/user_role_moderator.svg b/backend/public/img/badges/verification_red_moderator.svg similarity index 100% rename from backend/public/img/badges/user_role_moderator.svg rename to backend/public/img/badges/verification_red_moderator.svg diff --git a/backend/scripts/build.copy.files.sh b/backend/scripts/build.copy.files.sh index da76a623c..7279291d6 100755 --- a/backend/scripts/build.copy.files.sh +++ b/backend/scripts/build.copy.files.sh @@ -14,14 +14,14 @@ mkdir -p build/src/middleware/helpers/email/templates/de/ cp -r src/middleware/helpers/email/templates/de/*.html build/src/middleware/helpers/email/templates/de/ # gql files -mkdir -p build/src/schema/types/ -cp -r src/schema/types/*.gql build/src/schema/types/ +mkdir -p build/src/graphql/types/ +cp -r src/graphql/types/*.gql build/src/graphql/types/ -mkdir -p build/src/schema/types/enum/ -cp -r src/schema/types/enum/*.gql build/src/schema/types/enum/ +mkdir -p build/src/graphql/types/enum/ +cp -r src/graphql/types/enum/*.gql build/src/graphql/types/enum/ -mkdir -p build/src/schema/types/scalar/ -cp -r src/schema/types/scalar/*.gql build/src/schema/types/scalar/ +mkdir -p build/src/graphql/types/scalar/ +cp -r src/graphql/types/scalar/*.gql build/src/graphql/types/scalar/ -mkdir -p build/src/schema/types/type/ -cp -r src/schema/types/type/*.gql build/src/schema/types/type/ \ No newline at end of file +mkdir -p build/src/graphql/types/type/ +cp -r src/graphql/types/type/*.gql build/src/graphql/types/type/ \ No newline at end of file diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 328bbbd61..9b82299ae 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,26 +1,19 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable n/no-process-env */ -/* eslint-disable n/no-unpublished-require */ -/* eslint-disable n/no-missing-require */ import { config } from 'dotenv' import emails from './emails' import metadata from './metadata' // Load env file -if (require.resolve) { - try { - config({ path: require.resolve('../../.env') }) - } catch (error) { - // This error is thrown when the .env is not found - if (error.code !== 'MODULE_NOT_FOUND') { - throw error - } - } -} +config() // Use Cypress env or process.env +// eslint-disable-next-line @typescript-eslint/no-explicit-any declare let Cypress: any | undefined -const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env // eslint-disable-line no-undef +const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env const environment = { NODE_ENV: env.NODE_ENV || process.env.NODE_ENV, @@ -29,7 +22,9 @@ const environment = { PRODUCTION: env.NODE_ENV === 'production', // used for staging enviroments if 'PRODUCTION=true' and 'PRODUCTION_DB_CLEAN_ALLOW=true' PRODUCTION_DB_CLEAN_ALLOW: env.PRODUCTION_DB_CLEAN_ALLOW === 'true' || false, // default = false - DISABLED_MIDDLEWARES: (env.NODE_ENV !== 'production' && env.DISABLED_MIDDLEWARES) || false, + DISABLED_MIDDLEWARES: ['test', 'development'].includes(env.NODE_ENV as string) + ? (env.DISABLED_MIDDLEWARES?.split(',') ?? []) + : [], } const required = { diff --git a/backend/src/config/metadata.ts b/backend/src/config/metadata.ts index 282fcb655..9c87818ae 100644 --- a/backend/src/config/metadata.ts +++ b/backend/src/config/metadata.ts @@ -1,9 +1,10 @@ // this file is duplicated in `backend/src/config/metadata` and `webapp/constants/metadata.js` and replaced on rebranding export default { APPLICATION_NAME: 'ocelot.social', - APPLICATION_SHORT_NAME: 'ocelot', + APPLICATION_SHORT_NAME: 'ocelot.social', APPLICATION_DESCRIPTION: 'ocelot.social Community Network', COOKIE_NAME: 'ocelot-social-token', ORGANIZATION_NAME: 'ocelot.social Community', ORGANIZATION_JURISDICTION: 'City of Angels', + THEME_COLOR: 'rgb(23, 181, 63)', // $color-primary – as the main color in general. e.g. the color in the background of the app that is visible behind the transparent iPhone status bar to name one use case, or the current color of SVGs to name another use case } diff --git a/backend/src/constants/badges.ts b/backend/src/constants/badges.ts new file mode 100644 index 000000000..bccebb39a --- /dev/null +++ b/backend/src/constants/badges.ts @@ -0,0 +1,2 @@ +// this file is duplicated in `backend/src/constants/badges` and `webapp/constants/badges.js` +export const TROPHY_BADGES_SELECTED_MAX = 9 diff --git a/backend/src/constants/registration.ts b/backend/src/constants/registration.ts index a08be3521..8ebb40573 100644 --- a/backend/src/constants/registration.ts +++ b/backend/src/constants/registration.ts @@ -1,5 +1,2 @@ -// this file is duplicated in `backend/src/config/metadata` and `webapp/constants/metadata.js` -export default { - NONCE_LENGTH: 5, - INVITE_CODE_LENGTH: 6, -} +// this file is duplicated in `backend/src/config/registration.ts` and `webapp/constants/registration.js` +export default {} diff --git a/backend/src/constants/registrationBranded.ts b/backend/src/constants/registrationBranded.ts new file mode 100644 index 000000000..2ce1d6965 --- /dev/null +++ b/backend/src/constants/registrationBranded.ts @@ -0,0 +1,12 @@ +// this file is duplicated in `backend/src/config/registrationBranded.ts` and `webapp/constants/registrationBranded.js` +import { merge } from 'lodash' + +import registration from '@constants/registration' + +const defaultRegistration = { + NONCE_LENGTH: 5, + INVITE_CODE_LENGTH: 6, + LAYOUT: 'no-header', +} + +export default merge(defaultRegistration, registration) diff --git a/backend/src/db/admin.ts b/backend/src/db/admin.ts index f1575214f..1f62c8733 100644 --- a/backend/src/db/admin.ts +++ b/backend/src/db/admin.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/require-await */ + import { hashSync } from 'bcryptjs' import { v4 as uuid } from 'uuid' @@ -5,6 +8,7 @@ import { getDriver } from './neo4j' const defaultAdmin = { email: 'admin@example.org', + // eslint-disable-next-line n/no-sync password: hashSync('1234', 10), name: 'admin', id: uuid(), diff --git a/backend/src/db/badges.ts b/backend/src/db/badges.ts new file mode 100644 index 000000000..cbad0b004 --- /dev/null +++ b/backend/src/db/badges.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { getNeode } from './neo4j' +import { trophies, verification } from './seed/badges' + +// eslint-disable-next-line import/newline-after-import +;(async function () { + const neode = getNeode() + try { + await trophies() + await verification() + } finally { + neode.close() + } +})() diff --git a/backend/src/db/categories.ts b/backend/src/db/categories.ts index f550c4d94..a007b25ae 100644 --- a/backend/src/db/categories.ts +++ b/backend/src/db/categories.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/require-await */ + import { categories } from '@constants/categories' import { getDriver } from './neo4j' diff --git a/backend/src/db/compiler.ts b/backend/src/db/compiler.ts index 2d897762f..1b364f919 100644 --- a/backend/src/db/compiler.ts +++ b/backend/src/db/compiler.ts @@ -1,7 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable import/no-commonjs */ -// eslint-disable-next-line n/no-unpublished-require +// eslint-disable-next-line n/no-unpublished-require, @typescript-eslint/no-var-requires const tsNode = require('ts-node') -// eslint-disable-next-line import/no-unassigned-import, import/no-extraneous-dependencies, n/no-unpublished-require +// eslint-disable-next-line import/no-unassigned-import, n/no-unpublished-require require('tsconfig-paths/register') module.exports = tsNode.register diff --git a/backend/src/db/data-branding.ts b/backend/src/db/data-branding.ts new file mode 100644 index 000000000..eceaf391b --- /dev/null +++ b/backend/src/db/data-branding.ts @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { readdir } from 'node:fs/promises' +import path from 'node:path' + +import { getNeode } from './neo4j' + +const dataFolder = path.join(__dirname, 'data/') +const neode = getNeode() + +;(async function () { + const files = await readdir(dataFolder) + for await (const file of files) { + if (file.slice(0, -3).endsWith('-branding')) { + const importedModule = await import(path.join(dataFolder, file)) + if (!importedModule.default) { + throw new Error('Your data file must export a default function') + } + await importedModule.default() + } + } + + // close database connection + neode.close() +})() diff --git a/backend/src/db/data/.gitkeep b/backend/src/db/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/db/factories.ts b/backend/src/db/factories.ts index 136f0fe50..90a666205 100644 --- a/backend/src/db/factories.ts +++ b/backend/src/db/factories.ts @@ -1,10 +1,15 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { faker } from '@faker-js/faker' import { hashSync } from 'bcryptjs' import { Factory } from 'rosie' import slugify from 'slug' import { v4 as uuid } from 'uuid' -import CONFIG from '@config/index' import generateInviteCode from '@schema/resolvers/helpers/generateInviteCode' import { getDriver, getNeode } from './neo4j' @@ -12,7 +17,7 @@ import { getDriver, getNeode } from './neo4j' const neode = getNeode() const uniqueImageUrl = (imageUrl) => { - const newUrl = new URL(imageUrl, CONFIG.CLIENT_URI) + const newUrl = new URL(imageUrl) newUrl.search = `random=${uuid()}` return newUrl.toString() } @@ -32,7 +37,7 @@ export const cleanDatabase = async ({ withMigrations } = { withMigrations: false return transaction.run(clean) }) } finally { - session.close() + await session.close() } } @@ -40,25 +45,34 @@ Factory.define('category') .attr('id', uuid) .attr('icon', 'globe') .attr('name', 'Global Peace & Nonviolence') - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Category', buildObject) }) Factory.define('badge') .attr('type', 'crowdfunding') .attr('status', 'permanent') - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Badge', buildObject) }) Factory.define('image') - .attr('url', faker.image.url) - .attr('aspectRatio', 1.3333333333333333) + .attr('width', 400) + .attr('height', 300) + .attr('blur', 0) .attr('alt', faker.lorem.sentence) .attr('type', 'image/jpeg') - .after((buildObject, options) => { - const { url: imageUrl } = buildObject - if (imageUrl) buildObject.url = uniqueImageUrl(imageUrl) + .attr('url', null) + .after((buildObject, _options) => { + if (!buildObject.url) { + buildObject.url = faker.image.urlPicsumPhotos({ + width: buildObject.width, + height: buildObject.height, + blur: buildObject.blur, + }) + } + buildObject.url = uniqueImageUrl(buildObject.url) + buildObject.aspectRatio = buildObject.width / buildObject.height return neode.create('Image', buildObject) }) @@ -79,27 +93,28 @@ Factory.define('basicUser') return slug || slugify(name, { lower: true }) }) .attr('encryptedPassword', ['password'], (password) => { + // eslint-disable-next-line n/no-sync return hashSync(password, 10) }) Factory.define('userWithoutEmailAddress') .extend('basicUser') .option('about', faker.lorem.paragraph) - .after(async (buildObject, options) => { + .after(async (buildObject, _options) => { return neode.create('User', buildObject) }) Factory.define('userWithAboutNull') .extend('basicUser') .option('about', null) - .after(async (buildObject, options) => { + .after(async (buildObject, _options) => { return neode.create('User', buildObject) }) Factory.define('userWithAboutEmpty') .extend('basicUser') .option('about', '') - .after(async (buildObject, options) => { + .after(async (buildObject, _options) => { return neode.create('User', buildObject) }) @@ -224,7 +239,7 @@ Factory.define('donations') .attr('showDonations', true) .attr('goal', 15000) .attr('progress', 7000) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Donations', buildObject) }) @@ -235,13 +250,13 @@ const emailDefaults = { Factory.define('emailAddress') .attrs(emailDefaults) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('EmailAddress', buildObject) }) Factory.define('unverifiedEmailAddress') .attr(emailDefaults) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('UnverifiedEmailAddress', buildObject) }) @@ -281,11 +296,11 @@ Factory.define('location') id: 'country.10743216036480410', type: 'country', }) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Location', buildObject) }) -Factory.define('report').after((buildObject, options) => { +Factory.define('report').after((buildObject, _options) => { return neode.create('Report', buildObject) }) @@ -293,7 +308,7 @@ Factory.define('tag') .attrs({ name: '#human-connection', }) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Tag', buildObject) }) @@ -301,7 +316,7 @@ Factory.define('socialMedia') .attrs({ url: 'https://mastodon.social/@Gargron', }) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('SocialMedia', buildObject) }) diff --git a/backend/src/db/migrate/store.ts b/backend/src/db/migrate/store.ts index aa8bd66d1..9976be8b4 100644 --- a/backend/src/db/migrate/store.ts +++ b/backend/src/db/migrate/store.ts @@ -1,9 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { getDriver, getNeode } from '@db/neo4j' class Store { async init(errFn) { const neode = getNeode() - const session = neode.driver.session() + const session = neode.session() const txFreshIndicesConstrains = session.writeTransaction(async (txc) => { // drop all indices and constraints await txc.run('CALL apoc.schema.assert({},{},true)') @@ -30,6 +34,9 @@ class Store { // we need to have all constraints and indexes defined here. They can not be properly migrated await txFreshIndicesConstrains + // You have to wait for the schema to install, else the constraints will not be present. + // This is a type error of the library + // eslint-disable-next-line @typescript-eslint/await-thenable await getNeode().schema.install() // eslint-disable-next-line no-console console.log('Successfully created database indices and constraints!') @@ -38,8 +45,8 @@ class Store { console.log(error) // eslint-disable-line no-console errFn(error) } finally { - session.close() - neode.driver.close() + await session.close() + neode.close() } } @@ -68,7 +75,7 @@ class Store { console.log(error) // eslint-disable-line no-console next(error) } finally { - session.close() + await session.close() } } @@ -104,7 +111,7 @@ class Store { console.log(error) // eslint-disable-line no-console next(error) } finally { - session.close() + await session.close() } } } diff --git a/backend/src/db/migrate/template.ts b/backend/src/db/migrate/template.ts index f9eb1a338..a7287dd42 100644 --- a/backend/src/db/migrate/template.ts +++ b/backend/src/db/migrate/template.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { getDriver } from '@db/neo4j' export const description = '' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -11,7 +13,6 @@ export async function up(next) { // Implement your migration here. await transaction.run(``) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -20,11 +21,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -33,7 +34,6 @@ export async function down(next) { // Implement your migration here. await transaction.run(``) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -42,6 +42,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts b/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts index df4cec41e..eda3206b4 100644 --- a/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts +++ b/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable promise/prefer-await-to-callbacks */ import { throwError, concat } from 'rxjs' @@ -17,16 +23,19 @@ export const description = ` ` export function up(next) { const driver = getDriver() - const rxSession = driver.rxSession() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rxSession = driver.rxSession() as any rxSession .beginTransaction() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any flatMap((txc: any) => concat( txc .run('MATCH (email:EmailAddress) RETURN email {.email}') .records() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any map((record: any) => { const { email } = record.get('email') const normalizedEmail = normalizeEmail(email) @@ -48,6 +57,7 @@ export function up(next) { ) .records() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any map((r: any) => ({ oldEmail: email, email: r.get('email'), @@ -61,7 +71,7 @@ export function up(next) { ), ) .subscribe({ - next: ({ user, email, oldUser, oldEmail }) => + next: ({ user, email, _oldUser, oldEmail }) => // eslint-disable-next-line no-console console.log(` Merged: diff --git a/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts b/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts index 89cef62fc..de73bdaae 100644 --- a/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts +++ b/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable promise/prefer-await-to-callbacks */ import { throwError, concat } from 'rxjs' @@ -11,10 +17,12 @@ export const description = ` ` export function up(next) { const driver = getDriver() - const rxSession = driver.rxSession() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rxSession = driver.rxSession() as any rxSession .beginTransaction() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any flatMap((transaction: any) => concat( transaction @@ -26,6 +34,7 @@ export function up(next) { ) .records() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any map((record: any) => { const { id: locationId } = record.get('location') return { locationId } @@ -43,6 +52,7 @@ export function up(next) { ) .records() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any map((record: any) => ({ location: record.get('location'), updatedLocation: record.get('updatedLocation'), diff --git a/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts b/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts index 4743ff175..8be7bad07 100644 --- a/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts +++ b/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + import { getDriver } from '@db/neo4j' export const description = ` @@ -8,7 +10,7 @@ export const description = ` A blocked user will still be able to see your contributions, but will not be able to interact with them and vice versa. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -29,20 +31,20 @@ export async function up(next) { // eslint-disable-next-line no-console console.log('rolled back') } finally { - session.close() + await session.close() } } -export function down(next) { +export async function down(next) { const driver = getDriver() const session = driver.session() try { // Rollback your migration here. - next() + // next() // eslint-disable-next-line no-catch-all/no-catch-all } catch (err) { next(err) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts b/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts index 84e15f9fb..f63be216d 100644 --- a/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts +++ b/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + import { getDriver } from '@db/neo4j' export const description = ` diff --git a/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts b/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts index 79b46a1ff..f2e32d6f8 100644 --- a/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts +++ b/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { getDriver } from '@db/neo4j' export const description = @@ -31,7 +35,7 @@ export async function up(next) { throw new Error(error) } } finally { - session.close() + await session.close() } } @@ -58,6 +62,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts b/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts index 2a30d769e..81d96f68c 100644 --- a/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts +++ b/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { getDriver } from '@db/neo4j' export const description = ` @@ -32,7 +36,7 @@ export async function up(next) { throw new Error(error) } } finally { - session.close() + await session.close() } } @@ -56,6 +60,6 @@ export async function down(next) { // eslint-disable-next-line no-console console.log('rolled back') } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts b/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts index f0531b6c8..0307a2e6e 100644 --- a/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts +++ b/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable security/detect-non-literal-fs-filename */ import https from 'https' import { existsSync, createReadStream } from 'node:fs' @@ -48,6 +53,7 @@ export async function up(next) { const { pathname } = new URL(url, 'http://example.org') const fileLocation = path.join(__dirname, `../../../public/${pathname}`) const s3Location = `original${pathname}` + // eslint-disable-next-line n/no-sync if (existsSync(fileLocation)) { const mimeType = mime.lookup(fileLocation) const params = { @@ -84,7 +90,7 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -106,6 +112,6 @@ export async function down(next) { // eslint-disable-next-line no-console console.log('rolled back') } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts b/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts index 355eb8476..1d24bd141 100644 --- a/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts +++ b/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + /* eslint-disable no-console */ import { getDriver } from '@db/neo4j' @@ -58,7 +61,7 @@ export async function up() { console.log('Created image nodes from all user avatars and post images.') printSummaries(stats) } finally { - session.close() + await session.close() } } @@ -96,6 +99,6 @@ export async function down() { console.log('UNDO: Split images from users and posts.') printSummaries(stats) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts b/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts index 5ce75ab28..b75324a78 100644 --- a/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts +++ b/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + import { getDriver } from '@db/neo4j' export const description = @@ -20,7 +25,7 @@ export async function up(next) { `) try { // Implement your migration here. - const users = await updateDeletedUserAttributes.records.map((record) => record.get('user')) + const users = updateDeletedUserAttributes.records.map((record) => record.get('user')) // eslint-disable-next-line no-console console.log(users) await transaction.commit() @@ -33,7 +38,7 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts b/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts index a2b5ff159..597eb1d83 100644 --- a/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts +++ b/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + import { getDriver } from '@db/neo4j' export const description = @@ -22,7 +27,7 @@ export async function up(next) { `) try { // Implement your migration here. - const posts = await updateDeletedPostsAttributes.records.map((record) => record.get('post')) + const posts = updateDeletedPostsAttributes.records.map((record) => record.get('post')) // eslint-disable-next-line no-console console.log(posts) await transaction.commit() @@ -35,7 +40,7 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts b/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts index 0190ead48..1109ac623 100644 --- a/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts +++ b/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + /* eslint-disable security/detect-non-literal-fs-filename */ import { existsSync } from 'node:fs' @@ -26,6 +32,7 @@ export async function up(next) { const urls = records.map((record) => record.get('url')) const danglingUrls = urls.filter((url) => { const fileLocation = `public${url}` + // eslint-disable-next-line n/no-sync return !existsSync(fileLocation) }) await transaction.run( @@ -54,7 +61,7 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/1613589876420-null_mutation.ts b/backend/src/db/migrations/1613589876420-null_mutation.ts index 8efe667be..daeba5dca 100644 --- a/backend/src/db/migrations/1613589876420-null_mutation.ts +++ b/backend/src/db/migrations/1613589876420-null_mutation.ts @@ -1,9 +1,6 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ 'use strict' -export async function up(next) { - next() -} +export async function up(_next) {} -export async function down(next) { - next() -} +export async function down(_next) {} diff --git a/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts b/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts index ce3515ac7..7443b4749 100644 --- a/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts +++ b/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts @@ -1,10 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { getDriver } from '@db/neo4j' export const description = ` This migration adds the clickedCount property to all posts, setting it to 0. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -15,7 +17,6 @@ export async function up(next) { SET p.clickedCount = 0 `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -24,11 +25,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -39,7 +40,6 @@ export async function down(next) { REMOVE p.clickedCount `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -48,6 +48,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts b/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts index 5615aa4e0..b23bf96bf 100644 --- a/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts +++ b/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts @@ -1,10 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { getDriver } from '@db/neo4j' export const description = ` This migration adds the viewedTeaserCount property to all posts, setting it to 0. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -15,7 +17,6 @@ export async function up(next) { SET p.viewedTeaserCount = 0 `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -24,11 +25,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -39,7 +40,6 @@ export async function down(next) { REMOVE p.viewedTeaserCount `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -48,6 +48,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20210506150512-add-donations-node.ts b/backend/src/db/migrations/20210506150512-add-donations-node.ts index 3d01f28bb..90f00e26f 100644 --- a/backend/src/db/migrations/20210506150512-add-donations-node.ts +++ b/backend/src/db/migrations/20210506150512-add-donations-node.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { v4 as uuid } from 'uuid' import { getDriver } from '@db/neo4j' @@ -5,7 +7,7 @@ import { getDriver } from '@db/neo4j' export const description = 'This migration adds a Donations node with default settings to the database.' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -27,7 +29,6 @@ export async function up(next) { { donationId }, ) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -36,11 +37,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -53,7 +54,6 @@ export async function down(next) { RETURN donationInfo `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -62,6 +62,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts b/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts index bd886db02..5bc6aed17 100644 --- a/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts +++ b/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { getDriver } from '@db/neo4j' export const description = '' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -17,7 +19,6 @@ export async function up(next) { `, ) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -26,11 +27,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -45,7 +46,6 @@ export async function down(next) { `, ) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -54,6 +54,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts b/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts index c53edb9a0..f06c10984 100644 --- a/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts +++ b/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { getDriver } from '@db/neo4j' export const description = ` @@ -5,7 +7,7 @@ export const description = ` Additional we like to have fulltext indices the keys 'name', 'slug', 'about', and 'description'. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -26,7 +28,6 @@ export async function up(next) { `) */ await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -35,11 +36,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -59,7 +60,6 @@ export async function down(next) { `) await transaction.commit() */ - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -68,6 +68,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts b/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts index 765042aad..686d221de 100644 --- a/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts +++ b/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { getDriver } from '@db/neo4j' export const description = '' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -34,7 +36,6 @@ export async function up(next) { ) await transaction.commit() */ - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -43,11 +44,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -60,7 +61,6 @@ export async function down(next) { await transaction.run(`CALL db.index.fulltext.drop("tag_fulltext_search")`) await transaction.commit() */ - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -69,6 +69,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20230329150329-article-label-for-posts.ts b/backend/src/db/migrations/20230329150329-article-label-for-posts.ts index f33aa818a..44433e56b 100644 --- a/backend/src/db/migrations/20230329150329-article-label-for-posts.ts +++ b/backend/src/db/migrations/20230329150329-article-label-for-posts.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { getDriver } from '@db/neo4j' export const description = 'Add to all existing posts the Article label' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -14,7 +16,6 @@ export async function up(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -23,11 +24,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -39,7 +40,6 @@ export async function down(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -48,6 +48,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20230608130637-add-postType-property.ts b/backend/src/db/migrations/20230608130637-add-postType-property.ts index 26c99ce48..1e5474064 100644 --- a/backend/src/db/migrations/20230608130637-add-postType-property.ts +++ b/backend/src/db/migrations/20230608130637-add-postType-property.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { getDriver } from '@db/neo4j' export const description = 'Add postType property Article to all posts' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -14,7 +16,6 @@ export async function up(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -23,11 +24,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -39,7 +40,6 @@ export async function down(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -48,6 +48,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20231017141022-fix-event-dates.ts b/backend/src/db/migrations/20231017141022-fix-event-dates.ts index b2edf17dc..259e3ff65 100644 --- a/backend/src/db/migrations/20231017141022-fix-event-dates.ts +++ b/backend/src/db/migrations/20231017141022-fix-event-dates.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-base-to-string */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ + import { getDriver } from '@db/neo4j' export const description = ` @@ -5,7 +9,7 @@ Transform event start and end date of format 'YYYY-MM-DD HH:MM:SS' in CEST to ISOString in UTC. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -18,11 +22,11 @@ export async function up(next) { `) for (const event of events.records) { let [id, eventStart, eventEnd] = event - let date = new Date(eventStart) + let date = new Date(eventStart as string) date.setHours(date.getHours() - 1) eventStart = date.toISOString() if (eventEnd) { - date = new Date(eventEnd) + date = new Date(eventEnd as string) date.setHours(date.getHours() - 1) eventEnd = date.toISOString() } @@ -34,7 +38,6 @@ export async function up(next) { `) } await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -43,18 +46,17 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() try { // No sense in running this down - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -63,6 +65,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20250331130323-author-observes-own-post.ts b/backend/src/db/migrations/20250331130323-author-observes-own-post.ts index 619b5f1fa..df6eebf23 100644 --- a/backend/src/db/migrations/20250331130323-author-observes-own-post.ts +++ b/backend/src/db/migrations/20250331130323-author-observes-own-post.ts @@ -1,10 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { getDriver } from '@db/neo4j' export const description = ` All authors observe their posts. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -21,7 +23,6 @@ export async function up(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -30,11 +31,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -47,7 +48,6 @@ export async function down(next) { RETURN p `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -56,6 +56,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20250331140313-commenter-observes-post.ts b/backend/src/db/migrations/20250331140313-commenter-observes-post.ts index cc9a82160..ce1d32bc0 100644 --- a/backend/src/db/migrations/20250331140313-commenter-observes-post.ts +++ b/backend/src/db/migrations/20250331140313-commenter-observes-post.ts @@ -1,10 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { getDriver } from '@db/neo4j' export const description = ` All users commenting a post observe the post. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -21,7 +23,6 @@ export async function up(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -30,11 +31,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -48,7 +49,6 @@ export async function down(next) { RETURN p `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -57,6 +57,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20250405030454-email-notification-settings.ts b/backend/src/db/migrations/20250405030454-email-notification-settings.ts index 8b02e866a..eaa9a7a6e 100644 --- a/backend/src/db/migrations/20250405030454-email-notification-settings.ts +++ b/backend/src/db/migrations/20250405030454-email-notification-settings.ts @@ -1,9 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { getDriver } from '@db/neo4j' export const description = 'Transforms the `sendNotificationEmails` property on User to a multi value system' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -22,7 +24,6 @@ export async function up(next) { REMOVE user.sendNotificationEmails `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -31,11 +32,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -54,7 +55,6 @@ export async function down(next) { REMOVE user.emailNotificationsGroupMemberRoleChanged `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -63,6 +63,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20250414220436-delete-old-badges.ts b/backend/src/db/migrations/20250414220436-delete-old-badges.ts new file mode 100644 index 000000000..d03e14619 --- /dev/null +++ b/backend/src/db/migrations/20250414220436-delete-old-badges.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' + +export const description = '' + +export async function up(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + MATCH (badge:Badge) + DETACH DELETE badge + `) + await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} + +export async function down(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // cannot be rolled back + // Implement your migration here. + // await transaction.run(``) + // await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} diff --git a/backend/src/db/neo4j.ts b/backend/src/db/neo4j.ts index c94c552f0..dcd19a0ea 100644 --- a/backend/src/db/neo4j.ts +++ b/backend/src/db/neo4j.ts @@ -1,11 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable import/no-named-as-default-member */ -import neo4j from 'neo4j-driver' +import neo4j, { Driver } from 'neo4j-driver' import Neode from 'neode' import CONFIG from '@config/index' import models from '@models/index' -let driver +let driver: Driver const defaultOptions = { uri: CONFIG.NEO4J_URI, username: CONFIG.NEO4J_USERNAME, @@ -20,7 +23,7 @@ export function getDriver(options = {}) { return driver } -let neodeInstance +let neodeInstance: Neode export function getNeode(options = {}) { if (!neodeInstance) { const { uri, username, password } = { ...defaultOptions, ...options } diff --git a/backend/src/db/reset-with-migrations.ts b/backend/src/db/reset-with-migrations.ts index fc3d86b09..78db831ce 100644 --- a/backend/src/db/reset-with-migrations.ts +++ b/backend/src/db/reset-with-migrations.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable n/no-process-exit */ import CONFIG from '@config/index' diff --git a/backend/src/db/reset.ts b/backend/src/db/reset.ts index 0f316faf8..a381799c6 100644 --- a/backend/src/db/reset.ts +++ b/backend/src/db/reset.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable n/no-process-exit */ import CONFIG from '@config/index' diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index 34a6ebb03..0e2c2c61d 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable n/no-process-exit */ import { faker } from '@faker-js/faker' import { createTestClient } from 'apollo-server-testing' @@ -5,19 +9,18 @@ import sample from 'lodash/sample' import CONFIG from '@config/index' import { categories } from '@constants/categories' -import { createCommentMutation } from '@graphql/comments' -import { - createGroupMutation, - joinGroupMutation, - changeGroupMemberRoleMutation, -} from '@graphql/groups' -import { createMessageMutation } from '@graphql/messages' -import { createPostMutation } from '@graphql/posts' -import { createRoomMutation } from '@graphql/rooms' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createCommentMutation } from '@graphql/queries/createCommentMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createMessageMutation } from '@graphql/queries/createMessageMutation' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { createRoomMutation } from '@graphql/queries/createRoomMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' import createServer from '@src/server' import Factory from './factories' import { getNeode, getDriver } from './neo4j' +import { trophies, verification } from './seed/badges' if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) { throw new Error(`You cannot seed the database in a non-staging and real production environment!`) @@ -25,7 +28,6 @@ if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) { const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] -/* eslint-disable no-multi-spaces */ ;(async function () { let authenticatedUser = null const driver = getDriver() @@ -124,32 +126,28 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await Hamburg.relateTo(Germany, 'isIn') await Paris.relateTo(France, 'isIn') - // badges - const racoon = await Factory.build('badge', { - id: 'indiegogo_en_racoon', - icon: '/img/badges/indiegogo_en_racoon.svg', - }) - const rabbit = await Factory.build('badge', { - id: 'indiegogo_en_rabbit', - icon: '/img/badges/indiegogo_en_rabbit.svg', - }) - const wolf = await Factory.build('badge', { - id: 'indiegogo_en_wolf', - icon: '/img/badges/indiegogo_en_wolf.svg', - }) - const bear = await Factory.build('badge', { - id: 'indiegogo_en_bear', - icon: '/img/badges/indiegogo_en_bear.svg', - }) - const turtle = await Factory.build('badge', { - id: 'indiegogo_en_turtle', - icon: '/img/badges/indiegogo_en_turtle.svg', - }) - const rhino = await Factory.build('badge', { - id: 'indiegogo_en_rhino', - icon: '/img/badges/indiegogo_en_rhino.svg', - }) + const { + trophyAirship, + trophyBee, + trophyStarter, + trophyFlower, + trophyPanda, + trophyTiger, + trophyAlienship, + trophyBalloon, + trophyMagicrainbow, + trophySuperfounder, + trophyBigballoon, + trophyLifetree, + trophyRacoon, + trophyRhino, + trophyWolf, + trophyTurtle, + trophyBear, + trophyRabbit, + } = await trophies() + const { verificationAdmin, verificationModerator, verificationDeveloper } = await verification() // users const peterLustig = await Factory.build( 'user', @@ -243,14 +241,50 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await jennyRostock.relateTo(Paris, 'isIn') await huey.relateTo(Paris, 'isIn') - await peterLustig.relateTo(racoon, 'rewarded') - await peterLustig.relateTo(rhino, 'rewarded') - await peterLustig.relateTo(wolf, 'rewarded') - await bobDerBaumeister.relateTo(racoon, 'rewarded') - await bobDerBaumeister.relateTo(turtle, 'rewarded') - await jennyRostock.relateTo(bear, 'rewarded') - await dagobert.relateTo(rabbit, 'rewarded') + // badges + await peterLustig.relateTo(trophyRacoon, 'rewarded') + await peterLustig.relateTo(trophyRhino, 'rewarded') + await peterLustig.relateTo(trophyWolf, 'rewarded') + await peterLustig.relateTo(trophyAirship, 'rewarded') + await peterLustig.relateTo(verificationAdmin, 'verifies') + await peterLustig.relateTo(trophyRacoon, 'selected', { slot: 0 }) + await peterLustig.relateTo(trophyRhino, 'selected', { slot: 1 }) + await peterLustig.relateTo(trophyAirship, 'selected', { slot: 5 }) + await bobDerBaumeister.relateTo(trophyRacoon, 'rewarded') + await bobDerBaumeister.relateTo(trophyTurtle, 'rewarded') + await bobDerBaumeister.relateTo(trophyBee, 'rewarded') + await bobDerBaumeister.relateTo(verificationModerator, 'verifies') + await bobDerBaumeister.relateTo(trophyRacoon, 'selected', { slot: 1 }) + await bobDerBaumeister.relateTo(trophyTurtle, 'selected', { slot: 2 }) + + await jennyRostock.relateTo(trophyBear, 'rewarded') + await jennyRostock.relateTo(trophyStarter, 'rewarded') + await jennyRostock.relateTo(trophyFlower, 'rewarded') + await jennyRostock.relateTo(trophyBear, 'selected', { slot: 0 }) + await jennyRostock.relateTo(trophyStarter, 'selected', { slot: 1 }) + await jennyRostock.relateTo(trophyFlower, 'selected', { slot: 2 }) + + await huey.relateTo(trophyPanda, 'rewarded') + await huey.relateTo(trophyTiger, 'rewarded') + await huey.relateTo(trophyAlienship, 'rewarded') + await huey.relateTo(trophyBalloon, 'rewarded') + await huey.relateTo(trophyMagicrainbow, 'rewarded') + await huey.relateTo(trophySuperfounder, 'rewarded') + await huey.relateTo(verificationDeveloper, 'verifies') + await huey.relateTo(trophyPanda, 'selected', { slot: 0 }) + await huey.relateTo(trophyTiger, 'selected', { slot: 1 }) + await huey.relateTo(trophyAlienship, 'selected', { slot: 2 }) + + await dewey.relateTo(trophyBigballoon, 'rewarded') + await dewey.relateTo(trophyLifetree, 'rewarded') + await dewey.relateTo(trophyBigballoon, 'selected', { slot: 7 }) + await dewey.relateTo(trophyLifetree, 'selected', { slot: 8 }) + + await louie.relateTo(trophyRabbit, 'rewarded') + await louie.relateTo(trophyRabbit, 'selected', { slot: 4 }) + + // Friends await peterLustig.relateTo(bobDerBaumeister, 'friends') await peterLustig.relateTo(jennyRostock, 'friends') await bobDerBaumeister.relateTo(jennyRostock, 'friends') @@ -635,9 +669,9 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat16'], author: peterLustig, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'food', width: 300, height: 169 }), + width: 300, + height: 169, sensitive: true, - aspectRatio: 300 / 169, }), }, ) @@ -651,8 +685,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat1'], author: bobDerBaumeister, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'technics', width: 300, height: 1500 }), - aspectRatio: 300 / 1500, + width: 300, + height: 1500, }), }, ) @@ -699,8 +733,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat6'], author: peterLustig, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'city', width: 300, height: 857 }), - aspectRatio: 300 / 857, + width: 300, + height: 857, }), }, ) @@ -738,8 +772,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat11'], author: louie, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'people', width: 300, height: 901 }), - aspectRatio: 300 / 901, + width: 300, + height: 901, }), }, ) @@ -764,8 +798,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat14'], author: jennyRostock, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'abstract', width: 300, height: 200 }), - aspectRatio: 300 / 450, + width: 300, + height: 200, }), }, ) @@ -824,7 +858,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] mutation: createPostMutation(), variables: { id: 'p8', - image: faker.image.urlLoremFlickr({ category: 'nature' }), title: `Quantum Flow Theory explains Quantum Gravity`, content: hashtagAndMention1, categoryIds: ['cat8'], @@ -878,6 +911,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] authenticatedUser = null + // eslint-disable-next-line @typescript-eslint/no-explicit-any const comments: any[] = [] comments.push( await Factory.build( @@ -1052,6 +1086,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await huey.relateTo(p9, 'shouted') await louie.relateTo(p10, 'shouted') + // eslint-disable-next-line @typescript-eslint/no-explicit-any const reports: any[] = [] reports.push( await Factory.build('report'), @@ -1159,6 +1194,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] closed: true, }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any const additionalUsers: any[] = [] for (let i = 0; i < 30; i++) { const user = await Factory.build('user') @@ -1180,9 +1216,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: jennyRostock, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'abstract' }), - }), }, ) } @@ -1231,9 +1264,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: peterLustig, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'city' }), - }), }, ) } @@ -1282,9 +1312,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: dewey, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'food' }), - }), }, ) } @@ -1333,9 +1360,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: louie, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'technics' }), - }), }, ) } @@ -1384,9 +1408,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: bobDerBaumeister, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'people' }), - }), }, ) } @@ -1435,9 +1456,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: huey, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'nature' }), - }), }, ) } @@ -1566,7 +1584,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] /* eslint-disable-next-line no-console */ console.log('Seeded Data...') await driver.close() - await neode.close() + neode.close() process.exit(0) // eslint-disable-next-line no-catch-all/no-catch-all } catch (err) { @@ -1575,4 +1593,3 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] process.exit(1) } })() -/* eslint-enable no-multi-spaces */ diff --git a/backend/src/db/seed/badges.ts b/backend/src/db/seed/badges.ts new file mode 100644 index 000000000..ce8bff9ba --- /dev/null +++ b/backend/src/db/seed/badges.ts @@ -0,0 +1,188 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import Factory from '@db/factories' + +export const trophies = async () => { + return { + // Blue Animals + trophyBear: await Factory.build('badge', { + id: 'trophy_bear', + type: 'trophy', + description: 'You earned a Bear', + icon: '/img/badges/trophy_blue_bear.svg', + }), + trophyPanda: await Factory.build('badge', { + id: 'trophy_panda', + type: 'trophy', + description: 'You earned a Panda', + icon: '/img/badges/trophy_blue_panda.svg', + }), + trophyRabbit: await Factory.build('badge', { + id: 'trophy_rabbit', + type: 'trophy', + description: 'You earned a Rabbit', + icon: '/img/badges/trophy_blue_rabbit.svg', + }), + trophyRacoon: await Factory.build('badge', { + id: 'trophy_racoon', + type: 'trophy', + description: 'You earned a Racoon', + icon: '/img/badges/trophy_blue_racoon.svg', + }), + trophyRhino: await Factory.build('badge', { + id: 'trophy_rhino', + type: 'trophy', + description: 'You earned a Rhino', + icon: '/img/badges/trophy_blue_rhino.svg', + }), + trophyTiger: await Factory.build('badge', { + id: 'trophy_tiger', + type: 'trophy', + description: 'You earned a Tiger', + icon: '/img/badges/trophy_blue_tiger.svg', + }), + trophyTurtle: await Factory.build('badge', { + id: 'trophy_turtle', + type: 'trophy', + description: 'You earned a Turtle', + icon: '/img/badges/trophy_blue_turtle.svg', + }), + trophyWhale: await Factory.build('badge', { + id: 'trophy_whale', + type: 'trophy', + description: 'You earned a Whale', + icon: '/img/badges/trophy_blue_whale.svg', + }), + trophyWolf: await Factory.build('badge', { + id: 'trophy_wolf', + type: 'trophy', + description: 'You earned a Wolf', + icon: '/img/badges/trophy_blue_wolf.svg', + }), + // Green Transports + trophyAirship: await Factory.build('badge', { + id: 'trophy_airship', + type: 'trophy', + description: 'You earned an Airship', + icon: '/img/badges/trophy_green_airship.svg', + }), + trophyAlienship: await Factory.build('badge', { + id: 'trophy_alienship', + type: 'trophy', + description: 'You earned an Alienship', + icon: '/img/badges/trophy_green_alienship.svg', + }), + trophyBalloon: await Factory.build('badge', { + id: 'trophy_balloon', + type: 'trophy', + description: 'You earned a Balloon', + icon: '/img/badges/trophy_green_balloon.svg', + }), + trophyBigballoon: await Factory.build('badge', { + id: 'trophy_bigballoon', + type: 'trophy', + description: 'You earned a Big Balloon', + icon: '/img/badges/trophy_green_bigballoon.svg', + }), + trophyCrane: await Factory.build('badge', { + id: 'trophy_crane', + type: 'trophy', + description: 'You earned a Crane', + icon: '/img/badges/trophy_green_crane.svg', + }), + trophyGlider: await Factory.build('badge', { + id: 'trophy_glider', + type: 'trophy', + description: 'You earned a Glider', + icon: '/img/badges/trophy_green_glider.svg', + }), + trophyHelicopter: await Factory.build('badge', { + id: 'trophy_helicopter', + type: 'trophy', + description: 'You earned a Helicopter', + icon: '/img/badges/trophy_green_helicopter.svg', + }), + // Green Animals + trophyBee: await Factory.build('badge', { + id: 'trophy_bee', + type: 'trophy', + description: 'You earned a Bee', + icon: '/img/badges/trophy_green_bee.svg', + }), + trophyButterfly: await Factory.build('badge', { + id: 'trophy_butterfly', + type: 'trophy', + description: 'You earned a Butterfly', + icon: '/img/badges/trophy_green_butterfly.svg', + }), + // Green Plants + trophyFlower: await Factory.build('badge', { + id: 'trophy_flower', + type: 'trophy', + description: 'You earned a Flower', + icon: '/img/badges/trophy_green_flower.svg', + }), + trophyLifetree: await Factory.build('badge', { + id: 'trophy_lifetree', + type: 'trophy', + description: 'You earned the tree of life', + icon: '/img/badges/trophy_green_lifetree.svg', + }), + // Green Misc + trophyDoublerainbow: await Factory.build('badge', { + id: 'trophy_doublerainbow', + type: 'trophy', + description: 'You earned the Double Rainbow', + icon: '/img/badges/trophy_green_doublerainbow.svg', + }), + trophyEndrainbow: await Factory.build('badge', { + id: 'trophy_endrainbow', + type: 'trophy', + description: 'You earned the End of the Rainbow', + icon: '/img/badges/trophy_green_endrainbow.svg', + }), + trophyMagicrainbow: await Factory.build('badge', { + id: 'trophy_magicrainbow', + type: 'trophy', + description: 'You earned the Magic Rainbow', + icon: '/img/badges/trophy_green_magicrainbow.svg', + }), + trophyStarter: await Factory.build('badge', { + id: 'trophy_starter', + type: 'trophy', + description: 'You earned the Starter Badge', + icon: '/img/badges/trophy_green_starter.svg', + }), + trophySuperfounder: await Factory.build('badge', { + id: 'trophy_superfounder', + type: 'trophy', + description: 'You earned the Super Founder Badge', + icon: '/img/badges/trophy_green_superfounder.svg', + }), + } +} + +export const verification = async () => { + return { + // Red Role + verificationModerator: await Factory.build('badge', { + id: 'verification_moderator', + type: 'verification', + description: 'You are a Moderator', + icon: '/img/badges/verification_red_moderator.svg', + }), + verificationAdmin: await Factory.build('badge', { + id: 'verification_admin', + type: 'verification', + description: 'You are an Administrator', + icon: '/img/badges/verification_red_admin.svg', + }), + verificationDeveloper: await Factory.build('badge', { + id: 'verification_developer', + type: 'verification', + description: 'You are a Developer', + icon: '/img/badges/verification_red_developer.svg', + }), + } +} diff --git a/backend/src/graphql/groups.ts b/backend/src/graphql/groups.ts deleted file mode 100644 index a7cfc3351..000000000 --- a/backend/src/graphql/groups.ts +++ /dev/null @@ -1,216 +0,0 @@ -import gql from 'graphql-tag' - -// ------ mutations - -export const createGroupMutation = () => { - return gql` - mutation ( - $id: ID - $name: String! - $slug: String - $about: String - $description: String! - $groupType: GroupType! - $actionRadius: GroupActionRadius! - $categoryIds: [ID] - $locationName: String # empty string '' sets it to null - ) { - CreateGroup( - id: $id - name: $name - slug: $slug - about: $about - description: $description - groupType: $groupType - actionRadius: $actionRadius - categoryIds: $categoryIds - locationName: $locationName - ) { - id - name - slug - createdAt - updatedAt - disabled - deleted - about - description - descriptionExcerpt - groupType - actionRadius - categories { - id - slug - name - icon - } - locationName - location { - name - nameDE - nameEN - } - myRole - } - } - ` -} - -export const updateGroupMutation = () => { - return gql` - mutation ( - $id: ID! - $name: String - $slug: String - $about: String - $description: String - $actionRadius: GroupActionRadius - $categoryIds: [ID] - $avatar: ImageInput - $locationName: String # empty string '' sets it to null - ) { - UpdateGroup( - id: $id - name: $name - slug: $slug - about: $about - description: $description - actionRadius: $actionRadius - categoryIds: $categoryIds - avatar: $avatar - locationName: $locationName - ) { - id - name - slug - createdAt - updatedAt - disabled - deleted - about - description - descriptionExcerpt - groupType - actionRadius - categories { - id - slug - name - icon - } - # avatar # test this as result - locationName - location { - name - nameDE - nameEN - } - myRole - } - } - ` -} - -export const joinGroupMutation = () => { - return gql` - mutation ($groupId: ID!, $userId: ID!) { - JoinGroup(groupId: $groupId, userId: $userId) { - id - name - slug - myRoleInGroup - } - } - ` -} - -export const leaveGroupMutation = () => { - return gql` - mutation ($groupId: ID!, $userId: ID!) { - LeaveGroup(groupId: $groupId, userId: $userId) { - id - name - slug - myRoleInGroup - } - } - ` -} - -export const changeGroupMemberRoleMutation = () => { - return gql` - mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) { - ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) { - id - name - slug - myRoleInGroup - } - } - ` -} - -export const removeUserFromGroupMutation = () => { - return gql` - mutation ($groupId: ID!, $userId: ID!) { - RemoveUserFromGroup(groupId: $groupId, userId: $userId) { - id - name - slug - myRoleInGroup - } - } - ` -} - -// ------ queries - -export const groupQuery = () => { - return gql` - query ($isMember: Boolean, $id: ID, $slug: String) { - Group(isMember: $isMember, id: $id, slug: $slug) { - id - name - slug - createdAt - updatedAt - disabled - deleted - about - description - descriptionExcerpt - groupType - actionRadius - categories { - id - slug - name - icon - } - avatar { - url - } - locationName - location { - name - nameDE - nameEN - } - myRole - } - } - ` -} - -export const groupMembersQuery = () => { - return gql` - query ($id: ID!) { - GroupMembers(id: $id) { - id - name - slug - myRoleInGroup - } - } - ` -} diff --git a/backend/src/graphql/messages.ts b/backend/src/graphql/messages.ts deleted file mode 100644 index 2842c7230..000000000 --- a/backend/src/graphql/messages.ts +++ /dev/null @@ -1,50 +0,0 @@ -import gql from 'graphql-tag' - -export const createMessageMutation = () => { - return gql` - mutation ($roomId: ID!, $content: String!) { - CreateMessage(roomId: $roomId, content: $content) { - id - content - senderId - username - avatar - date - saved - distributed - seen - } - } - ` -} - -export const messageQuery = () => { - return gql` - query ($roomId: ID!, $first: Int, $offset: Int) { - Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) { - _id - id - indexId - content - senderId - author { - id - } - username - avatar - date - saved - distributed - seen - } - } - ` -} - -export const markMessagesAsSeen = () => { - return gql` - mutation ($messageIds: [String!]) { - MarkMessagesAsSeen(messageIds: $messageIds) - } - ` -} diff --git a/backend/src/graphql/notifications.ts b/backend/src/graphql/notifications.ts deleted file mode 100644 index 233077372..000000000 --- a/backend/src/graphql/notifications.ts +++ /dev/null @@ -1,65 +0,0 @@ -import gql from 'graphql-tag' - -// ------ mutations - -export const markAsReadMutation = () => { - return gql` - mutation ($id: ID!) { - markAsRead(id: $id) { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` -} - -export const markAllAsReadMutation = () => { - return gql` - mutation { - markAllAsRead { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` -} - -// ------ queries - -export const notificationQuery = () => { - return gql` - query ($read: Boolean, $orderBy: NotificationOrdering) { - notifications(read: $read, orderBy: $orderBy) { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` -} diff --git a/backend/src/graphql/posts.ts b/backend/src/graphql/posts.ts deleted file mode 100644 index dcd75a4ff..000000000 --- a/backend/src/graphql/posts.ts +++ /dev/null @@ -1,113 +0,0 @@ -import gql from 'graphql-tag' - -// ------ mutations - -export const createPostMutation = () => { - return gql` - mutation ( - $id: ID - $title: String! - $slug: String - $content: String! - $categoryIds: [ID] - $groupId: ID - $postType: PostType - $eventInput: _EventInput - ) { - CreatePost( - id: $id - title: $title - slug: $slug - content: $content - categoryIds: $categoryIds - groupId: $groupId - postType: $postType - eventInput: $eventInput - ) { - id - slug - title - content - disabled - deleted - postType - author { - name - } - categories { - id - } - eventStart - eventEnd - eventLocationName - eventVenue - eventIsOnline - eventLocation { - lng - lat - } - isObservedByMe - observingUsersCount - } - } - ` -} - -// ------ queries - -export const postQuery = () => { - return gql` - query Post($id: ID!) { - Post(id: $id) { - id - title - content - } - } - ` -} - -export const filterPosts = () => { - return gql` - query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) { - Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { - id - title - content - eventStart - } - } - ` -} - -export const profilePagePosts = () => { - return gql` - query profilePagePosts( - $filter: _PostFilter - $first: Int - $offset: Int - $orderBy: [_PostOrdering] - ) { - profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { - id - title - content - } - } - ` -} - -export const searchPosts = () => { - return gql` - query ($query: String!, $firstPosts: Int, $postsOffset: Int) { - searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) { - postCount - posts { - id - title - content - } - } - } - ` -} diff --git a/backend/src/graphql/queries/changeGroupMemberRoleMutation.ts b/backend/src/graphql/queries/changeGroupMemberRoleMutation.ts new file mode 100644 index 000000000..a01c19cfb --- /dev/null +++ b/backend/src/graphql/queries/changeGroupMemberRoleMutation.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const changeGroupMemberRoleMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) { + ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/comments.ts b/backend/src/graphql/queries/createCommentMutation.ts similarity index 76% rename from backend/src/graphql/comments.ts rename to backend/src/graphql/queries/createCommentMutation.ts index b408c5e95..c3824e1d0 100644 --- a/backend/src/graphql/comments.ts +++ b/backend/src/graphql/queries/createCommentMutation.ts @@ -1,7 +1,5 @@ import gql from 'graphql-tag' -// ------ mutations - export const createCommentMutation = gql` mutation ($id: ID, $postId: ID!, $content: String!) { CreateComment(id: $id, postId: $postId, content: $content) { @@ -9,7 +7,3 @@ export const createCommentMutation = gql` } } ` - -// ------ queries - -// fill queries in here diff --git a/backend/src/graphql/queries/createGroupMutation.ts b/backend/src/graphql/queries/createGroupMutation.ts new file mode 100644 index 000000000..20cd93323 --- /dev/null +++ b/backend/src/graphql/queries/createGroupMutation.ts @@ -0,0 +1,55 @@ +import gql from 'graphql-tag' + +export const createGroupMutation = () => { + return gql` + mutation ( + $id: ID + $name: String! + $slug: String + $about: String + $description: String! + $groupType: GroupType! + $actionRadius: GroupActionRadius! + $categoryIds: [ID] + $locationName: String # empty string '' sets it to null + ) { + CreateGroup( + id: $id + name: $name + slug: $slug + about: $about + description: $description + groupType: $groupType + actionRadius: $actionRadius + categoryIds: $categoryIds + locationName: $locationName + ) { + id + name + slug + createdAt + updatedAt + disabled + deleted + about + description + descriptionExcerpt + groupType + actionRadius + categories { + id + slug + name + icon + } + locationName + location { + name + nameDE + nameEN + } + myRole + } + } + ` +} diff --git a/backend/src/graphql/queries/createMessageMutation.ts b/backend/src/graphql/queries/createMessageMutation.ts new file mode 100644 index 000000000..e8c6fc7b8 --- /dev/null +++ b/backend/src/graphql/queries/createMessageMutation.ts @@ -0,0 +1,19 @@ +import gql from 'graphql-tag' + +export const createMessageMutation = () => { + return gql` + mutation ($roomId: ID!, $content: String!) { + CreateMessage(roomId: $roomId, content: $content) { + id + content + senderId + username + avatar + date + saved + distributed + seen + } + } + ` +} diff --git a/backend/src/graphql/queries/createPostMutation.ts b/backend/src/graphql/queries/createPostMutation.ts new file mode 100644 index 000000000..f0a01b303 --- /dev/null +++ b/backend/src/graphql/queries/createPostMutation.ts @@ -0,0 +1,52 @@ +import gql from 'graphql-tag' + +export const createPostMutation = () => { + return gql` + mutation ( + $id: ID + $title: String! + $slug: String + $content: String! + $categoryIds: [ID] + $groupId: ID + $postType: PostType + $eventInput: _EventInput + ) { + CreatePost( + id: $id + title: $title + slug: $slug + content: $content + categoryIds: $categoryIds + groupId: $groupId + postType: $postType + eventInput: $eventInput + ) { + id + slug + title + content + disabled + deleted + postType + author { + name + } + categories { + id + } + eventStart + eventEnd + eventLocationName + eventVenue + eventIsOnline + eventLocation { + lng + lat + } + isObservedByMe + observingUsersCount + } + } + ` +} diff --git a/backend/src/graphql/queries/createRoomMutation.ts b/backend/src/graphql/queries/createRoomMutation.ts new file mode 100644 index 000000000..3a791d294 --- /dev/null +++ b/backend/src/graphql/queries/createRoomMutation.ts @@ -0,0 +1,24 @@ +import gql from 'graphql-tag' + +export const createRoomMutation = () => { + return gql` + mutation ($userId: ID!) { + CreateRoom(userId: $userId) { + id + roomId + roomName + lastMessageAt + unreadCount + #avatar + users { + _id + id + name + avatar { + url + } + } + } + } + ` +} diff --git a/backend/src/graphql/queries/filterPosts.ts b/backend/src/graphql/queries/filterPosts.ts new file mode 100644 index 000000000..7e6d5059f --- /dev/null +++ b/backend/src/graphql/queries/filterPosts.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const filterPosts = () => { + return gql` + query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) { + Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { + id + title + content + eventStart + } + } + ` +} diff --git a/backend/src/graphql/queries/groupMembersQuery.ts b/backend/src/graphql/queries/groupMembersQuery.ts new file mode 100644 index 000000000..b1b8cb313 --- /dev/null +++ b/backend/src/graphql/queries/groupMembersQuery.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const groupMembersQuery = () => { + return gql` + query ($id: ID!) { + GroupMembers(id: $id) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/queries/groupQuery.ts b/backend/src/graphql/queries/groupQuery.ts new file mode 100644 index 000000000..463e9e13e --- /dev/null +++ b/backend/src/graphql/queries/groupQuery.ts @@ -0,0 +1,38 @@ +import gql from 'graphql-tag' + +export const groupQuery = () => { + return gql` + query ($isMember: Boolean, $id: ID, $slug: String) { + Group(isMember: $isMember, id: $id, slug: $slug) { + id + name + slug + createdAt + updatedAt + disabled + deleted + about + description + descriptionExcerpt + groupType + actionRadius + categories { + id + slug + name + icon + } + avatar { + url + } + locationName + location { + name + nameDE + nameEN + } + myRole + } + } + ` +} diff --git a/backend/src/graphql/queries/joinGroupMutation.ts b/backend/src/graphql/queries/joinGroupMutation.ts new file mode 100644 index 000000000..ce627b1ef --- /dev/null +++ b/backend/src/graphql/queries/joinGroupMutation.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const joinGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + JoinGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/queries/leaveGroupMutation.ts b/backend/src/graphql/queries/leaveGroupMutation.ts new file mode 100644 index 000000000..470bd6a7a --- /dev/null +++ b/backend/src/graphql/queries/leaveGroupMutation.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const leaveGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + LeaveGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/userManagement.ts b/backend/src/graphql/queries/loginMutation.ts similarity index 72% rename from backend/src/graphql/userManagement.ts rename to backend/src/graphql/queries/loginMutation.ts index 3cb8a05f8..8c7b36f12 100644 --- a/backend/src/graphql/userManagement.ts +++ b/backend/src/graphql/queries/loginMutation.ts @@ -1,13 +1,7 @@ import gql from 'graphql-tag' -// ------ mutations - export const loginMutation = gql` mutation ($email: String!, $password: String!) { login(email: $email, password: $password) } ` - -// ------ queries - -// fill queries in here diff --git a/backend/src/graphql/queries/markAllAsReadMutation.ts b/backend/src/graphql/queries/markAllAsReadMutation.ts new file mode 100644 index 000000000..d1f19e369 --- /dev/null +++ b/backend/src/graphql/queries/markAllAsReadMutation.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag' + +export const markAllAsReadMutation = () => { + return gql` + mutation { + markAllAsRead { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` +} diff --git a/backend/src/graphql/queries/markAsReadMutation.ts b/backend/src/graphql/queries/markAsReadMutation.ts new file mode 100644 index 000000000..fd855665a --- /dev/null +++ b/backend/src/graphql/queries/markAsReadMutation.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag' + +export const markAsReadMutation = () => { + return gql` + mutation ($id: ID!) { + markAsRead(id: $id) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` +} diff --git a/backend/src/graphql/queries/markMessagesAsSeen.ts b/backend/src/graphql/queries/markMessagesAsSeen.ts new file mode 100644 index 000000000..9081c5def --- /dev/null +++ b/backend/src/graphql/queries/markMessagesAsSeen.ts @@ -0,0 +1,9 @@ +import gql from 'graphql-tag' + +export const markMessagesAsSeen = () => { + return gql` + mutation ($messageIds: [String!]) { + MarkMessagesAsSeen(messageIds: $messageIds) + } + ` +} diff --git a/backend/src/graphql/queries/messageQuery.ts b/backend/src/graphql/queries/messageQuery.ts new file mode 100644 index 000000000..791851121 --- /dev/null +++ b/backend/src/graphql/queries/messageQuery.ts @@ -0,0 +1,24 @@ +import gql from 'graphql-tag' + +export const messageQuery = () => { + return gql` + query ($roomId: ID!, $first: Int, $offset: Int) { + Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) { + _id + id + indexId + content + senderId + author { + id + } + username + avatar + date + saved + distributed + seen + } + } + ` +} diff --git a/backend/src/graphql/queries/notificationQuery.ts b/backend/src/graphql/queries/notificationQuery.ts new file mode 100644 index 000000000..965fb9ce9 --- /dev/null +++ b/backend/src/graphql/queries/notificationQuery.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag' + +export const notificationQuery = () => { + return gql` + query ($read: Boolean, $orderBy: NotificationOrdering) { + notifications(read: $read, orderBy: $orderBy) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` +} diff --git a/backend/src/graphql/queries/postQuery.ts b/backend/src/graphql/queries/postQuery.ts new file mode 100644 index 000000000..ff8faf311 --- /dev/null +++ b/backend/src/graphql/queries/postQuery.ts @@ -0,0 +1,13 @@ +import gql from 'graphql-tag' + +export const postQuery = () => { + return gql` + query Post($id: ID!) { + Post(id: $id) { + id + title + content + } + } + ` +} diff --git a/backend/src/graphql/queries/profilePagePosts.ts b/backend/src/graphql/queries/profilePagePosts.ts new file mode 100644 index 000000000..5d713a23c --- /dev/null +++ b/backend/src/graphql/queries/profilePagePosts.ts @@ -0,0 +1,18 @@ +import gql from 'graphql-tag' + +export const profilePagePosts = () => { + return gql` + query profilePagePosts( + $filter: _PostFilter + $first: Int + $offset: Int + $orderBy: [_PostOrdering] + ) { + profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { + id + title + content + } + } + ` +} diff --git a/backend/src/graphql/queries/removeUserFromGroupMutation.ts b/backend/src/graphql/queries/removeUserFromGroupMutation.ts new file mode 100644 index 000000000..bdb9792d9 --- /dev/null +++ b/backend/src/graphql/queries/removeUserFromGroupMutation.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const removeUserFromGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + RemoveUserFromGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/rooms.ts b/backend/src/graphql/queries/roomQuery.ts similarity index 58% rename from backend/src/graphql/rooms.ts rename to backend/src/graphql/queries/roomQuery.ts index 7612641f3..01b24654e 100644 --- a/backend/src/graphql/rooms.ts +++ b/backend/src/graphql/queries/roomQuery.ts @@ -1,28 +1,5 @@ import gql from 'graphql-tag' -export const createRoomMutation = () => { - return gql` - mutation ($userId: ID!) { - CreateRoom(userId: $userId) { - id - roomId - roomName - lastMessageAt - unreadCount - #avatar - users { - _id - id - name - avatar { - url - } - } - } - } - ` -} - export const roomQuery = () => { return gql` query Room($first: Int, $offset: Int, $id: ID) { @@ -57,11 +34,3 @@ export const roomQuery = () => { } ` } - -export const unreadRoomsQuery = () => { - return gql` - query { - UnreadRooms - } - ` -} diff --git a/backend/src/graphql/queries/searchPosts.ts b/backend/src/graphql/queries/searchPosts.ts new file mode 100644 index 000000000..ed9e9a641 --- /dev/null +++ b/backend/src/graphql/queries/searchPosts.ts @@ -0,0 +1,16 @@ +import gql from 'graphql-tag' + +export const searchPosts = () => { + return gql` + query ($query: String!, $firstPosts: Int, $postsOffset: Int) { + searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) { + postCount + posts { + id + title + content + } + } + } + ` +} diff --git a/backend/src/graphql/authentications.ts b/backend/src/graphql/queries/signupVerificationMutation.ts similarity index 88% rename from backend/src/graphql/authentications.ts rename to backend/src/graphql/queries/signupVerificationMutation.ts index 91605ec9f..f504da0ce 100644 --- a/backend/src/graphql/authentications.ts +++ b/backend/src/graphql/queries/signupVerificationMutation.ts @@ -1,7 +1,5 @@ import gql from 'graphql-tag' -// ------ mutations - export const signupVerificationMutation = gql` mutation ( $password: String! @@ -24,7 +22,3 @@ export const signupVerificationMutation = gql` } } ` - -// ------ queries - -// fill queries in here diff --git a/backend/src/graphql/queries/unreadRoomsQuery.ts b/backend/src/graphql/queries/unreadRoomsQuery.ts new file mode 100644 index 000000000..d5612dcad --- /dev/null +++ b/backend/src/graphql/queries/unreadRoomsQuery.ts @@ -0,0 +1,9 @@ +import gql from 'graphql-tag' + +export const unreadRoomsQuery = () => { + return gql` + query { + UnreadRooms + } + ` +} diff --git a/backend/src/graphql/queries/updateGroupMutation.ts b/backend/src/graphql/queries/updateGroupMutation.ts new file mode 100644 index 000000000..826a9c9d4 --- /dev/null +++ b/backend/src/graphql/queries/updateGroupMutation.ts @@ -0,0 +1,56 @@ +import gql from 'graphql-tag' + +export const updateGroupMutation = () => { + return gql` + mutation ( + $id: ID! + $name: String + $slug: String + $about: String + $description: String + $actionRadius: GroupActionRadius + $categoryIds: [ID] + $avatar: ImageInput + $locationName: String # empty string '' sets it to null + ) { + UpdateGroup( + id: $id + name: $name + slug: $slug + about: $about + description: $description + actionRadius: $actionRadius + categoryIds: $categoryIds + avatar: $avatar + locationName: $locationName + ) { + id + name + slug + createdAt + updatedAt + disabled + deleted + about + description + descriptionExcerpt + groupType + actionRadius + categories { + id + slug + name + icon + } + # avatar # test this as result + locationName + location { + name + nameDE + nameEN + } + myRole + } + } + ` +} diff --git a/backend/src/schema/types/enum/EmailNotificationSettingsName.gql b/backend/src/graphql/types/enum/EmailNotificationSettingsName.gql similarity index 100% rename from backend/src/schema/types/enum/EmailNotificationSettingsName.gql rename to backend/src/graphql/types/enum/EmailNotificationSettingsName.gql diff --git a/backend/src/schema/types/enum/EmailNotificationSettingsType.gql b/backend/src/graphql/types/enum/EmailNotificationSettingsType.gql similarity index 100% rename from backend/src/schema/types/enum/EmailNotificationSettingsType.gql rename to backend/src/graphql/types/enum/EmailNotificationSettingsType.gql diff --git a/backend/src/schema/types/enum/Emotion.gql b/backend/src/graphql/types/enum/Emotion.gql similarity index 100% rename from backend/src/schema/types/enum/Emotion.gql rename to backend/src/graphql/types/enum/Emotion.gql diff --git a/backend/src/schema/types/enum/GroupActionRadius.gql b/backend/src/graphql/types/enum/GroupActionRadius.gql similarity index 100% rename from backend/src/schema/types/enum/GroupActionRadius.gql rename to backend/src/graphql/types/enum/GroupActionRadius.gql diff --git a/backend/src/schema/types/enum/GroupMemberRole.gql b/backend/src/graphql/types/enum/GroupMemberRole.gql similarity index 100% rename from backend/src/schema/types/enum/GroupMemberRole.gql rename to backend/src/graphql/types/enum/GroupMemberRole.gql diff --git a/backend/src/schema/types/enum/GroupType.gql b/backend/src/graphql/types/enum/GroupType.gql similarity index 100% rename from backend/src/schema/types/enum/GroupType.gql rename to backend/src/graphql/types/enum/GroupType.gql diff --git a/backend/src/schema/types/enum/OnlineStatus.gql b/backend/src/graphql/types/enum/OnlineStatus.gql similarity index 100% rename from backend/src/schema/types/enum/OnlineStatus.gql rename to backend/src/graphql/types/enum/OnlineStatus.gql diff --git a/backend/src/schema/types/enum/PostType.gql b/backend/src/graphql/types/enum/PostType.gql similarity index 100% rename from backend/src/schema/types/enum/PostType.gql rename to backend/src/graphql/types/enum/PostType.gql diff --git a/backend/src/graphql/types/enum/ShoutTypeEnum.gql b/backend/src/graphql/types/enum/ShoutTypeEnum.gql new file mode 100644 index 000000000..87fcbc5ff --- /dev/null +++ b/backend/src/graphql/types/enum/ShoutTypeEnum.gql @@ -0,0 +1,3 @@ +enum ShoutTypeEnum { + Post +} \ No newline at end of file diff --git a/backend/src/schema/types/enum/UserRole.gql b/backend/src/graphql/types/enum/UserRole.gql similarity index 100% rename from backend/src/schema/types/enum/UserRole.gql rename to backend/src/graphql/types/enum/UserRole.gql diff --git a/backend/src/schema/types/enum/Visibility.gql b/backend/src/graphql/types/enum/Visibility.gql similarity index 100% rename from backend/src/schema/types/enum/Visibility.gql rename to backend/src/graphql/types/enum/Visibility.gql diff --git a/backend/src/schema/types/index.ts b/backend/src/graphql/types/index.ts similarity index 100% rename from backend/src/schema/types/index.ts rename to backend/src/graphql/types/index.ts diff --git a/backend/src/schema/types/scalar/Upload.gql b/backend/src/graphql/types/scalar/Upload.gql similarity index 100% rename from backend/src/schema/types/scalar/Upload.gql rename to backend/src/graphql/types/scalar/Upload.gql diff --git a/backend/src/graphql/types/type/Badge.gql b/backend/src/graphql/types/type/Badge.gql new file mode 100644 index 000000000..8cdad2ee7 --- /dev/null +++ b/backend/src/graphql/types/type/Badge.gql @@ -0,0 +1,26 @@ +type Badge { + id: ID! + type: BadgeType! + icon: String! + createdAt: String + description: String! + isDefault: Boolean! + + rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") + verifies: [User]! @relation(name: "VERIFIES", direction: "OUT") +} + +enum BadgeType { + verification + trophy +} + +type Query { + Badge: [Badge] +} + +type Mutation { + setVerificationBadge(badgeId: ID!, userId: ID!): User + rewardTrophyBadge(badgeId: ID!, userId: ID!): User + revokeBadge(badgeId: ID!, userId: ID!): User +} diff --git a/backend/src/schema/types/type/Category.gql b/backend/src/graphql/types/type/Category.gql similarity index 100% rename from backend/src/schema/types/type/Category.gql rename to backend/src/graphql/types/type/Category.gql diff --git a/backend/src/schema/types/type/Comment.gql b/backend/src/graphql/types/type/Comment.gql similarity index 100% rename from backend/src/schema/types/type/Comment.gql rename to backend/src/graphql/types/type/Comment.gql diff --git a/backend/src/schema/types/type/Donations.gql b/backend/src/graphql/types/type/Donations.gql similarity index 100% rename from backend/src/schema/types/type/Donations.gql rename to backend/src/graphql/types/type/Donations.gql diff --git a/backend/src/schema/types/type/EMOTED.gql b/backend/src/graphql/types/type/EMOTED.gql similarity index 100% rename from backend/src/schema/types/type/EMOTED.gql rename to backend/src/graphql/types/type/EMOTED.gql diff --git a/backend/src/schema/types/type/EmailAddress.gql b/backend/src/graphql/types/type/EmailAddress.gql similarity index 100% rename from backend/src/schema/types/type/EmailAddress.gql rename to backend/src/graphql/types/type/EmailAddress.gql diff --git a/backend/src/schema/types/embed.gql b/backend/src/graphql/types/type/Embed.gql similarity index 100% rename from backend/src/schema/types/embed.gql rename to backend/src/graphql/types/type/Embed.gql diff --git a/backend/src/schema/types/type/FILED.gql b/backend/src/graphql/types/type/FILED.gql similarity index 100% rename from backend/src/schema/types/type/FILED.gql rename to backend/src/graphql/types/type/FILED.gql diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/graphql/types/type/Group.gql similarity index 95% rename from backend/src/schema/types/type/Group.gql rename to backend/src/graphql/types/type/Group.gql index 0d399d287..9bcac5047 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/graphql/types/type/Group.gql @@ -42,9 +42,7 @@ type Group { posts: [Post] @relation(name: "IN", direction: "IN") - isMutedByMe: Boolean! - @cypher( - statement: "MATCH (this)<-[m:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(m) >= 1") + isMutedByMe: Boolean! @cypher(statement: "MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )") } diff --git a/backend/src/schema/types/type/Image.gql b/backend/src/graphql/types/type/Image.gql similarity index 100% rename from backend/src/schema/types/type/Image.gql rename to backend/src/graphql/types/type/Image.gql diff --git a/backend/src/schema/types/type/InviteCode.gql b/backend/src/graphql/types/type/InviteCode.gql similarity index 100% rename from backend/src/schema/types/type/InviteCode.gql rename to backend/src/graphql/types/type/InviteCode.gql diff --git a/backend/src/schema/types/type/Location.gql b/backend/src/graphql/types/type/Location.gql similarity index 100% rename from backend/src/schema/types/type/Location.gql rename to backend/src/graphql/types/type/Location.gql diff --git a/backend/src/schema/types/type/MEMBER_OF.gql b/backend/src/graphql/types/type/MEMBER_OF.gql similarity index 100% rename from backend/src/schema/types/type/MEMBER_OF.gql rename to backend/src/graphql/types/type/MEMBER_OF.gql diff --git a/backend/src/schema/types/type/Message.gql b/backend/src/graphql/types/type/Message.gql similarity index 100% rename from backend/src/schema/types/type/Message.gql rename to backend/src/graphql/types/type/Message.gql diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/graphql/types/type/NOTIFIED.gql similarity index 100% rename from backend/src/schema/types/type/NOTIFIED.gql rename to backend/src/graphql/types/type/NOTIFIED.gql diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/graphql/types/type/Post.gql similarity index 100% rename from backend/src/schema/types/type/Post.gql rename to backend/src/graphql/types/type/Post.gql diff --git a/backend/src/schema/types/type/REVIEWED.gql b/backend/src/graphql/types/type/REVIEWED.gql similarity index 100% rename from backend/src/schema/types/type/REVIEWED.gql rename to backend/src/graphql/types/type/REVIEWED.gql diff --git a/backend/src/schema/types/type/Report.gql b/backend/src/graphql/types/type/Report.gql similarity index 100% rename from backend/src/schema/types/type/Report.gql rename to backend/src/graphql/types/type/Report.gql diff --git a/backend/src/schema/types/schema.gql b/backend/src/graphql/types/type/Reward.gql.unused similarity index 68% rename from backend/src/schema/types/schema.gql rename to backend/src/graphql/types/type/Reward.gql.unused index 9e2d00bca..6691b34f1 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/graphql/types/type/Reward.gql.unused @@ -1,16 +1,7 @@ -enum ShoutTypeEnum { - Post -} - type Reward { id: ID! user: User @relation(name: "REWARDED", direction: "IN") rewarderId: ID createdAt: String badge: Badge @relation(name: "REWARDED", direction: "OUT") -} - -type SharedInboxEndpoint { - id: ID! - uri: String } \ No newline at end of file diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/graphql/types/type/Room.gql similarity index 100% rename from backend/src/schema/types/type/Room.gql rename to backend/src/graphql/types/type/Room.gql diff --git a/backend/src/schema/types/type/Search.gql b/backend/src/graphql/types/type/Search.gql similarity index 100% rename from backend/src/schema/types/type/Search.gql rename to backend/src/graphql/types/type/Search.gql diff --git a/backend/src/graphql/types/type/SharedInboxEndpoint.gql.old b/backend/src/graphql/types/type/SharedInboxEndpoint.gql.old new file mode 100644 index 000000000..b078af63b --- /dev/null +++ b/backend/src/graphql/types/type/SharedInboxEndpoint.gql.old @@ -0,0 +1,4 @@ +type SharedInboxEndpoint { + id: ID! + uri: String +} \ No newline at end of file diff --git a/backend/src/schema/types/type/SocialMedia.gql b/backend/src/graphql/types/type/SocialMedia.gql similarity index 100% rename from backend/src/schema/types/type/SocialMedia.gql rename to backend/src/graphql/types/type/SocialMedia.gql diff --git a/backend/src/schema/types/type/Statistics.gql b/backend/src/graphql/types/type/Statistics.gql similarity index 100% rename from backend/src/schema/types/type/Statistics.gql rename to backend/src/graphql/types/type/Statistics.gql diff --git a/backend/src/schema/types/type/Tag.gql b/backend/src/graphql/types/type/Tag.gql similarity index 100% rename from backend/src/schema/types/type/Tag.gql rename to backend/src/graphql/types/type/Tag.gql diff --git a/backend/src/schema/types/type/User.gql b/backend/src/graphql/types/type/User.gql similarity index 93% rename from backend/src/schema/types/type/User.gql rename to backend/src/graphql/types/type/User.gql index cac622316..81dd9cf5b 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/graphql/types/type/User.gql @@ -125,8 +125,12 @@ type User { categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT") - badges: [Badge]! @relation(name: "REWARDED", direction: "IN") - badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") + badgeVerification: Badge! @neo4j_ignore + badgeTrophies: [Badge]! @relation(name: "REWARDED", direction: "IN") + badgeTrophiesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") + badgeTrophiesSelected: [Badge!]! @neo4j_ignore + badgeTrophiesUnused: [Badge]! @neo4j_ignore + badgeTrophiesUnusedCount: Int! @neo4j_ignore emotions: [EMOTED] @@ -247,4 +251,7 @@ type Mutation { # Get a JWT Token for the given Email and password login(email: String!, password: String!): String! + + setTrophyBadgeSelected(slot: Int!, badgeId: ID): User + resetTrophyBadgesSelected: User } diff --git a/backend/src/schema/types/type/UserData.gql b/backend/src/graphql/types/type/UserData.gql similarity index 100% rename from backend/src/schema/types/type/UserData.gql rename to backend/src/graphql/types/type/UserData.gql diff --git a/backend/src/helpers/asyncForEach.ts b/backend/src/helpers/asyncForEach.ts index 00b0f85a3..354f2cd07 100644 --- a/backend/src/helpers/asyncForEach.ts +++ b/backend/src/helpers/asyncForEach.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable promise/prefer-await-to-callbacks */ /* eslint-disable security/detect-object-injection */ /** diff --git a/backend/src/helpers/encryptPassword.ts b/backend/src/helpers/encryptPassword.ts index 657dee98a..1d12556ea 100644 --- a/backend/src/helpers/encryptPassword.ts +++ b/backend/src/helpers/encryptPassword.ts @@ -1,6 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ import { hashSync } from 'bcryptjs' export default function (args) { + // eslint-disable-next-line n/no-sync args.encryptedPassword = hashSync(args.password, 10) delete args.password return args diff --git a/backend/src/helpers/jest.ts b/backend/src/helpers/jest.ts index f1a0deb15..5594eb348 100644 --- a/backend/src/helpers/jest.ts +++ b/backend/src/helpers/jest.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable promise/avoid-new */ // sometime we have to wait to check a db state by having a look into the db in a certain moment // or we wait a bit to check if we missed to set an await somewhere diff --git a/backend/src/helpers/walkRecursive.ts b/backend/src/helpers/walkRecursive.ts index 4937f61bb..5874ca3af 100644 --- a/backend/src/helpers/walkRecursive.ts +++ b/backend/src/helpers/walkRecursive.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable promise/prefer-await-to-callbacks */ /* eslint-disable security/detect-object-injection */ /** diff --git a/backend/src/index.ts b/backend/src/index.ts index 35c215803..3da4ec7ae 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ import CONFIG from './config' import createServer from './server' diff --git a/backend/src/jwt/decode.spec.ts b/backend/src/jwt/decode.spec.ts index 29783bc6b..0cd52a5d5 100644 --- a/backend/src/jwt/decode.spec.ts +++ b/backend/src/jwt/decode.spec.ts @@ -1,5 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import Factory, { cleanDatabase } from '@db/factories' import { getDriver, getNeode } from '@db/neo4j' +import User from '@models/User' import decode from './decode' import encode from './encode' @@ -13,7 +17,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 @@ -83,26 +87,28 @@ describe('decode', () => { }) it('sets `lastActiveAt`', async () => { - let user = await neode.first('User', { id: 'u3' }) + let user = await neode.first('User', { id: 'u3' }, undefined) await expect(user.toJson()).resolves.not.toHaveProperty('lastActiveAt') await decode(driver, validAuthorizationHeader) - user = await neode.first('User', { id: 'u3' }) + user = await neode.first('User', { id: 'u3' }, undefined) await expect(user.toJson()).resolves.toMatchObject({ lastActiveAt: expect.any(String), }) }) it('updates `lastActiveAt` for every authenticated request', async () => { - let user = await neode.first('User', { id: 'u3' }) + let user = await neode.first('User', { id: 'u3' }, undefined) await user.update({ - updatedAt: new Date().toISOString(), - lastActiveAt: '2019-10-03T23:33:08.598Z', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updatedAt: new Date().toISOString() as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + lastActiveAt: '2019-10-03T23:33:08.598Z' as any, }) await expect(user.toJson()).resolves.toMatchObject({ lastActiveAt: '2019-10-03T23:33:08.598Z', }) await decode(driver, validAuthorizationHeader) - user = await neode.first('User', { id: 'u3' }) + user = await neode.first('User', { id: 'u3' }, undefined) await expect(user.toJson()).resolves.toMatchObject({ // should be a different time by now ;) lastActiveAt: expect.not.stringContaining('2019-10-03T23:33'), diff --git a/backend/src/jwt/decode.ts b/backend/src/jwt/decode.ts index b4424e660..0a433d38f 100644 --- a/backend/src/jwt/decode.ts +++ b/backend/src/jwt/decode.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import jwt from 'jsonwebtoken' import CONFIG from '@config/index' diff --git a/backend/src/jwt/encode.spec.ts b/backend/src/jwt/encode.spec.ts index 55c74bf8d..8121118f3 100644 --- a/backend/src/jwt/encode.spec.ts +++ b/backend/src/jwt/encode.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import jwt from 'jsonwebtoken' import CONFIG from '@config/index' diff --git a/backend/src/jwt/encode.ts b/backend/src/jwt/encode.ts index 110111faf..742bf438b 100644 --- a/backend/src/jwt/encode.ts +++ b/backend/src/jwt/encode.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import jwt from 'jsonwebtoken' import CONFIG from '@config/index' diff --git a/backend/src/middleware/branding/brandingMiddlewares.ts b/backend/src/middleware/branding/brandingMiddlewares.ts new file mode 100644 index 000000000..8d47043e8 --- /dev/null +++ b/backend/src/middleware/branding/brandingMiddlewares.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line import/no-cycle +import { MiddlewareOrder } from '@middleware/index' + +export default (): MiddlewareOrder[] => { + return [] +} diff --git a/backend/src/middleware/chatMiddleware.ts b/backend/src/middleware/chatMiddleware.ts index 8ae252e13..17d01fd95 100644 --- a/backend/src/middleware/chatMiddleware.ts +++ b/backend/src/middleware/chatMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { isArray } from 'lodash' const setRoomProps = (room) => { diff --git a/backend/src/middleware/excerptMiddleware.ts b/backend/src/middleware/excerptMiddleware.ts index f903dd01c..7a865be90 100644 --- a/backend/src/middleware/excerptMiddleware.ts +++ b/backend/src/middleware/excerptMiddleware.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import trunc from 'trunc-html' import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '@constants/groups' diff --git a/backend/src/middleware/hashtags/extractHashtags.spec.ts b/backend/src/middleware/hashtags/extractHashtags.spec.ts index 739c7de54..134ede761 100644 --- a/backend/src/middleware/hashtags/extractHashtags.spec.ts +++ b/backend/src/middleware/hashtags/extractHashtags.spec.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import extractHashtags from './extractHashtags' describe('extractHashtags', () => { diff --git a/backend/src/middleware/hashtags/extractHashtags.ts b/backend/src/middleware/hashtags/extractHashtags.ts index fc7a93d17..26b224a9a 100644 --- a/backend/src/middleware/hashtags/extractHashtags.ts +++ b/backend/src/middleware/hashtags/extractHashtags.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { load } from 'cheerio' // eslint-disable-next-line import/extensions import { exec, build } from 'xregexp/xregexp-all.js' @@ -19,6 +24,7 @@ export default function (content?) { return $(el).attr('data-hashtag-id') }) .get() + // eslint-disable-next-line @typescript-eslint/no-explicit-any const hashtags: any = [] ids.forEach((id) => { const match = exec(id, regX) diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts index 2bb617a3d..bc3b96594 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -51,22 +55,15 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { - hashtagingUser = await neode.create( - 'User', - { - id: 'you', - name: 'Al Capone', - slug: 'al-capone', - }, - { - password: '1234', - email: 'test@example.org', - }, - ) + hashtagingUser = await neode.create('User', { + id: 'you', + name: 'Al Capone', + slug: 'al-capone', + }) await neode.create('Category', { id: 'cat9', name: 'Democracy & Politics', diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.ts b/backend/src/middleware/hashtags/hashtagsMiddleware.ts index 76939d59d..2f53ee1a5 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.ts +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import extractHashtags from './extractHashtags' const updateHashtagsOfPost = async (postId, hashtags, context) => { diff --git a/backend/src/middleware/helpers/cleanHtml.ts b/backend/src/middleware/helpers/cleanHtml.ts index e72746fcf..d429f8f9d 100644 --- a/backend/src/middleware/helpers/cleanHtml.ts +++ b/backend/src/middleware/helpers/cleanHtml.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable security/detect-unsafe-regex */ import linkifyHtml from 'linkify-html' import sanitizeHtml from 'sanitize-html' diff --git a/backend/src/middleware/helpers/email/sendMail.ts b/backend/src/middleware/helpers/email/sendMail.ts index 46d808742..a7d223f1c 100644 --- a/backend/src/middleware/helpers/email/sendMail.ts +++ b/backend/src/middleware/helpers/email/sendMail.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import nodemailer from 'nodemailer' import { htmlToText } from 'nodemailer-html-to-text' @@ -28,6 +34,7 @@ const transporter = nodemailer.createTransport({ }, }) +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-function let sendMailCallback: any = async () => {} if (!hasEmailConfig) { if (!CONFIG.TEST) { @@ -51,6 +58,7 @@ if (!hasEmailConfig) { cleanHtml(templateArgs.html, 'dummyKey', { allowedTags: ['a'], allowedAttributes: { a: ['href'] }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any).replace(/&/g, '&'), ) } diff --git a/backend/src/middleware/helpers/email/templateBuilder.spec.ts b/backend/src/middleware/helpers/email/templateBuilder.spec.ts index 9dbfca91f..85608b55a 100644 --- a/backend/src/middleware/helpers/email/templateBuilder.spec.ts +++ b/backend/src/middleware/helpers/email/templateBuilder.spec.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ import CONFIG from '@config/index' import logosWebapp from '@config/logos' diff --git a/backend/src/middleware/helpers/email/templateBuilder.ts b/backend/src/middleware/helpers/email/templateBuilder.ts index bd44716fe..ffceb49f6 100644 --- a/backend/src/middleware/helpers/email/templateBuilder.ts +++ b/backend/src/middleware/helpers/email/templateBuilder.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable import/no-namespace */ import mustache from 'mustache' diff --git a/backend/src/middleware/helpers/email/templates/de/index.ts b/backend/src/middleware/helpers/email/templates/de/index.ts index 6f0803bc7..4aa323b9f 100644 --- a/backend/src/middleware/helpers/email/templates/de/index.ts +++ b/backend/src/middleware/helpers/email/templates/de/index.ts @@ -1,7 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable security/detect-non-literal-fs-filename */ import fs from 'node:fs' import path from 'node:path' +// eslint-disable-next-line n/no-sync const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8') export const notification = readFile('./notification.html') diff --git a/backend/src/middleware/helpers/email/templates/en/index.ts b/backend/src/middleware/helpers/email/templates/en/index.ts index 6f0803bc7..4aa323b9f 100644 --- a/backend/src/middleware/helpers/email/templates/en/index.ts +++ b/backend/src/middleware/helpers/email/templates/en/index.ts @@ -1,7 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable security/detect-non-literal-fs-filename */ import fs from 'node:fs' import path from 'node:path' +// eslint-disable-next-line n/no-sync const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8') export const notification = readFile('./notification.html') diff --git a/backend/src/middleware/helpers/email/templates/index.ts b/backend/src/middleware/helpers/email/templates/index.ts index 79de6b8ae..9a64192ce 100644 --- a/backend/src/middleware/helpers/email/templates/index.ts +++ b/backend/src/middleware/helpers/email/templates/index.ts @@ -1,7 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable security/detect-non-literal-fs-filename */ import fs from 'node:fs' import path from 'node:path' +// eslint-disable-next-line n/no-sync const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8') export const signup = readFile('./signup.html') diff --git a/backend/src/middleware/helpers/isUserOnline.spec.ts b/backend/src/middleware/helpers/isUserOnline.spec.ts index 62ed17f79..de4c840e3 100644 --- a/backend/src/middleware/helpers/isUserOnline.spec.ts +++ b/backend/src/middleware/helpers/isUserOnline.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { isUserOnline } from './isUserOnline' let user diff --git a/backend/src/middleware/helpers/isUserOnline.ts b/backend/src/middleware/helpers/isUserOnline.ts index 23ddeb0dc..b2d0b601f 100644 --- a/backend/src/middleware/helpers/isUserOnline.ts +++ b/backend/src/middleware/helpers/isUserOnline.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export const isUserOnline = (user) => { // Is Recipient considered online const lastActive = new Date(user.lastActiveAt).getTime() diff --git a/backend/src/middleware/includedFieldsMiddleware.ts b/backend/src/middleware/includedFieldsMiddleware.ts index fd95029b0..4b0ab1b1e 100644 --- a/backend/src/middleware/includedFieldsMiddleware.ts +++ b/backend/src/middleware/includedFieldsMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import cloneDeep from 'lodash/cloneDeep' const _includeFieldsRecursively = (selectionSet, includedFields) => { diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 225e02209..37fd33ef9 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -1,8 +1,14 @@ -/* eslint-disable security/detect-object-injection */ -import { applyMiddleware } from 'graphql-middleware' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { applyMiddleware, IMiddleware } from 'graphql-middleware' import CONFIG from '@config/index' +// eslint-disable-next-line import/no-cycle +import brandingMiddlewares from './branding/brandingMiddlewares' import chatMiddleware from './chatMiddleware' import excerpt from './excerptMiddleware' import hashtags from './hashtags/hashtagsMiddleware' @@ -20,56 +26,44 @@ import userInteractions from './userInteractions' import validation from './validation/validationMiddleware' import xss from './xssMiddleware' -export default (schema) => { - const middlewares = { - sentry, - permissions, - xss, - validation, - sluggify, - excerpt, - login, - notifications, - hashtags, - softDelete, - includedFields, - orderBy, - languages, - userInteractions, - chatMiddleware, - } - - let order = [ - 'sentry', - 'permissions', - 'xss', - // 'activityPub', disabled temporarily - 'validation', - 'userInteractions', - 'sluggify', - 'languages', - 'excerpt', - 'login', - 'notifications', - 'hashtags', - 'softDelete', - 'includedFields', - 'orderBy', - 'chatMiddleware', - ] - - // add permisions middleware at the first position (unless we're seeding) - if (CONFIG.DISABLED_MIDDLEWARES) { - const disabledMiddlewares = CONFIG.DISABLED_MIDDLEWARES.split(',') - order = order.filter((key) => { - if (disabledMiddlewares.includes(key)) { - /* eslint-disable-next-line no-console */ - console.log(`Warning: Disabled "${disabledMiddlewares}" middleware.`) - } - return !disabledMiddlewares.includes(key) - }) - } - - const appliedMiddlewares = order.map((key) => middlewares[key]) - return applyMiddleware(schema, ...appliedMiddlewares) +export interface MiddlewareOrder { + order: number + name: string + middleware: IMiddleware +} + +const ocelotMiddlewares: MiddlewareOrder[] = [ + { order: -200, name: 'sentry', middleware: sentry }, + { order: -190, name: 'permissions', middleware: permissions }, + { order: -180, name: 'xss', middleware: xss }, + { order: -170, name: 'validation', middleware: validation }, + { order: -160, name: 'userInteractions', middleware: userInteractions }, + { order: -150, name: 'sluggify', middleware: sluggify }, + { order: -140, name: 'languages', middleware: languages }, + { order: -130, name: 'excerpt', middleware: excerpt }, + { order: -120, name: 'login', middleware: login }, + { order: -110, name: 'notifications', middleware: notifications }, + { order: -100, name: 'hashtags', middleware: hashtags }, + { order: -90, name: 'softDelete', middleware: softDelete }, + { order: -80, name: 'includedFields', middleware: includedFields }, + { order: -70, name: 'orderBy', middleware: orderBy }, + { order: -60, name: 'chatMiddleware', middleware: chatMiddleware }, +] + +export default (schema) => { + const middlewares = ocelotMiddlewares + .concat(brandingMiddlewares()) + .sort((a, b) => a.order - b.order) + + const filteredMiddlewares = middlewares.filter( + (middleware) => !CONFIG.DISABLED_MIDDLEWARES.includes(middleware.name), + ) + + // Warn if we filtered + if (middlewares.length < filteredMiddlewares.length) { + // eslint-disable-next-line no-console + console.log(`Warning: Disabled "${CONFIG.DISABLED_MIDDLEWARES.join(', ')}" middleware.`) + } + + return applyMiddleware(schema, ...filteredMiddlewares.map((middleware) => middleware.middleware)) } diff --git a/backend/src/middleware/languages/languages.spec.ts b/backend/src/middleware/languages/languages.spec.ts index ca77acac8..50e3a028f 100644 --- a/backend/src/middleware/languages/languages.spec.ts +++ b/backend/src/middleware/languages/languages.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -29,7 +32,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) const createPostMutation = gql` diff --git a/backend/src/middleware/languages/languages.ts b/backend/src/middleware/languages/languages.ts index 6149b90d5..fb8c51a1f 100644 --- a/backend/src/middleware/languages/languages.ts +++ b/backend/src/middleware/languages/languages.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import LanguageDetect from 'languagedetect' import { removeHtmlTags } from '@middleware/helpers/cleanHtml' diff --git a/backend/src/middleware/login/loginMiddleware.ts b/backend/src/middleware/login/loginMiddleware.ts index 04d189b4b..b67e5f60a 100644 --- a/backend/src/middleware/login/loginMiddleware.ts +++ b/backend/src/middleware/login/loginMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { sendMail } from '@middleware/helpers/email/sendMail' import { signupTemplate, diff --git a/backend/src/middleware/notifications/mentions/extractMentionedUsers.ts b/backend/src/middleware/notifications/mentions/extractMentionedUsers.ts index b7dc0fed1..ccb7dd1be 100644 --- a/backend/src/middleware/notifications/mentions/extractMentionedUsers.ts +++ b/backend/src/middleware/notifications/mentions/extractMentionedUsers.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ import { load } from 'cheerio' export default (content?) => { diff --git a/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts new file mode 100644 index 000000000..79d95e43e --- /dev/null +++ b/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts @@ -0,0 +1,741 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import CONFIG from '@src/config' +import createServer from '@src/server' + +CONFIG.CATEGORIES_ACTIVE = false + +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), +})) + +let server, query, mutate, authenticatedUser, emaillessMember + +let postAuthor, groupMember + +const driver = getDriver() +const neode = getNeode() + +const mentionString = ` + @group-member + @email-less-member` + +const createPostMutation = gql` + mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { + CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) { + id + title + content + } + } +` + +const createCommentMutation = gql` + mutation ($id: ID, $postId: ID!, $content: String!) { + CreateComment(id: $id, postId: $postId, content: $content) { + id + content + } + } +` + +const notificationQuery = gql` + query ($read: Boolean) { + notifications(read: $read, orderBy: updatedAt_desc) { + read + reason + createdAt + relatedUser { + id + } + from { + __typename + ... on Post { + id + content + } + ... on Comment { + id + content + } + ... on Group { + id + } + } + } + } +` + +const followUserMutation = gql` + mutation ($id: ID!) { + followUser(id: $id) { + id + } + } +` + +const markAllAsRead = async () => + mutate({ + mutation: gql` + mutation { + markAllAsRead { + id + } + } + `, + }) + +beforeAll(async () => { + await cleanDatabase() + + const createServerResult = createServer({ + context: () => { + return { + user: authenticatedUser, + neode, + driver, + cypherParams: { + currentUserId: authenticatedUser ? authenticatedUser.id : null, + }, + } + }, + }) + server = createServerResult.server + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +afterAll(async () => { + await cleanDatabase() + await driver.close() +}) + +describe('emails sent for notifications', () => { + beforeEach(async () => { + postAuthor = await Factory.build( + 'user', + { + id: 'post-author', + name: 'Post Author', + slug: 'post-author', + }, + { + email: 'test@example.org', + password: '1234', + }, + ) + groupMember = await Factory.build( + 'user', + { + id: 'group-member', + name: 'Group Member', + slug: 'group-member', + }, + { + email: 'group.member@example.org', + password: '1234', + }, + ) + emaillessMember = await neode.create('User', { + id: 'email-less-member', + name: 'Email-less Member', + slug: 'email-less-member', + }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'public-group', + name: 'A public group', + description: 'A public group to test the notifications of mentions', + groupType: 'public', + actionRadius: 'national', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) + authenticatedUser = await emaillessMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) + }) + + afterEach(async () => { + await cleanDatabase() + }) + + describe('handleContentDataOfPost', () => { + describe('post-author posts into group and mentions following group-member', () => { + describe('all email notification settings are true', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + }) + + it('sends only one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + + it('sends 3 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'followed_user_posted', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('email notification for mention in post is false', () => { + beforeEach(async () => { + jest.clearAllMocks() + await groupMember.update({ emailNotificationsMention: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + }) + + it('sends only one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + + it('sends 3 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'followed_user_posted', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('email notification for mention in post and followed users is false', () => { + beforeEach(async () => { + jest.clearAllMocks() + await groupMember.update({ emailNotificationsMention: false }) + await groupMember.update({ emailNotificationsFollowingUsers: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + }) + + it('sends only one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + + it('sends 3 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'followed_user_posted', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('all relevant email notifications are false', () => { + beforeEach(async () => { + jest.clearAllMocks() + await groupMember.update({ emailNotificationsMention: false }) + await groupMember.update({ emailNotificationsFollowingUsers: false }) + await groupMember.update({ emailNotificationsPostInGroup: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + }) + + it('sends NO email', () => { + expect(sendMailMock).not.toHaveBeenCalled() + }) + + it('sends 3 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'followed_user_posted', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) + }) + + describe('handleContentDataOfComment', () => { + describe('user comments post and author responds with in a comment and mentions the user', () => { + describe('all email notification settings are true', () => { + beforeEach(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment', + content: `Hello, my beloved author.`, + postId: 'post', + }, + }) + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment-2', + content: `Hello, ${mentionString}, my trusty followers.`, + postId: 'post', + }, + }) + }) + + it('sends only one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + + it('sends 2 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'commented_on_post', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('email notification commented on post is false', () => { + beforeEach(async () => { + await groupMember.update({ emailNotificationsCommentOnObservedPost: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment', + content: `Hello, my beloved author.`, + postId: 'post', + }, + }) + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment-2', + content: `Hello, ${mentionString}, my trusty followers.`, + postId: 'post', + }, + }) + }) + + it('sends only one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + + it('sends 2 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'commented_on_post', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('all relevant email notifications are false', () => { + beforeEach(async () => { + await groupMember.update({ emailNotificationsCommentOnObservedPost: false }) + await groupMember.update({ emailNotificationsMention: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment', + content: `Hello, my beloved author.`, + postId: 'post', + }, + }) + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment-2', + content: `Hello, ${mentionString}, my trusty followers.`, + postId: 'post', + }, + }) + }) + + it('sends NO email', () => { + expect(sendMailMock).not.toHaveBeenCalled() + }) + + it('sends 2 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'commented_on_post', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/followed-users.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts similarity index 79% rename from backend/src/middleware/notifications/followed-users.spec.ts rename to backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts index 4d4b0e872..21d4a14a0 100644 --- a/backend/src/middleware/notifications/followed-users.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts @@ -1,22 +1,27 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { createGroupMutation } from '@graphql/groups' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' import CONFIG from '@src/config' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false -const sendMailMock = jest.fn() -jest.mock('../helpers/email/sendMail', () => ({ - sendMail: () => sendMailMock(), +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), })) let server, query, mutate, authenticatedUser -let postAuthor, firstFollower, secondFollower +let postAuthor, firstFollower, secondFollower, thirdFollower, emaillessFollower const driver = getDriver() const neode = getNeode() @@ -89,7 +94,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('following users notifications', () => { @@ -102,7 +107,7 @@ describe('following users notifications', () => { slug: 'post-author', }, { - email: 'test@example.org', + email: 'post-author@example.org', password: '1234', }, ) @@ -114,7 +119,7 @@ describe('following users notifications', () => { slug: 'first-follower', }, { - email: 'test2@example.org', + email: 'first-follower@example.org', password: '1234', }, ) @@ -126,10 +131,27 @@ describe('following users notifications', () => { slug: 'second-follower', }, { - email: 'test3@example.org', + email: 'second-follower@example.org', password: '1234', }, ) + thirdFollower = await Factory.build( + 'user', + { + id: 'third-follower', + name: 'Third Follower', + slug: 'third-follower', + }, + { + email: 'third-follower@example.org', + password: '1234', + }, + ) + emaillessFollower = await neode.create('User', { + id: 'email-less-follower', + name: 'Email-less Follower', + slug: 'email-less-follower', + }) await secondFollower.update({ emailNotificationsFollowingUsers: false }) authenticatedUser = await firstFollower.toJson() await mutate({ @@ -141,6 +163,16 @@ describe('following users notifications', () => { mutation: followUserMutation, variables: { id: 'post-author' }, }) + authenticatedUser = await thirdFollower.toJson() + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) + authenticatedUser = await emaillessFollower.toJson() + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) jest.clearAllMocks() }) @@ -216,8 +248,43 @@ describe('following users notifications', () => { }) }) - it('sends only one email, as second follower has emails disabled', () => { - expect(sendMailMock).toHaveBeenCalledTimes(1) + it('sends notification to the email-less follower', async () => { + authenticatedUser = await emaillessFollower.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'post', + }, + read: false, + reason: 'followed_user_posted', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends only two emails, as second follower has emails disabled and email-less follower has no email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(2) + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + html: expect.stringContaining('Hello First Follower'), + to: 'first-follower@example.org', + }), + ) + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + html: expect.stringContaining('Hello Third Follower'), + to: 'third-follower@example.org', + }), + ) }) }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts new file mode 100644 index 000000000..96c7e9d18 --- /dev/null +++ b/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts @@ -0,0 +1,864 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import CONFIG from '@src/config' +import createServer from '@src/server' + +CONFIG.CATEGORIES_ACTIVE = false + +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), +})) + +let server, query, mutate, authenticatedUser + +let postAuthor, groupMember, pendingMember, noMember, emaillessMember + +const driver = getDriver() +const neode = getNeode() + +const mentionString = ` + @no-member + @pending-member + @group-member. + @email-less-member. +` + +const createPostMutation = gql` + mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { + CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) { + id + title + content + } + } +` + +const createCommentMutation = gql` + mutation ($id: ID, $postId: ID!, $commentContent: String!) { + CreateComment(id: $id, postId: $postId, content: $commentContent) { + id + content + } + } +` + +const notificationQuery = gql` + query ($read: Boolean) { + notifications(read: $read, orderBy: updatedAt_desc) { + read + reason + createdAt + relatedUser { + id + } + from { + __typename + ... on Post { + id + content + } + ... on Comment { + id + content + } + ... on Group { + id + } + } + } + } +` + +const markAllAsRead = async () => + mutate({ + mutation: gql` + mutation { + markAllAsRead { + id + } + } + `, + }) + +beforeAll(async () => { + await cleanDatabase() + + const createServerResult = createServer({ + context: () => { + return { + user: authenticatedUser, + neode, + driver, + cypherParams: { + currentUserId: authenticatedUser ? authenticatedUser.id : null, + }, + } + }, + }) + server = createServerResult.server + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +afterAll(async () => { + await cleanDatabase() + await driver.close() +}) + +describe('mentions in groups', () => { + beforeEach(async () => { + postAuthor = await Factory.build( + 'user', + { + id: 'post-author', + name: 'Post Author', + slug: 'post-author', + }, + { + email: 'post.author@example.org', + password: '1234', + }, + ) + groupMember = await Factory.build( + 'user', + { + id: 'group-member', + name: 'Group Member', + slug: 'group-member', + }, + { + email: 'group.member@example.org', + password: '1234', + }, + ) + pendingMember = await Factory.build( + 'user', + { + id: 'pending-member', + name: 'Pending Member', + slug: 'pending-member', + }, + { + email: 'pending.member@example.org', + password: '1234', + }, + ) + noMember = await Factory.build( + 'user', + { + id: 'no-member', + name: 'No Member', + slug: 'no-member', + }, + { + email: 'no.member@example.org', + password: '1234', + }, + ) + emaillessMember = await neode.create('User', { + id: 'email-less-member', + name: 'Email-less Member', + slug: 'email-less-member', + }) + + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'public-group', + name: 'A public group', + description: 'A public group to test the notifications of mentions', + groupType: 'public', + actionRadius: 'national', + }, + }) + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'closed-group', + name: 'A closed group', + description: 'A closed group to test the notifications of mentions', + groupType: 'closed', + actionRadius: 'national', + }, + }) + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'hidden-group', + name: 'A hidden group', + description: 'A hidden group to test the notifications of mentions', + groupType: 'hidden', + actionRadius: 'national', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'group-member', + }, + }) + authenticatedUser = await pendingMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'pending-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: 'pending-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'pending-member', + }, + }) + authenticatedUser = await emaillessMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'group-member', + }, + }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'closed-group', + userId: 'group-member', + roleInGroup: 'usual', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'hidden-group', + userId: 'group-member', + roleInGroup: 'usual', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'closed-group', + userId: 'email-less-member', + roleInGroup: 'usual', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'hidden-group', + userId: 'email-less-member', + roleInGroup: 'usual', + }, + }) + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await emaillessMember.toJson() + await markAllAsRead() + }) + + afterEach(async () => { + await cleanDatabase() + }) + + describe('post in public group', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'public-post', + title: 'This is the post in the public group', + content: `Hey ${mentionString}! Please read this`, + groupId: 'public-group', + }, + }) + }) + + it('sends a notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'public-post', + }, + read: false, + reason: 'mentioned_in_post', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'public-post', + content: + 'Hey
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'public-post', + content: + 'Hey
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + + it('sends only 3 emails, one for each user with an email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(3) + }) + }) + + describe('post in closed group', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'closed-post', + title: 'This is the post in the closed group', + content: `Hey members ${mentionString}! Please read this`, + groupId: 'closed-group', + }, + }) + }) + + it('sends NO notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'closed-post', + content: + 'Hey members
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'closed-post', + content: + 'Hey members
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + + it('sends only 1 email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + }) + + describe('post in hidden group', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'hidden-post', + title: 'This is the post in the hidden group', + content: `Hey hiders ${mentionString}! Please read this`, + groupId: 'hidden-group', + }, + }) + }) + + it('sends NO notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'hidden-post', + content: + 'Hey hiders
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'hidden-post', + content: + 'Hey hiders
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + + it('sends only 1 email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + }) + + describe('comments on group posts', () => { + describe('public group', () => { + beforeEach(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'public-post', + title: 'This is the post in the public group', + content: `Some public content`, + groupId: 'public-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await postAuthor.toJson() + jest.clearAllMocks() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'public-comment', + postId: 'public-post', + commentContent: `Hey everyone ${mentionString}! Please read this`, + }, + }) + }) + + it('sends a notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + id: 'public-comment', + }, + read: false, + reason: 'mentioned_in_comment', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + id: 'public-comment', + }, + read: false, + reason: 'mentioned_in_comment', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends 2 emails', () => { + expect(sendMailMock).toHaveBeenCalledTimes(3) + }) + }) + + describe('closed group', () => { + beforeEach(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'closed-post', + title: 'This is the post in the closed group', + content: `Some closed content`, + groupId: 'closed-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await postAuthor.toJson() + jest.clearAllMocks() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'closed-comment', + postId: 'closed-post', + commentContent: `Hey members ${mentionString}! Please read this`, + }, + }) + }) + + it('sends NO notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + id: 'closed-comment', + }, + read: false, + reason: 'mentioned_in_comment', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends 1 email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + }) + + describe('hidden group', () => { + beforeEach(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'hidden-post', + title: 'This is the post in the hidden group', + content: `Some hidden content`, + groupId: 'hidden-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await postAuthor.toJson() + jest.clearAllMocks() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'hidden-comment', + postId: 'hidden-post', + commentContent: `Hey hiders ${mentionString}! Please read this`, + }, + }) + }) + + it('sends NO notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + id: 'hidden-comment', + }, + read: false, + reason: 'mentioned_in_comment', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends 1 email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/observing-posts.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts similarity index 79% rename from backend/src/middleware/notifications/observing-posts.spec.ts rename to backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts index e10d61d9f..a0864fe07 100644 --- a/backend/src/middleware/notifications/observing-posts.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts @@ -1,16 +1,25 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import CONFIG from '@config/index' -import { cleanDatabase } from '@db/factories' +import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), +})) + let server, query, mutate, authenticatedUser -let postAuthor, firstCommenter, secondCommenter +let postAuthor, firstCommenter, secondCommenter, emaillessObserver const driver = getDriver() const neode = getNeode() @@ -93,47 +102,52 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('notifications for users that observe a post', () => { beforeAll(async () => { - postAuthor = await neode.create( - 'User', + postAuthor = await Factory.build( + 'user', { id: 'post-author', name: 'Post Author', slug: 'post-author', }, { - email: 'test@example.org', + email: 'post-author@example.org', password: '1234', }, ) - firstCommenter = await neode.create( - 'User', + firstCommenter = await Factory.build( + 'user', { id: 'first-commenter', name: 'First Commenter', slug: 'first-commenter', }, { - email: 'test2@example.org', + email: 'first-commenter@example.org', password: '1234', }, ) - secondCommenter = await neode.create( - 'User', + secondCommenter = await Factory.build( + 'user', { id: 'second-commenter', name: 'Second Commenter', slug: 'second-commenter', }, { - email: 'test3@example.org', + email: 'second-commenter@example.org', password: '1234', }, ) + emaillessObserver = await neode.create('User', { + id: 'email-less-observer', + name: 'Email-less Observer', + slug: 'email-less-observer', + }) authenticatedUser = await postAuthor.toJson() await mutate({ mutation: createPostMutation, @@ -143,6 +157,14 @@ describe('notifications for users that observe a post', () => { content: 'This is the content of the post', }, }) + authenticatedUser = await emaillessObserver.toJson() + await mutate({ + mutation: toggleObservePostMutation, + variables: { + id: 'post', + value: true, + }, + }) }) describe('first comment on the post', () => { @@ -194,8 +216,18 @@ describe('notifications for users that observe a post', () => { }) }) + it('sends one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + to: 'post-author@example.org', + }), + ) + }) + describe('second comment on post', () => { beforeAll(async () => { + jest.clearAllMocks() authenticatedUser = await secondCommenter.toJson() await mutate({ mutation: createCommentMutation, @@ -273,10 +305,25 @@ describe('notifications for users that observe a post', () => { errors: undefined, }) }) + + it('sends two emails', () => { + expect(sendMailMock).toHaveBeenCalledTimes(2) + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + to: 'post-author@example.org', + }), + ) + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + to: 'first-commenter@example.org', + }), + ) + }) }) describe('first commenter unfollows the post and post author comments post', () => { beforeAll(async () => { + jest.clearAllMocks() authenticatedUser = await firstCommenter.toJson() await mutate({ mutation: toggleObservePostMutation, @@ -372,6 +419,15 @@ describe('notifications for users that observe a post', () => { errors: undefined, }) }) + + it('sends one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + to: 'second-commenter@example.org', + }), + ) + }) }) }) }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts new file mode 100644 index 000000000..3a47d376d --- /dev/null +++ b/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts @@ -0,0 +1,151 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import CONFIG from '@src/config' +import createServer from '@src/server' + +CONFIG.CATEGORIES_ACTIVE = false + +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), +})) + +let isUserOnlineMock = jest.fn().mockReturnValue(false) +jest.mock('../helpers/isUserOnline', () => ({ + isUserOnline: () => isUserOnlineMock(), +})) + +let server, mutate, authenticatedUser + +let postAuthor + +const driver = getDriver() +const neode = getNeode() + +const createPostMutation = gql` + mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { + CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) { + id + title + content + } + } +` + +beforeAll(async () => { + await cleanDatabase() + + const createServerResult = createServer({ + context: () => { + return { + user: authenticatedUser, + neode, + driver, + cypherParams: { + currentUserId: authenticatedUser ? authenticatedUser.id : null, + }, + } + }, + }) + server = createServerResult.server + const createTestClientResult = createTestClient(server) + mutate = createTestClientResult.mutate +}) + +afterAll(async () => { + await cleanDatabase() + await driver.close() +}) + +afterEach(async () => { + await cleanDatabase() +}) + +describe('online status and sending emails', () => { + beforeEach(async () => { + postAuthor = await Factory.build( + 'user', + { + id: 'post-author', + name: 'Post Author', + slug: 'post-author', + }, + { + email: 'test@example.org', + password: '1234', + }, + ) + await Factory.build( + 'user', + { + id: 'other-user', + name: 'Other User', + slug: 'other-user', + }, + { + email: 'test2@example.org', + password: '1234', + }, + ) + }) + + describe('user is online', () => { + beforeAll(() => { + isUserOnlineMock = jest.fn().mockReturnValue(true) + }) + + describe('mentioned in post', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-online-1', + title: 'This post mentions the other user', + content: + 'Hello @other-user, are you fine?', + }, + }) + }) + + it('sends NO email to the other user', () => { + expect(sendMailMock).not.toBeCalled() + }) + }) + }) + + describe('user is offline', () => { + beforeAll(() => { + isUserOnlineMock = jest.fn().mockReturnValue(false) + }) + + describe('mentioned in post', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-offline-1', + title: 'This post mentions the other user', + content: + 'Hello @other-user, are you fine?', + }, + }) + }) + + it('sends email to the other user', () => { + expect(sendMailMock).toBeCalledTimes(1) + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/posts-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts similarity index 70% rename from backend/src/middleware/notifications/posts-in-groups.spec.ts rename to backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts index a46de2830..25aef2e2b 100644 --- a/backend/src/middleware/notifications/posts-in-groups.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts @@ -1,26 +1,30 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { - createGroupMutation, - joinGroupMutation, - changeGroupMemberRoleMutation, -} from '@graphql/groups' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' import CONFIG from '@src/config' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false -const sendMailMock = jest.fn() -jest.mock('../helpers/email/sendMail', () => ({ - sendMail: () => sendMailMock(), +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), })) let server, query, mutate, authenticatedUser -let postAuthor, groupMember, pendingMember +let postAuthor, groupMember, pendingMember, emaillessMember const driver = getDriver() const neode = getNeode() @@ -114,11 +118,11 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('notify group members of new posts in group', () => { - beforeAll(async () => { + beforeEach(async () => { postAuthor = await Factory.build( 'user', { @@ -155,6 +159,12 @@ describe('notify group members of new posts in group', () => { password: '1234', }, ) + emaillessMember = await neode.create('User', { + id: 'email-less-member', + name: 'Email-less Member', + slug: 'email-less-member', + }) + authenticatedUser = await postAuthor.toJson() await mutate({ mutation: createGroupMutation(), @@ -182,6 +192,14 @@ describe('notify group members of new posts in group', () => { userId: 'pending-member', }, }) + authenticatedUser = await emaillessMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g-1', + userId: 'group-member', + }, + }) authenticatedUser = await postAuthor.toJson() await mutate({ mutation: changeGroupMemberRoleMutation(), @@ -191,10 +209,22 @@ describe('notify group members of new posts in group', () => { roleInGroup: 'usual', }, }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g-1', + userId: 'email-less-member', + roleInGroup: 'usual', + }, + }) + }) + + afterEach(async () => { + await cleanDatabase() }) describe('group owner posts in group', () => { - beforeAll(async () => { + beforeEach(async () => { jest.clearAllMocks() authenticatedUser = await groupMember.toJson() await markAllAsRead() @@ -275,29 +305,15 @@ describe('notify group members of new posts in group', () => { }) describe('group member mutes group', () => { - it('sets the muted status correctly', async () => { + beforeEach(async () => { authenticatedUser = await groupMember.toJson() - await expect( - mutate({ - mutation: muteGroupMutation, - variables: { - groupId: 'g-1', - }, - }), - ).resolves.toMatchObject({ - data: { - muteGroup: { - isMutedByMe: true, - }, + await mutate({ + mutation: muteGroupMutation, + variables: { + groupId: 'g-1', }, - errors: undefined, }) - }) - - it('sends NO notification when another post is posted', async () => { jest.clearAllMocks() - authenticatedUser = await groupMember.toJson() - await markAllAsRead() authenticatedUser = await postAuthor.toJson() await mutate({ mutation: createPostMutation, @@ -308,7 +324,9 @@ describe('notify group members of new posts in group', () => { groupId: 'g-1', }, }) - authenticatedUser = await groupMember.toJson() + }) + + it('sends NO notification when another post is posted', async () => { await expect( query({ query: notificationQuery, @@ -329,30 +347,18 @@ describe('notify group members of new posts in group', () => { }) describe('group member unmutes group again but disables email', () => { - beforeAll(async () => { + beforeEach(async () => { + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: unmuteGroupMutation, + variables: { + groupId: 'g-1', + }, + }) jest.clearAllMocks() await groupMember.update({ emailNotificationsPostInGroup: false }) }) - it('sets the muted status correctly', async () => { - authenticatedUser = await groupMember.toJson() - await expect( - mutate({ - mutation: unmuteGroupMutation, - variables: { - groupId: 'g-1', - }, - }), - ).resolves.toMatchObject({ - data: { - unmuteGroup: { - isMutedByMe: false, - }, - }, - errors: undefined, - }) - }) - it('sends notification when another post is posted', async () => { authenticatedUser = await groupMember.toJson() await markAllAsRead() @@ -396,5 +402,85 @@ describe('notify group members of new posts in group', () => { }) }) }) + + describe('group member blocks author', () => { + beforeEach(async () => { + await groupMember.relateTo(postAuthor, 'blocked') + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-1', + title: 'This is another post in the group', + content: 'This is the content of another post in the group', + groupId: 'g-1', + }, + }) + }) + + it('sends no notification to the user', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO email', () => { + expect(sendMailMock).not.toHaveBeenCalled() + }) + }) + + describe('group member mutes author', () => { + beforeEach(async () => { + await groupMember.relateTo(postAuthor, 'muted') + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-1', + title: 'This is another post in the group', + content: 'This is the content of another post in the group', + groupId: 'g-1', + }, + }) + }) + + it('sends no notification to the user', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO email', () => { + expect(sendMailMock).not.toHaveBeenCalled() + }) + }) }) }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts index c636a7c87..ab0a6a5b2 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts @@ -1,22 +1,26 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { - createGroupMutation, - joinGroupMutation, - leaveGroupMutation, - changeGroupMemberRoleMutation, - removeUserFromGroupMutation, -} from '@graphql/groups' -import { createMessageMutation } from '@graphql/messages' -import { createRoomMutation } from '@graphql/rooms' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createMessageMutation } from '@graphql/queries/createMessageMutation' +import { createRoomMutation } from '@graphql/queries/createRoomMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation' +import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation' import createServer, { pubsub } from '@src/server' -const sendMailMock = jest.fn() -jest.mock('../helpers/email/sendMail', () => ({ - sendMail: () => sendMailMock(), +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), })) const chatMessageTemplateMock = jest.fn() @@ -31,8 +35,10 @@ jest.mock('../helpers/isUserOnline', () => ({ isUserOnline: () => isUserOnlineMock(), })) +const pubsubSpy = jest.spyOn(pubsub, 'publish') + let server, query, mutate, notifiedUser, authenticatedUser -let publishSpy + const driver = getDriver() const neode = getNeode() const categoryIds = ['cat9'] @@ -65,7 +71,6 @@ const createCommentMutation = gql` beforeAll(async () => { await cleanDatabase() - publishSpy = jest.spyOn(pubsub, 'publish') const createServerResult = createServer({ context: () => { return { @@ -83,11 +88,10 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { - publishSpy.mockClear() notifiedUser = await Factory.build( 'user', { @@ -191,8 +195,8 @@ describe('notifications', () => { beforeEach(async () => { jest.clearAllMocks() commentContent = 'Commenters comment.' - commentAuthor = await neode.create( - 'User', + commentAuthor = await Factory.build( + 'user', { id: 'commentAuthor', name: 'Mrs Comment', @@ -294,6 +298,25 @@ describe('notifications', () => { ).resolves.toEqual(expected) }) }) + + describe('if I have muted the comment author', () => { + it('sends me no notification', async () => { + await notifiedUser.relateTo(commentAuthor, 'muted') + await createCommentOnPostAction() + const expected = expect.objectContaining({ + data: { notifications: [] }, + }) + + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) + }) }) describe('commenter is me', () => { @@ -322,8 +345,8 @@ describe('notifications', () => { beforeEach(async () => { jest.clearAllMocks() - postAuthor = await neode.create( - 'User', + postAuthor = await Factory.build( + 'user', { id: 'postAuthor', name: 'Mrs Post', @@ -417,7 +440,7 @@ describe('notifications', () => { it('publishes `NOTIFICATION_ADDED` to me', async () => { await createPostAction() - expect(publishSpy).toHaveBeenCalledWith( + expect(pubsubSpy).toHaveBeenCalledWith( 'NOTIFICATION_ADDED', expect.objectContaining({ notificationAdded: expect.objectContaining({ @@ -428,7 +451,7 @@ describe('notifications', () => { }), }), ) - expect(publishSpy).toHaveBeenCalledTimes(1) + expect(pubsubSpy).toHaveBeenCalledTimes(1) }) describe('updates the post and mentions me again', () => { @@ -578,7 +601,49 @@ describe('notifications', () => { it('does not publish `NOTIFICATION_ADDED`', async () => { await createPostAction() - expect(publishSpy).not.toHaveBeenCalled() + expect(pubsubSpy).not.toHaveBeenCalled() + }) + }) + + describe('but the author of the post muted me', () => { + beforeEach(async () => { + await postAuthor.relateTo(notifiedUser, 'muted') + }) + + it('sends me a notification', async () => { + await createPostAction() + const expected = expect.objectContaining({ + data: { + notifications: [ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + content: + 'Hey @al-capone how do you do?', + id: 'p47', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ], + }, + }) + + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) + + it('publishes `NOTIFICATION_ADDED`', async () => { + await createPostAction() + expect(pubsubSpy).toHaveBeenCalled() }) }) }) @@ -593,8 +658,8 @@ describe('notifications', () => { beforeEach(async () => { commentContent = 'One mention about me with @al-capone.' - commentAuthor = await neode.create( - 'User', + commentAuthor = await Factory.build( + 'user', { id: 'commentAuthor', name: 'Mrs Comment', @@ -608,15 +673,15 @@ describe('notifications', () => { }) it('sends only one notification with reason mentioned_in_comment', async () => { - postAuthor = await neode.create( - 'User', + postAuthor = await Factory.build( + 'user', { id: 'MrPostAuthor', name: 'Mr Author', slug: 'mr-author', }, { - email: 'post-author@example.org', + email: 'post-author2@example.org', password: '1234', }, ) @@ -691,8 +756,8 @@ describe('notifications', () => { await postAuthor.relateTo(notifiedUser, 'blocked') commentContent = 'One mention about me with @al-capone.' - commentAuthor = await neode.create( - 'User', + commentAuthor = await Factory.build( + 'user', { id: 'commentAuthor', name: 'Mrs Comment', @@ -722,7 +787,7 @@ describe('notifications', () => { it('does not publish `NOTIFICATION_ADDED` to authenticated user', async () => { await createCommentOnPostAction() - expect(publishSpy).toHaveBeenCalledWith( + expect(pubsubSpy).toHaveBeenCalledWith( 'NOTIFICATION_ADDED', expect.objectContaining({ notificationAdded: expect.objectContaining({ @@ -733,14 +798,80 @@ describe('notifications', () => { }), }), ) - expect(publishSpy).toHaveBeenCalledTimes(1) + expect(pubsubSpy).toHaveBeenCalledTimes(1) + }) + }) + + describe('but the author of the post muted me', () => { + beforeEach(async () => { + await postAuthor.relateTo(notifiedUser, 'muted') + commentContent = + 'One mention about me with @al-capone.' + commentAuthor = await Factory.build( + 'user', + { + id: 'commentAuthor', + name: 'Mrs Comment', + slug: 'mrs-comment', + }, + { + email: 'comment-author@example.org', + password: '1234', + }, + ) + }) + + it('sends me a notification', async () => { + await createCommentOnPostAction() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + content: + 'One mention about me with @al-capone.', + id: 'c47', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ], + }, + errors: undefined, + }) + }) + + it('publishes `NOTIFICATION_ADDED` to authenticated user and me', async () => { + await createCommentOnPostAction() + expect(pubsubSpy).toHaveBeenCalledWith( + 'NOTIFICATION_ADDED', + expect.objectContaining({ + notificationAdded: expect.objectContaining({ + reason: 'commented_on_post', + to: expect.objectContaining({ + id: 'postAuthor', // that's expected, it's not me but the post author + }), + }), + }), + ) + expect(pubsubSpy).toHaveBeenCalledTimes(2) }) }) }) }) }) - describe('chat email notifications', () => { + describe('chat notifications', () => { let chatSender let chatReceiver let roomId @@ -748,8 +879,8 @@ describe('notifications', () => { beforeEach(async () => { jest.clearAllMocks() - chatSender = await neode.create( - 'User', + chatSender = await Factory.build( + 'user', { id: 'chatSender', name: 'chatSender', @@ -779,7 +910,7 @@ describe('notifications', () => { }) describe('if the chatReceiver is online', () => { - it('sends no email', async () => { + it('publishes subscriptions but sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(true) await mutate({ @@ -790,13 +921,32 @@ describe('notifications', () => { }, }) + expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'chatReceiver', + }) + expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to chatReceiver', + senderId: 'chatSender', + username: 'chatSender', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'chatReceiver', + }) + expect(sendMailMock).not.toHaveBeenCalled() expect(chatMessageTemplateMock).not.toHaveBeenCalled() }) }) describe('if the chatReceiver is offline', () => { - it('sends an email', async () => { + it('publishes subscriptions and sends an email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await mutate({ @@ -807,13 +957,32 @@ describe('notifications', () => { }, }) + expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'chatReceiver', + }) + expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to chatReceiver', + senderId: 'chatSender', + username: 'chatSender', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'chatReceiver', + }) + expect(sendMailMock).toHaveBeenCalledTimes(1) expect(chatMessageTemplateMock).toHaveBeenCalledTimes(1) }) }) describe('if the chatReceiver has blocked chatSender', () => { - it('sends no email', async () => { + it('publishes no subscriptions and sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await chatReceiver.relateTo(chatSender, 'blocked') @@ -825,13 +994,37 @@ describe('notifications', () => { }, }) + expect(pubsubSpy).not.toHaveBeenCalled() + expect(pubsubSpy).not.toHaveBeenCalled() + + expect(sendMailMock).not.toHaveBeenCalled() + expect(chatMessageTemplateMock).not.toHaveBeenCalled() + }) + }) + + describe('if the chatReceiver has muted chatSender', () => { + it('publishes no subscriptions and sends no email', async () => { + isUserOnlineMock = jest.fn().mockReturnValue(false) + await chatReceiver.relateTo(chatSender, 'muted') + + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'Some nice message to chatReceiver', + }, + }) + + expect(pubsubSpy).not.toHaveBeenCalled() + expect(pubsubSpy).not.toHaveBeenCalled() + expect(sendMailMock).not.toHaveBeenCalled() expect(chatMessageTemplateMock).not.toHaveBeenCalled() }) }) describe('if the chatReceiver has disabled `emailNotificationsChatMessage`', () => { - it('sends no email', async () => { + it('publishes subscriptions but sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await chatReceiver.update({ emailNotificationsChatMessage: false }) @@ -843,6 +1036,25 @@ describe('notifications', () => { }, }) + expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'chatReceiver', + }) + expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to chatReceiver', + senderId: 'chatSender', + username: 'chatSender', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'chatReceiver', + }) + expect(sendMailMock).not.toHaveBeenCalled() expect(chatMessageTemplateMock).not.toHaveBeenCalled() }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.ts b/backend/src/middleware/notifications/notificationsMiddleware.ts index ebbcd7886..4926dc94e 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable security/detect-object-injection */ import { sendMail } from '@middleware/helpers/email/sendMail' import { @@ -7,55 +12,36 @@ import { import { isUserOnline } from '@middleware/helpers/isUserOnline' import { validateNotifyUsers } from '@middleware/validation/validationMiddleware' // eslint-disable-next-line import/no-cycle -import { pubsub, NOTIFICATION_ADDED } from '@src/server' +import { getUnreadRoomsCount } from '@schema/resolvers/rooms' +import { pubsub, NOTIFICATION_ADDED, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } from '@src/server' import extractMentionedUsers from './mentions/extractMentionedUsers' -const queryNotificationEmails = async (context, notificationUserIds) => { - if (!(notificationUserIds && notificationUserIds.length)) return [] - const userEmailCypher = ` - MATCH (user: User) - // blocked users are filtered out from notifications already - WHERE user.id in $notificationUserIds - WITH user - MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) - RETURN emailAddress {.email} - ` - const session = context.driver.session() - const writeTxResultPromise = session.readTransaction(async (transaction) => { - const emailAddressTransactionResponse = await transaction.run(userEmailCypher, { - notificationUserIds, - }) - return emailAddressTransactionResponse.records.map((record) => record.get('emailAddress')) - }) - try { - const emailAddresses = await writeTxResultPromise - return emailAddresses - } catch (error) { - throw new Error(error) - } finally { - session.close() - } -} - -const publishNotifications = async (context, promises, emailNotificationSetting: string) => { - let notifications = await Promise.all(promises) - notifications = notifications.flat() - const notificationsEmailAddresses = await queryNotificationEmails( - context, - notifications.map((notification) => notification.to.id), - ) - notifications.forEach((notificationAdded, index) => { +const publishNotifications = async ( + context, + notificationsPromise, + emailNotificationSetting: string, + emailsSent: string[] = [], +): Promise => { + const notifications = await notificationsPromise + notifications.forEach((notificationAdded) => { pubsub.publish(NOTIFICATION_ADDED, { notificationAdded }) - if (notificationAdded.to[emailNotificationSetting] ?? true) { + if ( + notificationAdded.email && // no primary email was found + (notificationAdded.to[emailNotificationSetting] ?? true) && + !isUserOnline(notificationAdded.to) && + !emailsSent.includes(notificationAdded.email) + ) { sendMail( notificationTemplate({ - email: notificationsEmailAddresses[index].email, + email: notificationAdded.email, variables: { notification: notificationAdded }, }), ) + emailsSent.push(notificationAdded.email) } }) + return emailsSent } const handleJoinGroup = async (resolve, root, args, context, resolveInfo) => { @@ -64,7 +50,7 @@ const handleJoinGroup = async (resolve, root, args, context, resolveInfo) => { if (user) { await publishNotifications( context, - [notifyOwnersOfGroup(groupId, userId, 'user_joined_group', context)], + notifyOwnersOfGroup(groupId, userId, 'user_joined_group', context), 'emailNotificationsGroupMemberJoined', ) } @@ -77,7 +63,7 @@ const handleLeaveGroup = async (resolve, root, args, context, resolveInfo) => { if (user) { await publishNotifications( context, - [notifyOwnersOfGroup(groupId, userId, 'user_left_group', context)], + notifyOwnersOfGroup(groupId, userId, 'user_left_group', context), 'emailNotificationsGroupMemberLeft', ) } @@ -90,7 +76,7 @@ const handleChangeGroupMemberRole = async (resolve, root, args, context, resolve if (user) { await publishNotifications( context, - [notifyMemberOfGroup(groupId, userId, 'changed_group_member_role', context)], + notifyMemberOfGroup(groupId, userId, 'changed_group_member_role', context), 'emailNotificationsGroupMemberRoleChanged', ) } @@ -103,7 +89,7 @@ const handleRemoveUserFromGroup = async (resolve, root, args, context, resolveIn if (user) { await publishNotifications( context, - [notifyMemberOfGroup(groupId, userId, 'removed_user_from_group', context)], + notifyMemberOfGroup(groupId, userId, 'removed_user_from_group', context), 'emailNotificationsGroupMemberRemoved', ) } @@ -115,20 +101,24 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo const idsOfUsers = extractMentionedUsers(args.content) const post = await resolve(root, args, context, resolveInfo) if (post) { - await publishNotifications( + const sentEmails: string[] = await publishNotifications( context, - [notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context)], + notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context), 'emailNotificationsMention', ) - await publishNotifications( - context, - [notifyFollowingUsers(post.id, groupId, context)], - 'emailNotificationsFollowingUsers', + sentEmails.concat( + await publishNotifications( + context, + notifyFollowingUsers(post.id, groupId, context), + 'emailNotificationsFollowingUsers', + sentEmails, + ), ) await publishNotifications( context, - [notifyGroupMembersOfNewPost(post.id, groupId, context)], + notifyGroupMembersOfNewPost(post.id, groupId, context), 'emailNotificationsPostInGroup', + sentEmails, ) } return post @@ -140,26 +130,23 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI const comment = await resolve(root, args, context, resolveInfo) const [postAuthor] = await postAuthorOfComment(comment.id, { context }) idsOfMentionedUsers = idsOfMentionedUsers.filter((id) => id !== postAuthor.id) - await publishNotifications( + const sentEmails: string[] = await publishNotifications( context, - [ - notifyUsersOfMention( - 'Comment', - comment.id, - idsOfMentionedUsers, - 'mentioned_in_comment', - context, - ), - ], + notifyUsersOfMention( + 'Comment', + comment.id, + idsOfMentionedUsers, + 'mentioned_in_comment', + context, + ), 'emailNotificationsMention', ) - await publishNotifications( context, - [notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context)], + notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context), 'emailNotificationsCommentOnObservedPost', + sentEmails, ) - return comment } @@ -187,17 +174,20 @@ const notifyFollowingUsers = async (postId, groupId, context) => { const cypher = ` MATCH (post:Post { id: $postId })<-[:WROTE]-(author:User { id: $userId })<-[:FOLLOWS]-(user:User) OPTIONAL MATCH (post)-[:IN]->(group:Group { id: $groupId }) - WITH post, author, user, group WHERE group IS NULL OR group.groupType = 'public' + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + WITH post, author, user, emailAddress, group + WHERE group IS NULL OR group.groupType = 'public' MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) - WITH notification, author, user, + WITH notification, author, user, emailAddress.email as email, post {.*, author: properties(author) } AS finalResource RETURN notification { .*, from: finalResource, to: properties(user), + email: email, relatedUser: properties(author) } ` @@ -212,8 +202,7 @@ const notifyFollowingUsers = async (postId, groupId, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -226,21 +215,25 @@ const notifyGroupMembersOfNewPost = async (postId, groupId, context) => { const reason = 'post_in_group' const cypher = ` MATCH (post:Post { id: $postId })<-[:WROTE]-(author:User { id: $userId }) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) MATCH (post)-[:IN]->(group:Group { id: $groupId })<-[membership:MEMBER_OF]-(user:User) WHERE NOT membership.role = 'pending' AND NOT (user)-[:MUTED]->(group) + AND NOT (user)-[:MUTED]->(author) + AND NOT (user)-[:BLOCKED]-(author) AND NOT user.id = $userId - WITH post, author, user + WITH post, author, user, emailAddress MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) - WITH notification, author, user, + WITH notification, author, user, emailAddress.email as email, post {.*, author: properties(author) } AS finalResource RETURN notification { .*, from: finalResource, to: properties(user), + email: email, relatedUser: properties(author) } ` @@ -255,8 +248,7 @@ const notifyGroupMembersOfNewPost = async (postId, groupId, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -272,12 +264,13 @@ const notifyOwnersOfGroup = async (groupId, userId, reason, context) => { WITH owner, group, user, membership MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(owner) WITH group, owner, notification, user, membership + OPTIONAL MATCH (owner)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) SET notification.relatedUserId = $userId - WITH owner, group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, user, notification - RETURN notification {.*, from: finalGroup, to: properties(owner), relatedUser: properties(user) } + WITH owner, emailAddress.email as email, group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, user, notification + RETURN notification {.*, from: finalGroup, to: properties(owner), email: email, relatedUser: properties(user) } ` const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -289,8 +282,7 @@ const notifyOwnersOfGroup = async (groupId, userId, reason, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -304,17 +296,18 @@ const notifyMemberOfGroup = async (groupId, userId, reason, context) => { MATCH (owner:User { id: $ownerId }) MATCH (user:User { id: $userId }) MATCH (group:Group { id: $groupId }) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) OPTIONAL MATCH (user)-[membership:MEMBER_OF]->(group) - WITH user, group, owner, membership + WITH user, group, owner, membership, emailAddress MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(user) - WITH group, user, notification, owner, membership + WITH group, user, notification, owner, membership, emailAddress SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) SET notification.relatedUserId = $ownerId WITH group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, - notification, user, owner - RETURN notification {.*, from: finalGroup, to: properties(user), relatedUser: properties(owner) } + notification, user, emailAddress.email as email, owner + RETURN notification {.*, from: finalGroup, to: properties(user), email: email, relatedUser: properties(owner) } ` const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -327,8 +320,7 @@ const notifyMemberOfGroup = async (groupId, userId, reason, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -337,7 +329,7 @@ const notifyMemberOfGroup = async (groupId, userId, reason, context) => { } const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { - if (!(idsOfUsers && idsOfUsers.length)) return [] + if (!idsOfUsers?.length) return [] await validateNotifyUsers(label, reason) let mentionedCypher switch (reason) { @@ -345,10 +337,16 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { mentionedCypher = ` MATCH (post: Post { id: $id })<-[:WROTE]-(author: User) MATCH (user: User) - WHERE user.id in $idsOfUsers - AND NOT (user)-[:BLOCKED]-(author) + WHERE user.id in $idsOfUsers + AND NOT (user)-[:BLOCKED]-(author) + AND NOT (user)-[:MUTED]->(author) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + OPTIONAL MATCH (post)-[:IN]->(group:Group) + OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(user) + WITH post, author, user, group, emailAddress + WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner'] MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) - WITH post AS resource, notification, user + WITH post AS resource, notification, user, emailAddress ` break } @@ -356,25 +354,32 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { mentionedCypher = ` MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(commenter: User) MATCH (user: User) - WHERE user.id in $idsOfUsers - AND NOT (user)-[:BLOCKED]-(commenter) - AND NOT (user)-[:BLOCKED]-(postAuthor) + WHERE user.id in $idsOfUsers + AND NOT (user)-[:BLOCKED]-(commenter) + AND NOT (user)-[:BLOCKED]-(postAuthor) + AND NOT (user)-[:MUTED]->(commenter) + AND NOT (user)-[:MUTED]->(postAuthor) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + OPTIONAL MATCH (post)-[:IN]->(group:Group) + OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(user) + WITH comment, user, group, emailAddress + WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner'] MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user) - WITH comment AS resource, notification, user + WITH comment AS resource, notification, user, emailAddress ` break } } mentionedCypher += ` - WITH notification, user, resource, + WITH notification, user, resource, emailAddress, [(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors, [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts - WITH resource, user, notification, authors, posts, + WITH resource, user, emailAddress.email as email, notification, authors, posts, resource {.*, __typename: [l IN labels(resource) WHERE l IN ['Post', 'Comment', 'Group']][0], author: authors[0], post: posts[0]} AS finalResource SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) - RETURN notification {.*, from: finalResource, to: properties(user), relatedUser: properties(user) } + RETURN notification {.*, from: finalResource, to: properties(user), email: email, relatedUser: properties(user) } ` const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -386,8 +391,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -402,19 +406,23 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => { const notificationTransactionResponse = await transaction.run( ` MATCH (observingUser:User)-[:OBSERVES { active: true }]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User) - WHERE NOT (observingUser)-[:BLOCKED]-(commenter) AND NOT observingUser.id = $userId - WITH observingUser, post, comment, commenter + WHERE NOT (observingUser)-[:BLOCKED]-(commenter) + AND NOT (observingUser)-[:MUTED]->(commenter) + AND NOT observingUser.id = $userId + OPTIONAL MATCH (observingUser)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + WITH observingUser, emailAddress, post, comment, commenter MATCH (postAuthor:User)-[:WROTE]->(post) MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(observingUser) SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) - WITH notification, observingUser, post, commenter, postAuthor, + WITH notification, observingUser, emailAddress.email as email, post, commenter, postAuthor, comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource RETURN notification { .*, from: finalResource, to: properties(observingUser), + email: email, relatedUser: properties(commenter) } `, @@ -427,8 +435,7 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } finally { session.close() } @@ -436,7 +443,7 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => { const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => { // Execute resolver - const result = await resolve(root, args, context, resolveInfo) + const message = await resolve(root, args, context, resolveInfo) // Query Parameters const { roomId } = args @@ -452,7 +459,7 @@ const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => MATCH (room)<-[:CHATS_IN]-(recipientUser:User)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) WHERE NOT recipientUser.id = $currentUserId AND NOT (recipientUser)-[:BLOCKED]-(senderUser) - AND NOT recipientUser.emailNotificationsChatMessage = false + AND NOT (recipientUser)-[:MUTED]->(senderUser) RETURN senderUser {.*}, recipientUser {.*}, emailAddress {.email} ` const txResponse = await transaction.run(messageRecipientCypher, { @@ -471,13 +478,27 @@ const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => // Execute Query const { senderUser, recipientUser, email } = await messageRecipient - // Send EMail if we found a user(not blocked) and he is not considered online - if (recipientUser && !isUserOnline(recipientUser)) { - void sendMail(chatMessageTemplate({ email, variables: { senderUser, recipientUser } })) + if (recipientUser) { + // send subscriptions + const roomCountUpdated = await getUnreadRoomsCount(recipientUser.id, session) + + void pubsub.publish(ROOM_COUNT_UPDATED, { + roomCountUpdated, + userId: recipientUser.id, + }) + void pubsub.publish(CHAT_MESSAGE_ADDED, { + chatMessageAdded: message, + userId: recipientUser.id, + }) + + // Send EMail if we found a user(not blocked) and he is not considered online + if (recipientUser.emailNotificationsChatMessage !== false && !isUserOnline(recipientUser)) { + void sendMail(chatMessageTemplate({ email, variables: { senderUser, recipientUser } })) + } } // Return resolver result to client - return result + return message } catch (error) { throw new Error(error) } finally { diff --git a/backend/src/middleware/orderByMiddleware.spec.ts b/backend/src/middleware/orderByMiddleware.spec.ts index 9534af76d..b98c21c0b 100644 --- a/backend/src/middleware/orderByMiddleware.spec.ts +++ b/backend/src/middleware/orderByMiddleware.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -25,7 +26,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/middleware/orderByMiddleware.ts b/backend/src/middleware/orderByMiddleware.ts index 64eac8b74..9b437a5e9 100644 --- a/backend/src/middleware/orderByMiddleware.ts +++ b/backend/src/middleware/orderByMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import cloneDeep from 'lodash/cloneDeep' const defaultOrderBy = (resolve, root, args, context, resolveInfo) => { diff --git a/backend/src/middleware/permissionsMiddleware.spec.ts b/backend/src/middleware/permissionsMiddleware.spec.ts index 81d73bae8..ca45005fe 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.ts +++ b/backend/src/middleware/permissionsMiddleware.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -29,7 +33,7 @@ describe('authorization', () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index 0f2b71678..3897a61e9 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -1,7 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { rule, shield, deny, allow, or, and } from 'graphql-shield' import CONFIG from '@config/index' import { getNeode } from '@db/neo4j' +import SocialMedia from '@models/SocialMedia' import { validateInviteCode } from '@schema/resolvers/transactions/inviteCodes' const debug = !!CONFIG.DEBUG @@ -12,26 +19,26 @@ const neode = getNeode() const isAuthenticated = rule({ cache: 'contextual', })(async (_parent, _args, ctx, _info) => { - return !!(ctx && ctx.user && ctx.user.id) + return !!ctx?.user?.id }) -const isModerator = rule()(async (parent, args, { user }, info) => { +const isModerator = rule()(async (_parent, _args, { user }, _info) => { return user && (user.role === 'moderator' || user.role === 'admin') }) -const isAdmin = rule()(async (parent, args, { user }, info) => { +const isAdmin = rule()(async (_parent, _args, { user }, _info) => { return user && user.role === 'admin' }) const onlyYourself = rule({ cache: 'no_cache', -})(async (parent, args, context, info) => { +})(async (_parent, args, context, _info) => { return context.user.id === args.id }) const isMyOwn = rule({ cache: 'no_cache', -})(async (parent, args, { user }, info) => { +})(async (parent, _args, { user }, _info) => { return user && user.id === parent.id }) @@ -42,21 +49,22 @@ const isMySocialMedia = rule({ if (!user) { return false } - let socialMedia = await neode.find('SocialMedia', args.id) + const socialMedia = await neode.find('SocialMedia', args.id) // Did we find a social media node? if (!socialMedia) { return false } - socialMedia = await socialMedia.toJson() // whats this for? + const socialMediaJson = await socialMedia.toJson() // whats this for? // Is it my social media entry? - return socialMedia.ownedBy.node.id === user.id + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (socialMediaJson.ownedBy as any).node.id === user.id }) const isAllowedToChangeGroupSettings = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const ownerId = user.id const { id: groupId } = args const session = driver.session() @@ -86,7 +94,7 @@ const isAllowedToChangeGroupSettings = rule({ const isAllowedSeeingGroupMembers = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { id: groupId } = args const session = driver.session() const readTxPromise = session.readTransaction(async (transaction) => { @@ -122,7 +130,7 @@ const isAllowedSeeingGroupMembers = rule({ const isAllowedToChangeGroupMemberRole = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const currentUserId = user.id const { groupId, userId, roleInGroup } = args if (currentUserId === userId) return false @@ -169,7 +177,7 @@ const isAllowedToChangeGroupMemberRole = rule({ const isAllowedToJoinGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { groupId, userId } = args const session = driver.session() const readTxPromise = session.readTransaction(async (transaction) => { @@ -199,7 +207,7 @@ const isAllowedToJoinGroup = rule({ const isAllowedToLeaveGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { groupId, userId } = args if (user.id !== userId) return false const session = driver.session() @@ -229,7 +237,7 @@ const isAllowedToLeaveGroup = rule({ const isMemberOfGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { groupId } = args if (!groupId) return true const userId = user.id @@ -257,7 +265,7 @@ const isMemberOfGroup = rule({ const canRemoveUserFromGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { groupId, userId } = args const currentUserId = user.id if (currentUserId === userId) return false @@ -293,7 +301,7 @@ const canRemoveUserFromGroup = rule({ const canCommentPost = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { postId } = args const userId = user.id const session = driver.session() @@ -350,7 +358,7 @@ const isAuthor = rule({ const isDeletingOwnAccount = rule({ cache: 'no_cache', -})(async (parent, args, context, _info) => { +})(async (_parent, args, context, _info) => { return context.user.id === args.id }) @@ -362,7 +370,7 @@ const noEmailFilter = rule({ const publicRegistration = rule()(() => CONFIG.PUBLIC_REGISTRATION) -const inviteRegistration = rule()(async (_parent, args, { user, driver }) => { +const inviteRegistration = rule()(async (_parent, args, { _user, driver }) => { if (!CONFIG.INVITE_REGISTRATION) return false const { inviteCode } = args const session = driver.session() @@ -429,10 +437,9 @@ export default shield( CreateSocialMedia: isAuthenticated, UpdateSocialMedia: isMySocialMedia, DeleteSocialMedia: isMySocialMedia, - // AddBadgeRewarded: isAdmin, - // RemoveBadgeRewarded: isAdmin, - reward: isAdmin, - unreward: isAdmin, + setVerificationBadge: isAdmin, + rewardTrophyBadge: isAdmin, + revokeBadge: isAdmin, followUser: isAuthenticated, unfollowUser: isAuthenticated, shout: isAuthenticated, @@ -469,6 +476,8 @@ export default shield( toggleObservePost: isAuthenticated, muteGroup: and(isAuthenticated, isMemberOfGroup), unmuteGroup: and(isAuthenticated, isMemberOfGroup), + setTrophyBadgeSelected: isAuthenticated, + resetTrophyBadgesSelected: isAuthenticated, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/middleware/sentryMiddleware.ts b/backend/src/middleware/sentryMiddleware.ts index b77f680d6..743ec32df 100644 --- a/backend/src/middleware/sentryMiddleware.ts +++ b/backend/src/middleware/sentryMiddleware.ts @@ -1,8 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { sentry } from 'graphql-middleware-sentry' import CONFIG from '@config/index' -// eslint-disable-next-line import/no-mutable-exports +// eslint-disable-next-line import/no-mutable-exports, @typescript-eslint/no-explicit-any let sentryMiddleware: any = (resolve, root, args, context, resolveInfo) => resolve(root, args, context, resolveInfo) @@ -14,9 +18,10 @@ if (CONFIG.SENTRY_DSN_BACKEND) { release: CONFIG.COMMIT, environment: CONFIG.NODE_ENV, }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any withScope: (scope, error, context: any) => { scope.setUser({ - id: context.user && context.user.id, + id: context.user?.id, }) scope.setExtra('body', context.req.body) scope.setExtra('origin', context.req.headers.origin) diff --git a/backend/src/middleware/sluggifyMiddleware.ts b/backend/src/middleware/sluggifyMiddleware.ts index bbe47c9aa..92c2c1367 100644 --- a/backend/src/middleware/sluggifyMiddleware.ts +++ b/backend/src/middleware/sluggifyMiddleware.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import uniqueSlug from './slugify/uniqueSlug' const isUniqueFor = (context, type) => { diff --git a/backend/src/middleware/slugify/uniqueSlug.ts b/backend/src/middleware/slugify/uniqueSlug.ts index 41d58ece3..e24b15eb3 100644 --- a/backend/src/middleware/slugify/uniqueSlug.ts +++ b/backend/src/middleware/slugify/uniqueSlug.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import slugify from 'slug' export default async function uniqueSlug(string, isUnique) { diff --git a/backend/src/middleware/slugifyMiddleware.spec.ts b/backend/src/middleware/slugifyMiddleware.spec.ts index 9e55d54b1..75a52e4cf 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.ts +++ b/backend/src/middleware/slugifyMiddleware.spec.ts @@ -1,10 +1,15 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { signupVerificationMutation } from '@graphql/authentications' -import { createGroupMutation, updateGroupMutation } from '@graphql/groups' -import { createPostMutation } from '@graphql/posts' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { signupVerificationMutation } from '@graphql/queries/signupVerificationMutation' +import { updateGroupMutation } from '@graphql/queries/updateGroupMutation' import createServer from '@src/server' let authenticatedUser @@ -37,7 +42,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts index fa62ed101..ed9dcbf37 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -42,7 +48,7 @@ beforeAll(async () => { }, { avatar: Factory.build('image', { - url: '/some/offensive/avatar.jpg', + url: 'http://localhost/some/offensive/avatar.jpg', }), }, ), @@ -110,7 +116,7 @@ beforeAll(async () => { }, { image: Factory.build('image', { - url: '/some/offensive/image.jpg', + url: 'http://localhost/some/offensive/image.jpg', }), author: troll, categoryIds, @@ -196,7 +202,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('softDeleteMiddleware', () => { @@ -272,7 +278,7 @@ describe('softDeleteMiddleware', () => { expect(subject.about).toEqual('This self description is very offensive')) it('displays avatar', () => expect(subject.avatar).toEqual({ - url: expect.stringContaining('/some/offensive/avatar.jpg'), + url: expect.stringMatching('http://localhost/some/offensive/avatar.jpg'), })) }) @@ -287,7 +293,7 @@ describe('softDeleteMiddleware', () => { expect(subject.contentExcerpt).toEqual('This is an offensive post content')) it('displays image', () => expect(subject.image).toEqual({ - url: expect.stringContaining('/some/offensive/image.jpg'), + url: expect.stringMatching('http://localhost/some/offensive/image.jpg'), })) }) diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.ts b/backend/src/middleware/softDelete/softDeleteMiddleware.ts index 2e1f60251..4120733ff 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.ts +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ const isModerator = ({ user }) => { return user && (user.role === 'moderator' || user.role === 'admin') } diff --git a/backend/src/middleware/userInteractions.spec.ts b/backend/src/middleware/userInteractions.spec.ts index 37b5401e3..61d92ff83 100644 --- a/backend/src/middleware/userInteractions.spec.ts +++ b/backend/src/middleware/userInteractions.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -43,7 +46,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('middleware/userInteractions', () => { diff --git a/backend/src/middleware/userInteractions.ts b/backend/src/middleware/userInteractions.ts index 62e8e47f7..bb850a650 100644 --- a/backend/src/middleware/userInteractions.ts +++ b/backend/src/middleware/userInteractions.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ const createRelatedCypher = (relation) => ` MATCH (user:User { id: $currentUser}) MATCH (post:Post { id: $postId}) diff --git a/backend/src/middleware/validation/validationMiddleware.spec.ts b/backend/src/middleware/validation/validationMiddleware.spec.ts index 8e4b4329f..ea4f6ec54 100644 --- a/backend/src/middleware/validation/validationMiddleware.spec.ts +++ b/backend/src/middleware/validation/validationMiddleware.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -76,7 +79,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/middleware/validation/validationMiddleware.ts b/backend/src/middleware/validation/validationMiddleware.ts index 072f2d7b9..75f8f5d09 100644 --- a/backend/src/middleware/validation/validationMiddleware.ts +++ b/backend/src/middleware/validation/validationMiddleware.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' const COMMENT_MIN_LENGTH = 1 @@ -80,7 +87,7 @@ const validateReview = async (resolve, root, args, context, info) => { try { const txResult = await reportReadTxPromise existingReportedResource = txResult - if (!existingReportedResource || !existingReportedResource.length) + if (!existingReportedResource?.length) throw new Error(`Resource not found or is not a Post|Comment|User!`) existingReportedResource = existingReportedResource[0] if (!existingReportedResource.filed) diff --git a/backend/src/middleware/xssMiddleware.ts b/backend/src/middleware/xssMiddleware.ts index 3ed310b40..31ded633c 100644 --- a/backend/src/middleware/xssMiddleware.ts +++ b/backend/src/middleware/xssMiddleware.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import walkRecursive from '@helpers/walkRecursive' import { cleanHtml } from './helpers/cleanHtml' diff --git a/backend/src/models/Badge.ts b/backend/src/models/Badge.ts index 9c4831041..e8d61cb42 100644 --- a/backend/src/models/Badge.ts +++ b/backend/src/models/Badge.ts @@ -1,7 +1,7 @@ export default { id: { type: 'string', primary: true, lowercase: true }, - status: { type: 'string', valid: ['permanent', 'temporary'] }, - type: { type: 'string', valid: ['role', 'crowdfunding'] }, + type: { type: 'string', valid: ['verification', 'trophy'] }, icon: { type: 'string', required: true }, + description: { type: 'string', required: true }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, } diff --git a/backend/src/models/User.spec.ts b/backend/src/models/User.spec.ts index 3fde03462..cea2d4db0 100644 --- a/backend/src/models/User.spec.ts +++ b/backend/src/models/User.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ + import { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' @@ -10,7 +13,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 754f879a4..77a37c3c1 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -52,6 +52,24 @@ export default { target: 'Badge', direction: 'in', }, + selected: { + type: 'relationship', + relationship: 'SELECTED', + target: 'Badge', + direction: 'out', + properties: { + slot: { + type: 'int', + required: true, + }, + }, + }, + verifies: { + type: 'relationship', + relationship: 'VERIFIES', + target: 'Badge', + direction: 'in', + }, invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' }, lastActiveAt: { type: 'string', isoDate: true }, lastOnlineStatus: { type: 'string' }, diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index e02cbc242..6bbdab338 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -1,7 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable n/no-missing-require */ /* eslint-disable n/global-require */ // NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm // module that is not browser-compatible. Node's `fs` module is server-side only +// eslint-disable-next-line @typescript-eslint/no-explicit-any declare let Cypress: any | undefined export default { Image: typeof Cypress !== 'undefined' ? require('./Image') : require('./Image').default, diff --git a/backend/src/schema/index.ts b/backend/src/schema/index.ts index e043bc243..55eccb4dc 100644 --- a/backend/src/schema/index.ts +++ b/backend/src/schema/index.ts @@ -1,7 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { makeAugmentedSchema } from 'neo4j-graphql-js' +import typeDefs from '@graphql/types/index' + import resolvers from './resolvers' -import typeDefs from './types' export default makeAugmentedSchema({ typeDefs, diff --git a/backend/src/schema/resolvers/badges.spec.ts b/backend/src/schema/resolvers/badges.spec.ts new file mode 100644 index 000000000..e6b5173a9 --- /dev/null +++ b/backend/src/schema/resolvers/badges.spec.ts @@ -0,0 +1,888 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' + +const driver = getDriver() +const instance = getNeode() + +let authenticatedUser, regularUser, administrator, moderator, badge, verification, query, mutate + +describe('Badges', () => { + beforeAll(async () => { + await cleanDatabase() + + const { server } = createServer({ + context: () => { + return { + driver, + neode: instance, + user: authenticatedUser, + } + }, + }) + query = createTestClient(server).query + mutate = createTestClient(server).mutate + }) + + afterAll(async () => { + await cleanDatabase() + await driver.close() + }) + + beforeEach(async () => { + regularUser = await Factory.build( + 'user', + { + id: 'regular-user-id', + role: 'user', + }, + { + email: 'user@example.org', + password: '1234', + }, + ) + moderator = await Factory.build( + 'user', + { + id: 'moderator-id', + role: 'moderator', + }, + { + email: 'moderator@example.org', + }, + ) + administrator = await Factory.build( + 'user', + { + id: 'admin-id', + role: 'admin', + }, + { + email: 'admin@example.org', + }, + ) + badge = await Factory.build('badge', { + id: 'trophy_rhino', + type: 'trophy', + description: 'You earned a rhino', + icon: '/img/badges/trophy_blue_rhino.svg', + }) + + verification = await Factory.build('badge', { + id: 'verification_moderator', + type: 'verification', + description: 'You are a moderator', + icon: '/img/badges/verification_red_moderator.svg', + }) + }) + + // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 + afterEach(async () => { + await cleanDatabase() + }) + + describe('setVerificationBadge', () => { + const variables = { + badgeId: 'verification_moderator', + userId: 'regular-user-id', + } + + const setVerificationBadgeMutation = gql` + mutation ($badgeId: ID!, $userId: ID!) { + setVerificationBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + isDefault + } + badgeTrophies { + id + } + } + } + ` + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect( + mutate({ mutation: setVerificationBadgeMutation, variables }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated as moderator', () => { + beforeEach(async () => { + authenticatedUser = moderator.toJson() + }) + + describe('rewards badge to user', () => { + it('throws authorization error', async () => { + await expect( + mutate({ mutation: setVerificationBadgeMutation, variables }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + }) + + describe('authenticated as admin', () => { + beforeEach(async () => { + authenticatedUser = await administrator.toJson() + }) + + describe('badge for id does not exist', () => { + it('rejects with an informative error message', async () => { + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { userId: 'regular-user-id', badgeId: 'non-existent-badge-id' }, + }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + describe('non-existent user', () => { + it('rejects with a telling error message', async () => { + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { userId: 'non-existent-user-id', badgeId: 'verification_moderator' }, + }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + describe('badge is not a verification badge', () => { + it('rejects with a telling error message', async () => { + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { userId: 'regular-user-id', badgeId: 'trophy_rhino' }, + }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + it('rewards a verification badge to the user', async () => { + const expected = { + data: { + setVerificationBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'verification_moderator', isDefault: false }, + badgeTrophies: [], + }, + }, + errors: undefined, + } + await expect( + mutate({ mutation: setVerificationBadgeMutation, variables }), + ).resolves.toMatchObject(expected) + }) + + it('overrides the existing verification if a second verification badge is rewarded to the same user', async () => { + await Factory.build('badge', { + id: 'verification_admin', + type: 'verification', + description: 'You are an admin', + icon: '/img/badges/verification_red_admin.svg', + }) + const expected = { + data: { + setVerificationBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'verification_admin', isDefault: false }, + badgeTrophies: [], + }, + }, + errors: undefined, + } + await mutate({ + mutation: setVerificationBadgeMutation, + variables: { + userId: 'regular-user-id', + badgeId: 'verification_moderator', + }, + }) + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { + userId: 'regular-user-id', + badgeId: 'verification_admin', + }, + }), + ).resolves.toMatchObject(expected) + }) + + it('rewards the same verification badge as well to another user', async () => { + const expected = { + data: { + setVerificationBadge: { + id: 'regular-user-2-id', + badgeVerification: { id: 'verification_moderator', isDefault: false }, + badgeTrophies: [], + }, + }, + errors: undefined, + } + await Factory.build( + 'user', + { + id: 'regular-user-2-id', + }, + { + email: 'regular2@email.com', + }, + ) + await mutate({ + mutation: setVerificationBadgeMutation, + variables, + }) + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { + userId: 'regular-user-2-id', + badgeId: 'verification_moderator', + }, + }), + ).resolves.toMatchObject(expected) + }) + }) + }) + + describe('rewardTrophyBadge', () => { + const variables = { + badgeId: 'trophy_rhino', + userId: 'regular-user-id', + } + + const rewardTrophyBadgeMutation = gql` + mutation ($badgeId: ID!, $userId: ID!) { + rewardTrophyBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + isDefault + } + badgeTrophies { + id + } + } + } + ` + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect( + mutate({ mutation: rewardTrophyBadgeMutation, variables }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated as moderator', () => { + beforeEach(async () => { + authenticatedUser = moderator.toJson() + }) + + describe('rewards badge to user', () => { + it('throws authorization error', async () => { + await expect( + mutate({ mutation: rewardTrophyBadgeMutation, variables }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + }) + + describe('authenticated as admin', () => { + beforeEach(async () => { + authenticatedUser = await administrator.toJson() + }) + + describe('badge for id does not exist', () => { + it('rejects with an informative error message', async () => { + await expect( + mutate({ + mutation: rewardTrophyBadgeMutation, + variables: { userId: 'regular-user-id', badgeId: 'non-existent-badge-id' }, + }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + describe('non-existent user', () => { + it('rejects with a telling error message', async () => { + await expect( + mutate({ + mutation: rewardTrophyBadgeMutation, + variables: { userId: 'non-existent-user-id', badgeId: 'trophy_rhino' }, + }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + describe('badge is a verification Badge', () => { + it('rejects with a telling error message', async () => { + await expect( + mutate({ + mutation: rewardTrophyBadgeMutation, + variables: { userId: 'regular-user-id', badgeId: 'verification_moderator' }, + }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + it('rewards a badge to the user', async () => { + const expected = { + data: { + rewardTrophyBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'default_verification', isDefault: true }, + badgeTrophies: [{ id: 'trophy_rhino' }], + }, + }, + errors: undefined, + } + await expect( + mutate({ mutation: rewardTrophyBadgeMutation, variables }), + ).resolves.toMatchObject(expected) + }) + + it('rewards a second different badge to the same user', async () => { + await Factory.build('badge', { + id: 'trophy_racoon', + type: 'trophy', + description: 'You earned a racoon', + icon: '/img/badges/trophy_blue_racoon.svg', + }) + const trophies = [{ id: 'trophy_racoon' }, { id: 'trophy_rhino' }] + const expected = { + data: { + rewardTrophyBadge: { + id: 'regular-user-id', + badgeTrophies: expect.arrayContaining(trophies), + }, + }, + errors: undefined, + } + await mutate({ + mutation: rewardTrophyBadgeMutation, + variables: { + userId: 'regular-user-id', + badgeId: 'trophy_rhino', + }, + }) + await expect( + mutate({ + mutation: rewardTrophyBadgeMutation, + variables: { + userId: 'regular-user-id', + badgeId: 'trophy_racoon', + }, + }), + ).resolves.toMatchObject(expected) + }) + + it('rewards the same badge as well to another user', async () => { + const expected = { + data: { + rewardTrophyBadge: { + id: 'regular-user-2-id', + badgeTrophies: [{ id: 'trophy_rhino' }], + }, + }, + errors: undefined, + } + await Factory.build( + 'user', + { + id: 'regular-user-2-id', + }, + { + email: 'regular2@email.com', + }, + ) + await mutate({ + mutation: rewardTrophyBadgeMutation, + variables, + }) + await expect( + mutate({ + mutation: rewardTrophyBadgeMutation, + variables: { + userId: 'regular-user-2-id', + badgeId: 'trophy_rhino', + }, + }), + ).resolves.toMatchObject(expected) + }) + + it('creates no duplicate reward relationships', async () => { + await mutate({ + mutation: rewardTrophyBadgeMutation, + variables, + }) + await mutate({ + mutation: rewardTrophyBadgeMutation, + variables, + }) + + const userQuery = gql` + { + User(id: "regular-user-id") { + badgeTrophiesCount + badgeTrophies { + id + } + } + } + ` + const expected = { + data: { User: [{ badgeTrophiesCount: 1, badgeTrophies: [{ id: 'trophy_rhino' }] }] }, + errors: undefined, + } + + await expect(query({ query: userQuery })).resolves.toMatchObject(expected) + }) + }) + }) + + describe('revokeBadge', () => { + const variables = { + badgeId: 'trophy_rhino', + userId: 'regular-user-id', + } + + beforeEach(async () => { + await regularUser.relateTo(badge, 'rewarded') + await regularUser.relateTo(verification, 'verifies') + await regularUser.relateTo(badge, 'selected', { slot: 6 }) + }) + + const revokeBadgeMutation = gql` + mutation ($badgeId: ID!, $userId: ID!) { + revokeBadge(badgeId: $badgeId, userId: $userId) { + id + badgeTrophies { + id + } + badgeVerification { + id + isDefault + } + badgeTrophiesSelected { + id + isDefault + } + } + } + ` + + describe('check test setup', () => { + it('user has one badge and has it selected', async () => { + authenticatedUser = regularUser.toJson() + const userQuery = gql` + { + User(id: "regular-user-id") { + badgeTrophiesCount + badgeTrophies { + id + } + badgeVerification { + id + isDefault + } + badgeTrophiesSelected { + id + isDefault + } + } + } + ` + const expected = { + data: { + User: [ + { + badgeTrophiesCount: 1, + badgeTrophies: [{ id: 'trophy_rhino' }], + badgeVerification: { + id: 'verification_moderator', + isDefault: false, + }, + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'trophy_rhino', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + }, + ], + }, + errors: undefined, + } + await expect(query({ query: userQuery })).resolves.toMatchObject(expected) + }) + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject({ + data: { revokeBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated moderator', () => { + beforeEach(async () => { + authenticatedUser = await moderator.toJson() + }) + + describe('removes badge from user', () => { + it('throws authorization error', async () => { + await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject( + { + data: { revokeBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }, + ) + }) + }) + }) + + describe('authenticated admin', () => { + beforeEach(async () => { + authenticatedUser = await administrator.toJson() + }) + + it('removes a badge from user', async () => { + await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject({ + data: { + revokeBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'verification_moderator', isDefault: false }, + badgeTrophies: [], + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('does not crash when revoking multiple times', async () => { + await mutate({ mutation: revokeBadgeMutation, variables }) + await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject({ + data: { + revokeBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'verification_moderator', isDefault: false }, + badgeTrophies: [], + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('removes a verification from user', async () => { + await expect( + mutate({ + mutation: revokeBadgeMutation, + variables: { + badgeId: 'verification_moderator', + userId: 'regular-user-id', + }, + }), + ).resolves.toMatchObject({ + data: { + revokeBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'default_verification', isDefault: true }, + badgeTrophies: [{ id: 'trophy_rhino' }], + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'trophy_rhino', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('does not crash when removing verification multiple times', async () => { + await mutate({ + mutation: revokeBadgeMutation, + variables: { + badgeId: 'verification_moderator', + userId: 'regular-user-id', + }, + }) + await expect( + mutate({ + mutation: revokeBadgeMutation, + variables: { + badgeId: 'verification_moderator', + userId: 'regular-user-id', + }, + }), + ).resolves.toMatchObject({ + data: { + revokeBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'default_verification', isDefault: true }, + badgeTrophies: [{ id: 'trophy_rhino' }], + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'trophy_rhino', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + }, + }, + errors: undefined, + }) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/badges.ts b/backend/src/schema/resolvers/badges.ts index d10d6b482..7c107e42c 100644 --- a/backend/src/schema/resolvers/badges.ts +++ b/backend/src/schema/resolvers/badges.ts @@ -1,9 +1,148 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { neo4jgraphql } from 'neo4j-graphql-js' +export const defaultTrophyBadge = { + id: 'default_trophy', + type: 'trophy', + icon: '/img/badges/default_trophy.svg', + description: '', + createdAt: '', +} + +export const defaultVerificationBadge = { + id: 'default_verification', + type: 'verification', + icon: '/img/badges/default_verification.svg', + description: '', + createdAt: '', +} + export default { Query: { - Badge: async (object, args, context, resolveInfo) => { - return neo4jgraphql(object, args, context, resolveInfo) + Badge: async (object, args, context, resolveInfo) => + neo4jgraphql(object, args, context, resolveInfo), + }, + + Mutation: { + setVerificationBadge: async (_object, args, context, _resolveInfo) => { + const { + user: { id: currentUserId }, + } = context + const { badgeId, userId } = args + const session = context.driver.session() + + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const response = await transaction.run( + ` + MATCH (badge:Badge {id: $badgeId, type: 'verification'}), (user:User {id: $userId}) + OPTIONAL MATCH (:Badge {type: 'verification'})-[verify:VERIFIES]->(user) + DELETE verify + MERGE (badge)-[relation:VERIFIES {by: $currentUserId}]->(user) + RETURN relation, user {.*} + `, + { + badgeId, + userId, + currentUserId, + }, + ) + return { + relation: response.records.map((record) => record.get('relation'))[0], + user: response.records.map((record) => record.get('user'))[0], + } + }) + try { + const { relation, user } = await writeTxResultPromise + if (!relation) { + throw new Error( + 'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + ) + } + return user + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + + rewardTrophyBadge: async (_object, args, context, _resolveInfo) => { + const { + user: { id: currentUserId }, + } = context + const { badgeId, userId } = args + const session = context.driver.session() + + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const response = await transaction.run( + ` + MATCH (badge:Badge {id: $badgeId, type: 'trophy'}), (user:User {id: $userId}) + MERGE (badge)-[relation:REWARDED {by: $currentUserId}]->(user) + RETURN relation, user {.*} + `, + { + badgeId, + userId, + currentUserId, + }, + ) + return { + relation: response.records.map((record) => record.get('relation'))[0], + user: response.records.map((record) => record.get('user'))[0], + } + }) + try { + const { relation, user } = await writeTxResultPromise + if (!relation) { + throw new Error( + 'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + ) + } + return user + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + + revokeBadge: async (_object, args, context, _resolveInfo) => { + const { badgeId, userId } = args + const session = context.driver.session() + + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const response = await transaction.run( + ` + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (badge:Badge {id: $badgeId})-[rewarded:REWARDED|VERIFIES]->(user) + OPTIONAL MATCH (user)-[selected:SELECTED]->(badge) + DELETE rewarded + DELETE selected + RETURN user {.*} + `, + { + badgeId, + userId, + }, + ) + return response.records.map((record) => record.get('user'))[0] + }) + try { + return await writeTxResultPromise + } catch (error) { + throw new Error(error) + } finally { + session.close() + } }, }, + Badge: { + isDefault: async (parent, _params, _context, _resolveInfo) => + [defaultTrophyBadge.id, defaultVerificationBadge.id].includes(parent.id), + }, } diff --git a/backend/src/schema/resolvers/comments.spec.ts b/backend/src/schema/resolvers/comments.spec.ts index e92daf86e..a7177d754 100644 --- a/backend/src/schema/resolvers/comments.spec.ts +++ b/backend/src/schema/resolvers/comments.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -26,7 +30,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/schema/resolvers/comments.ts b/backend/src/schema/resolvers/comments.ts index 897c71d6f..3400b1d23 100644 --- a/backend/src/schema/resolvers/comments.ts +++ b/backend/src/schema/resolvers/comments.ts @@ -1,10 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' import Resolver from './helpers/Resolver' export default { Mutation: { - CreateComment: async (object, params, context, resolveInfo) => { + CreateComment: async (_object, params, context, _resolveInfo) => { const { postId } = params const { user, driver } = context // Adding relationship from comment to post by passing in the postId, diff --git a/backend/src/schema/resolvers/donations.spec.ts b/backend/src/schema/resolvers/donations.spec.ts index ef2070d4e..8fc23d4e9 100644 --- a/backend/src/schema/resolvers/donations.spec.ts +++ b/backend/src/schema/resolvers/donations.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -38,7 +42,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('donations', () => { diff --git a/backend/src/schema/resolvers/donations.ts b/backend/src/schema/resolvers/donations.ts index d077e7bed..017a97f5f 100644 --- a/backend/src/schema/resolvers/donations.ts +++ b/backend/src/schema/resolvers/donations.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export default { Query: { Donations: async (_parent, _params, context, _resolveInfo) => { diff --git a/backend/src/schema/resolvers/emails.spec.ts b/backend/src/schema/resolvers/emails.spec.ts index 63141a3fc..f77602463 100644 --- a/backend/src/schema/resolvers/emails.spec.ts +++ b/backend/src/schema/resolvers/emails.spec.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -31,7 +36,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -105,11 +110,14 @@ describe('AddEmailAddress', () => { it('connects `UnverifiedEmailAddress` to the authenticated user', async () => { await mutate({ mutation, variables }) - const result = await neode.cypher(` + const result = await neode.cypher( + ` MATCH(u:User)-[:PRIMARY_EMAIL]->(:EmailAddress {email: "user@example.org"}) MATCH(u:User)<-[:BELONGS_TO]-(e:UnverifiedEmailAddress {email: "new-email@example.org"}) RETURN e - `) + `, + {}, + ) const email = neode.hydrateFirst(result, 'e', neode.model('UnverifiedEmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'new-email@example.org', @@ -252,10 +260,13 @@ describe('VerifyEmailAddress', () => { it('connects the new `EmailAddress` as PRIMARY', async () => { await mutate({ mutation, variables }) - const result = await neode.cypher(` + const result = await neode.cypher( + ` MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "to-be-verified@example.org"}) RETURN e - `) + `, + {}, + ) const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'to-be-verified@example.org', @@ -267,13 +278,13 @@ describe('VerifyEmailAddress', () => { MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "user@example.org"}) RETURN e ` - let result = await neode.cypher(cypherStatement) + let result = await neode.cypher(cypherStatement, {}) let email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'user@example.org', }) await mutate({ mutation, variables }) - result = await neode.cypher(cypherStatement) + result = await neode.cypher(cypherStatement, {}) email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email).toBe(false) }) @@ -283,13 +294,13 @@ describe('VerifyEmailAddress', () => { MATCH(u:User {id: "567"})<-[:BELONGS_TO]-(e:EmailAddress {email: "user@example.org"}) RETURN e ` - let result = await neode.cypher(cypherStatement) + let result = await neode.cypher(cypherStatement, {}) let email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'user@example.org', }) await mutate({ mutation, variables }) - result = await neode.cypher(cypherStatement) + result = await neode.cypher(cypherStatement, {}) email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email).toBe(false) }) @@ -314,10 +325,13 @@ describe('VerifyEmailAddress', () => { it('connects the new `EmailAddress` as PRIMARY', async () => { await mutate({ mutation, variables }) - const result = await neode.cypher(` + const result = await neode.cypher( + ` MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "to-be-verified@example.org"}) RETURN e - `) + `, + {}, + ) const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'to-be-verified@example.org', diff --git a/backend/src/schema/resolvers/emails.ts b/backend/src/schema/resolvers/emails.ts index 0638ec634..be721dda5 100644 --- a/backend/src/schema/resolvers/emails.ts +++ b/backend/src/schema/resolvers/emails.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' // eslint-disable-next-line import/extensions import Validator from 'neode/build/Services/Validator.js' @@ -43,7 +48,7 @@ export default { // check email does not belong to anybody const existingEmail = await existingEmailAddress({ args, context }) - if (existingEmail && existingEmail.alreadyExistingEmail && existingEmail.user) + if (existingEmail?.alreadyExistingEmail && existingEmail.user) return existingEmail.alreadyExistingEmail const nonce = generateNonce() diff --git a/backend/src/schema/resolvers/embeds.spec.ts b/backend/src/schema/resolvers/embeds.spec.ts index 92dd224e3..f6de4d13e 100644 --- a/backend/src/schema/resolvers/embeds.spec.ts +++ b/backend/src/schema/resolvers/embeds.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import fs from 'node:fs' import path from 'node:path' @@ -17,14 +20,17 @@ afterEach(() => { let variables = {} +// eslint-disable-next-line n/no-sync const HumanConnectionOrg = fs.readFileSync( path.join(__dirname, '../../../snapshots/embeds/HumanConnectionOrg.html'), 'utf8', ) +// eslint-disable-next-line n/no-sync const pr3934 = fs.readFileSync( path.join(__dirname, '../../../snapshots/embeds/pr3934.html'), 'utf8', ) +// eslint-disable-next-line n/no-sync const babyLovesCat = fs.readFileSync( path.join(__dirname, '../../../snapshots/embeds/babyLovesCat.html'), 'utf8', @@ -55,6 +61,7 @@ describe('Query', () => { beforeEach(() => { embedAction = async (variables) => { const { server } = createServer({ + // eslint-disable-next-line @typescript-eslint/no-empty-function context: () => {}, }) const { query } = createTestClient(server) diff --git a/backend/src/schema/resolvers/embeds.ts b/backend/src/schema/resolvers/embeds.ts index a75365ec7..8ce144b4f 100644 --- a/backend/src/schema/resolvers/embeds.ts +++ b/backend/src/schema/resolvers/embeds.ts @@ -1,9 +1,12 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import scrape from './embeds/scraper' import { undefinedToNullResolver } from './helpers/Resolver' export default { Query: { - embed: async (object, { url }, context, resolveInfo) => { + embed: async (_object, { url }, _context, _resolveInfo) => { return scrape(url) }, }, @@ -22,7 +25,7 @@ export default { 'lang', 'html', ]), - sources: async (parent, params, context, resolveInfo) => { + sources: async (parent, _params, _context, _resolveInfo) => { return typeof parent.sources === 'undefined' ? [] : parent.sources }, }, diff --git a/backend/src/schema/resolvers/embeds/findProvider.ts b/backend/src/schema/resolvers/embeds/findProvider.ts index a9a30f2bf..6f5e0df90 100644 --- a/backend/src/schema/resolvers/embeds/findProvider.ts +++ b/backend/src/schema/resolvers/embeds/findProvider.ts @@ -1,8 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import fs from 'node:fs' import path from 'node:path' import { minimatch } from 'minimatch' +// eslint-disable-next-line n/no-sync let oEmbedProvidersFile = fs.readFileSync( path.join(__dirname, '../../../../public/providers.json'), 'utf8', diff --git a/backend/src/schema/resolvers/embeds/scraper.ts b/backend/src/schema/resolvers/embeds/scraper.ts index e4e19e6b9..a8cd07a76 100644 --- a/backend/src/schema/resolvers/embeds/scraper.ts +++ b/backend/src/schema/resolvers/embeds/scraper.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable n/no-extraneous-require */ /* eslint-disable n/global-require */ /* eslint-disable import/no-commonjs */ @@ -80,8 +87,9 @@ export default async function scrape(url) { throw new ApolloError('Not found', 'NOT_FOUND') } - return { - type: 'link', - ...output, + if (!output.type) { + output.type = 'link' } + + return output } diff --git a/backend/src/schema/resolvers/filter-posts.spec.ts b/backend/src/schema/resolvers/filter-posts.spec.ts index d5d4485a3..c29b98365 100644 --- a/backend/src/schema/resolvers/filter-posts.spec.ts +++ b/backend/src/schema/resolvers/filter-posts.spec.ts @@ -1,9 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { filterPosts, createPostMutation } from '@graphql/posts' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { filterPosts } from '@graphql/queries/filterPosts' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false @@ -34,7 +38,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('Filter Posts', () => { diff --git a/backend/src/schema/resolvers/follow.spec.ts b/backend/src/schema/resolvers/follow.spec.ts index 1e05b2fea..e846eb56f 100644 --- a/backend/src/schema/resolvers/follow.spec.ts +++ b/backend/src/schema/resolvers/follow.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -72,7 +76,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/schema/resolvers/follow.ts b/backend/src/schema/resolvers/follow.ts index 11447974d..d08f243b1 100644 --- a/backend/src/schema/resolvers/follow.ts +++ b/backend/src/schema/resolvers/follow.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { getNeode } from '@db/neo4j' const neode = getNeode() diff --git a/backend/src/schema/resolvers/groups.spec.ts b/backend/src/schema/resolvers/groups.spec.ts index 624b09e39..664f57397 100644 --- a/backend/src/schema/resolvers/groups.spec.ts +++ b/backend/src/schema/resolvers/groups.spec.ts @@ -1,18 +1,21 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { createTestClient } from 'apollo-server-testing' import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { - createGroupMutation, - updateGroupMutation, - joinGroupMutation, - leaveGroupMutation, - changeGroupMemberRoleMutation, - removeUserFromGroupMutation, - groupMembersQuery, - groupQuery, -} from '@graphql/groups' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { groupMembersQuery } from '@graphql/queries/groupMembersQuery' +import { groupQuery } from '@graphql/queries/groupQuery' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation' +import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation' +import { updateGroupMutation } from '@graphql/queries/updateGroupMutation' import createServer from '@src/server' const driver = getDriver() @@ -238,7 +241,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('in mode', () => { @@ -2424,7 +2427,7 @@ describe('in mode', () => { id: groupId, }, }) - return result.data && result.data.GroupMembers + return result.data?.GroupMembers ? !!result.data.GroupMembers.find((member) => member.id === userId) : null } diff --git a/backend/src/schema/resolvers/groups.ts b/backend/src/schema/resolvers/groups.ts index 4bf535f35..96d806bf8 100644 --- a/backend/src/schema/resolvers/groups.ts +++ b/backend/src/schema/resolvers/groups.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' import { v4 as uuid } from 'uuid' @@ -440,7 +447,7 @@ export default { }, boolean: { isMutedByMe: - 'MATCH (this)<-[:MUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1', + 'MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )', }, }), }, diff --git a/backend/src/schema/resolvers/helpers/Resolver.ts b/backend/src/schema/resolvers/helpers/Resolver.ts index a21893f7d..71d7602a4 100644 --- a/backend/src/schema/resolvers/helpers/Resolver.ts +++ b/backend/src/schema/resolvers/helpers/Resolver.ts @@ -1,6 +1,12 @@ +/* eslint-disable @typescript-eslint/no-dynamic-delete */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable security/detect-object-injection */ -import log from './databaseLogger' - export const undefinedToNullResolver = (list) => { const resolvers = {} list.forEach((key) => { @@ -11,6 +17,7 @@ export const undefinedToNullResolver = (list) => { return resolvers } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export default function Resolver(type, options: any = {}) { const { idAttribute = 'id', @@ -21,8 +28,8 @@ export default function Resolver(type, options: any = {}) { hasMany = {}, } = options - const _hasResolver = (resolvers, { key, connection }, { returnType }) => { - return async (parent, params, { driver, cypherParams }, resolveInfo) => { + const _hasResolver = (_resolvers, { key, connection }, { returnType }) => { + return async (parent, _params, { driver, cypherParams }, _resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const id = parent[idAttribute] const session = driver.session() @@ -32,7 +39,6 @@ export default function Resolver(type, options: any = {}) { RETURN related {.*} as related ` const result = await txc.run(cypher, { id, cypherParams }) - log(result) return result.records.map((r) => r.get('related')) }) try { @@ -45,10 +51,11 @@ export default function Resolver(type, options: any = {}) { } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any const booleanResolver = (obj: any[]) => { const resolvers = {} for (const [key, condition] of Object.entries(obj)) { - resolvers[key] = async (parent, params, { cypherParams, driver }, resolveInfo) => { + resolvers[key] = async (parent, _params, { cypherParams, driver }, _resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const id = parent[idAttribute] const session = driver.session() @@ -56,7 +63,6 @@ export default function Resolver(type, options: any = {}) { const nodeCondition = condition.replace('this', 'this {id: $id}') const cypher = `${nodeCondition} as ${key}` const result = await txc.run(cypher, { id, cypherParams }) - log(result) const [response] = result.records.map((r) => r.get(key)) return response }) @@ -73,7 +79,7 @@ export default function Resolver(type, options: any = {}) { const countResolver = (obj) => { const resolvers = {} for (const [key, connection] of Object.entries(obj)) { - resolvers[key] = async (parent, params, { driver, cypherParams }, resolveInfo) => { + resolvers[key] = async (parent, _params, { driver, cypherParams }, _resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const session = driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { @@ -83,7 +89,6 @@ export default function Resolver(type, options: any = {}) { RETURN COUNT(DISTINCT(related)) as count ` const result = await txc.run(cypher, { id, cypherParams }) - log(result) const [response] = result.records.map((r) => r.get('count').toNumber()) return response }) diff --git a/backend/src/schema/resolvers/helpers/createPasswordReset.ts b/backend/src/schema/resolvers/helpers/createPasswordReset.ts index ec0349c18..0727c5d4e 100644 --- a/backend/src/schema/resolvers/helpers/createPasswordReset.ts +++ b/backend/src/schema/resolvers/helpers/createPasswordReset.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import normalizeEmail from './normalizeEmail' export default async function createPasswordReset(options) { diff --git a/backend/src/schema/resolvers/helpers/databaseLogger.ts b/backend/src/schema/resolvers/helpers/databaseLogger.ts deleted file mode 100644 index f2db22965..000000000 --- a/backend/src/schema/resolvers/helpers/databaseLogger.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable import/no-named-as-default */ -// eslint-disable-next-line import/no-extraneous-dependencies -import Debug from 'debug' - -const debugCypher = Debug('human-connection:neo4j:cypher') -const debugStats = Debug('human-connection:neo4j:stats') - -export default function log(response) { - const { counters, resultConsumedAfter, resultAvailableAfter, query } = response.summary - const { text, parameters } = query - debugCypher('%s', text) - debugCypher('%o', parameters) - debugStats('%o', counters) - debugStats('%o', { - resultConsumedAfter: resultConsumedAfter.toNumber(), - resultAvailableAfter: resultAvailableAfter.toNumber(), - }) -} diff --git a/backend/src/schema/resolvers/helpers/events.ts b/backend/src/schema/resolvers/helpers/events.ts index d4fc1fb11..3e5f8d5a8 100644 --- a/backend/src/schema/resolvers/helpers/events.ts +++ b/backend/src/schema/resolvers/helpers/events.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { UserInputError } from 'apollo-server' export const validateEventParams = (params) => { @@ -18,7 +22,7 @@ export const validateEventParams = (params) => { throw new UserInputError('Event venue must be present if event location is given!') } params.eventVenue = eventInput.eventVenue - params.eventLocationName = eventInput.eventLocationName && eventInput.eventLocationName.trim() + params.eventLocationName = eventInput.eventLocationName?.trim() if (params.eventLocationName) { locationName = params.eventLocationName } else { diff --git a/backend/src/schema/resolvers/helpers/existingEmailAddress.ts b/backend/src/schema/resolvers/helpers/existingEmailAddress.ts index 288a14a6d..e1e27bda0 100644 --- a/backend/src/schema/resolvers/helpers/existingEmailAddress.ts +++ b/backend/src/schema/resolvers/helpers/existingEmailAddress.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ export default async function alreadyExistingMail({ args, context }) { const session = context.driver.session() try { diff --git a/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts b/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts index 5a53bf9cb..88d66dd65 100644 --- a/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts +++ b/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { mergeWith, isArray } from 'lodash' import { getMutedUsers } from '@schema/resolvers/users' diff --git a/backend/src/schema/resolvers/helpers/filterInvisiblePosts.ts b/backend/src/schema/resolvers/helpers/filterInvisiblePosts.ts index 73dfaad91..2a264ced4 100644 --- a/backend/src/schema/resolvers/helpers/filterInvisiblePosts.ts +++ b/backend/src/schema/resolvers/helpers/filterInvisiblePosts.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { mergeWith, isArray } from 'lodash' const getInvisiblePosts = async (context) => { @@ -5,7 +9,7 @@ const getInvisiblePosts = async (context) => { const readTxResultPromise = await session.readTransaction(async (transaction) => { let cypher = '' const { user } = context - if (user && user.id) { + if (user?.id) { cypher = ` MATCH (post:Post)<-[:CANNOT_SEE]-(user:User { id: $userId }) RETURN collect(post.id) AS invisiblePostIds` diff --git a/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.ts b/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.ts index a808a5582..9d40b097e 100644 --- a/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.ts +++ b/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.ts @@ -1,8 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { mergeWith, isArray } from 'lodash' const getMyGroupIds = async (context) => { const { user } = context - if (!(user && user.id)) return [] + if (!user?.id) return [] const session = context.driver.session() const readTxResultPromise = await session.readTransaction(async (transaction) => { @@ -22,7 +26,7 @@ const getMyGroupIds = async (context) => { } export const filterPostsOfMyGroups = async (params, context) => { - if (!(params.filter && params.filter.postsInMyGroups)) return params + if (!params.filter?.postsInMyGroups) return params delete params.filter.postsInMyGroups const myGroupIds = await getMyGroupIds(context) params.filter = mergeWith( diff --git a/backend/src/schema/resolvers/helpers/generateInviteCode.ts b/backend/src/schema/resolvers/helpers/generateInviteCode.ts index 6e580fab9..980af4593 100644 --- a/backend/src/schema/resolvers/helpers/generateInviteCode.ts +++ b/backend/src/schema/resolvers/helpers/generateInviteCode.ts @@ -1,9 +1,9 @@ -import CONSTANTS_REGISTRATION from '@constants/registration' +import registrationConstants from '@constants/registrationBranded' export default function generateInviteCode() { // 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z]) return Array.from( - { length: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH }, + { length: registrationConstants.INVITE_CODE_LENGTH }, (n: number = Math.floor(Math.random() * 36)) => { // n > 9: it is a letter (ASCII 65 is A) -> 10 + 55 = 65 // else: it is a number (ASCII 48 is 0) -> 0 + 48 = 48 diff --git a/backend/src/schema/resolvers/helpers/generateNonce.ts b/backend/src/schema/resolvers/helpers/generateNonce.ts index 7e0f7542c..b7585b24f 100644 --- a/backend/src/schema/resolvers/helpers/generateNonce.ts +++ b/backend/src/schema/resolvers/helpers/generateNonce.ts @@ -1,9 +1,9 @@ -import CONSTANTS_REGISTRATION from '@constants/registration' +import registrationConstants from '@constants/registrationBranded' // TODO: why this is not used in resolver 'requestPasswordReset'? export default function generateNonce() { return Array.from( - { length: CONSTANTS_REGISTRATION.NONCE_LENGTH }, + { length: registrationConstants.NONCE_LENGTH }, (n: number = Math.floor(Math.random() * 10)) => { return String.fromCharCode(n + 48) }, diff --git a/backend/src/schema/resolvers/helpers/normalizeEmail.ts b/backend/src/schema/resolvers/helpers/normalizeEmail.ts index bc13467c3..9b6be73d7 100644 --- a/backend/src/schema/resolvers/helpers/normalizeEmail.ts +++ b/backend/src/schema/resolvers/helpers/normalizeEmail.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { normalizeEmail } from 'validator' export default (email) => diff --git a/backend/src/schema/resolvers/images/images.spec.ts b/backend/src/schema/resolvers/images/images.spec.ts index a4eb5b1a5..938571126 100644 --- a/backend/src/schema/resolvers/images/images.spec.ts +++ b/backend/src/schema/resolvers/images/images.spec.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable promise/prefer-await-to-callbacks */ import { UserInputError } from 'apollo-server' @@ -18,7 +25,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -40,7 +47,7 @@ describe('deleteImage', () => { {}, { avatar: Factory.build('image', { - url: '/some/avatar/url/', + url: 'http://localhost/some/avatar/url/', alt: 'This is the avatar image of a user', }), }, @@ -76,7 +83,7 @@ describe('deleteImage', () => { return result }) } finally { - session.close() + await session.close() } await expect(neode.all('Image')).resolves.toHaveLength(0) await expect(someString).toEqual('Hello') @@ -99,7 +106,7 @@ describe('deleteImage', () => { await expect(neode.all('Image')).resolves.toHaveLength(1) // all good } finally { - session.close() + await session.close() } }) }) @@ -191,9 +198,10 @@ describe('mergeImage', () => { it('connects resource with image via given image type', async () => { await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - const result = await neode.cypher(` - MATCH(p:Post {id: "p99"})-[:HERO_IMAGE]->(i:Image) RETURN i,p - `) + const result = await neode.cypher( + `MATCH(p:Post {id: "p99"})-[:HERO_IMAGE]->(i:Image) RETURN i,p`, + {}, + ) post = neode.hydrateFirst(result, 'p', neode.model('Post')) const image = neode.hydrateFirst(result, 'i', neode.model('Image')) expect(post).toBeTruthy() @@ -208,7 +216,7 @@ describe('mergeImage', () => { it('sets metadata', async () => { await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - const image = await neode.first('Image', {}) + const image = await neode.first('Image', {}, undefined) await expect(image.toJson()).resolves.toMatchObject({ alt: 'A description of the new image', createdAt: expect.any(String), @@ -236,9 +244,13 @@ describe('mergeImage', () => { ) }) } finally { - session.close() + await session.close() } - const image = await neode.first('Image', { alt: 'This alt text gets overwritten' }) + const image = await neode.first( + 'Image', + { alt: 'This alt text gets overwritten' }, + undefined, + ) await expect(image.toJson()).resolves.toMatchObject({ alt: 'This alt text gets overwritten', }) @@ -261,7 +273,7 @@ describe('mergeImage', () => { await expect(neode.all('Image')).resolves.toHaveLength(0) // all good } finally { - session.close() + await session.close() } }) }) @@ -289,7 +301,7 @@ describe('mergeImage', () => { await expect(neode.all('Image')).resolves.toHaveLength(1) await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) await expect(neode.all('Image')).resolves.toHaveLength(1) - const image = await neode.first('Image', {}) + const image = await neode.first('Image', {}, undefined) await expect(image.toJson()).resolves.toMatchObject({ alt: 'A description of the new image', createdAt: expect.any(String), @@ -329,7 +341,7 @@ describe('mergeImage', () => { ), image: Factory.build('image', { alt: 'This is the previous, not updated image', - url: '/some/original/url', + url: 'http://localhost/some/original/url', }), }, ) diff --git a/backend/src/schema/resolvers/images/images.ts b/backend/src/schema/resolvers/images/images.ts index 46eb453c5..2e76a7889 100644 --- a/backend/src/schema/resolvers/images/images.ts +++ b/backend/src/schema/resolvers/images/images.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable promise/avoid-new */ /* eslint-disable security/detect-non-literal-fs-filename */ import { existsSync, unlinkSync, createWriteStream } from 'node:fs' @@ -14,6 +21,7 @@ import { getDriver } from '@db/neo4j' // const widths = [34, 160, 320, 640, 1024] const { AWS_ENDPOINT: endpoint, AWS_REGION: region, AWS_BUCKET: Bucket, S3_CONFIGURED } = CONFIG +// eslint-disable-next-line @typescript-eslint/no-explicit-any export async function deleteImage(resource, relationshipType, opts: any = {}) { sanitizeRelationshipType(relationshipType) const { transaction, deleteCallback } = opts @@ -36,6 +44,7 @@ export async function deleteImage(resource, relationshipType, opts: any = {}) { return image } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export async function mergeImage(resource, relationshipType, imageInput, opts: any = {}) { if (typeof imageInput === 'undefined') return if (imageInput === null) return deleteImage(resource, relationshipType, opts) @@ -82,7 +91,7 @@ const wrapTransaction = async (wrappedCallback, args, opts) => { }) return result } finally { - session.close() + await session.close() } } @@ -143,6 +152,7 @@ const s3Upload = async ({ createReadStream, uniqueFilename, mimetype }) => { const localFileDelete = async (url) => { const location = `public${url}` + // eslint-disable-next-line n/no-sync if (existsSync(location)) unlinkSync(location) } diff --git a/backend/src/schema/resolvers/inviteCodes.spec.ts b/backend/src/schema/resolvers/inviteCodes.spec.ts index e1a0dac17..f44721cc9 100644 --- a/backend/src/schema/resolvers/inviteCodes.spec.ts +++ b/backend/src/schema/resolvers/inviteCodes.spec.ts @@ -1,8 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable security/detect-non-literal-regexp */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' -import CONSTANTS_REGISTRATION from '@constants/registration' +import registrationConstants from '@constants/registrationBranded' import Factory, { cleanDatabase } from '@db/factories' import { getDriver } from '@db/neo4j' import createServer from '@src/server' @@ -54,7 +57,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('inviteCodes', () => { @@ -113,7 +116,7 @@ describe('inviteCodes', () => { GenerateInviteCode: { code: expect.stringMatching( new RegExp( - `^[0-9A-Z]{${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH},${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH}}$`, + `^[0-9A-Z]{${registrationConstants.INVITE_CODE_LENGTH},${registrationConstants.INVITE_CODE_LENGTH}}$`, ), ), expiresAt: null, @@ -139,7 +142,7 @@ describe('inviteCodes', () => { GenerateInviteCode: { code: expect.stringMatching( new RegExp( - `^[0-9A-Z]{${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH},${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH}}$`, + `^[0-9A-Z]{${registrationConstants.INVITE_CODE_LENGTH},${registrationConstants.INVITE_CODE_LENGTH}}$`, ), ), expiresAt: nextWeek.toISOString(), diff --git a/backend/src/schema/resolvers/inviteCodes.ts b/backend/src/schema/resolvers/inviteCodes.ts index 442ff17b1..02680b5bc 100644 --- a/backend/src/schema/resolvers/inviteCodes.ts +++ b/backend/src/schema/resolvers/inviteCodes.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import generateInviteCode from './helpers/generateInviteCode' import Resolver from './helpers/Resolver' import { validateInviteCode } from './transactions/inviteCodes' diff --git a/backend/src/schema/resolvers/locations.spec.ts b/backend/src/schema/resolvers/locations.spec.ts index 824372d28..aed85da54 100644 --- a/backend/src/schema/resolvers/locations.spec.ts +++ b/backend/src/schema/resolvers/locations.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -27,7 +30,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 diff --git a/backend/src/schema/resolvers/locations.ts b/backend/src/schema/resolvers/locations.ts index fcc2fa0aa..bcefa2337 100644 --- a/backend/src/schema/resolvers/locations.ts +++ b/backend/src/schema/resolvers/locations.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ import { UserInputError } from 'apollo-server' import Resolver from './helpers/Resolver' @@ -20,7 +22,7 @@ export default { }), }, Query: { - queryLocations: async (object, args, context, resolveInfo) => { + queryLocations: async (_object, args, _context, _resolveInfo) => { try { return queryLocations(args) } catch (e) { diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index 4384ddd0f..8061cf460 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -1,21 +1,29 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { createMessageMutation, messageQuery, markMessagesAsSeen } from '@graphql/messages' -import { createRoomMutation, roomQuery } from '@graphql/rooms' +import { createMessageMutation } from '@graphql/queries/createMessageMutation' +import { createRoomMutation } from '@graphql/queries/createRoomMutation' +import { markMessagesAsSeen } from '@graphql/queries/markMessagesAsSeen' +import { messageQuery } from '@graphql/queries/messageQuery' +import { roomQuery } from '@graphql/queries/roomQuery' import createServer, { pubsub } from '@src/server' const driver = getDriver() const neode = getNeode() -const pubsubSpy = jest.spyOn(pubsub, 'publish') - let query let mutate let authenticatedUser let chattingUser, otherChattingUser, notChattingUser +const pubsubSpy = jest.spyOn(pubsub, 'publish') + beforeAll(async () => { await cleanDatabase() @@ -37,7 +45,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('Message', () => { @@ -118,7 +126,7 @@ describe('Message', () => { }) describe('user chats in room', () => { - it('returns the message and publishes subscriptions', async () => { + it('returns the message', async () => { await expect( mutate({ mutation: createMessageMutation(), @@ -143,24 +151,6 @@ describe('Message', () => { }, }, }) - expect(pubsubSpy).toBeCalledWith('ROOM_COUNT_UPDATED', { - roomCountUpdated: '1', - userId: 'other-chatting-user', - }) - expect(pubsubSpy).toBeCalledWith('CHAT_MESSAGE_ADDED', { - chatMessageAdded: expect.objectContaining({ - id: expect.any(String), - content: 'Some nice message to other chatting user', - senderId: 'chatting-user', - username: 'Chatting User', - avatar: expect.any(String), - date: expect.any(String), - saved: true, - distributed: false, - seen: false, - }), - userId: 'other-chatting-user', - }) }) describe('room is updated as well', () => { diff --git a/backend/src/schema/resolvers/messages.ts b/backend/src/schema/resolvers/messages.ts index 6879c4be9..c3f362660 100644 --- a/backend/src/schema/resolvers/messages.ts +++ b/backend/src/schema/resolvers/messages.ts @@ -1,10 +1,15 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { withFilter } from 'graphql-subscriptions' import { neo4jgraphql } from 'neo4j-graphql-js' -import { pubsub, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } from '@src/server' +import { pubsub, CHAT_MESSAGE_ADDED } from '@src/server' import Resolver from './helpers/Resolver' -import { getUnreadRoomsCount } from './rooms' const setMessagesAsDistributed = async (undistributedMessagesIds, session) => { return session.writeTransaction(async (transaction) => { @@ -111,22 +116,7 @@ export default { return message }) try { - const message = await writeTxResultPromise - if (message) { - const roomCountUpdated = await getUnreadRoomsCount(message.recipientId, session) - - // send subscriptions - void pubsub.publish(ROOM_COUNT_UPDATED, { - roomCountUpdated, - userId: message.recipientId, - }) - void pubsub.publish(CHAT_MESSAGE_ADDED, { - chatMessageAdded: message, - userId: message.recipientId, - }) - } - - return message + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { diff --git a/backend/src/schema/resolvers/moderation.spec.ts b/backend/src/schema/resolvers/moderation.spec.ts index 46befdf10..f3224421e 100644 --- a/backend/src/schema/resolvers/moderation.spec.ts +++ b/backend/src/schema/resolvers/moderation.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -71,7 +75,7 @@ describe('moderate resources', () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -190,7 +194,7 @@ describe('moderate resources', () => { ]) const cypher = 'MATCH (:Report)<-[review:REVIEWED]-(moderator:User {id: "moderator-id"}) RETURN review' - const reviews = await neode.cypher(cypher) + const reviews = await neode.cypher(cypher, {}) expect(reviews.records).toHaveLength(1) }) diff --git a/backend/src/schema/resolvers/moderation.ts b/backend/src/schema/resolvers/moderation.ts index a29a411aa..bcdb3992a 100644 --- a/backend/src/schema/resolvers/moderation.ts +++ b/backend/src/schema/resolvers/moderation.ts @@ -1,5 +1,7 @@ -import log from './helpers/databaseLogger' - +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export default { Mutation: { review: async (_object, params, context, _resolveInfo) => { @@ -27,7 +29,6 @@ export default { moderatorId: moderator.id, dateTime: new Date().toISOString(), }) - log(reviewTransactionResponse) return reviewTransactionResponse.records.map((record) => record.get('review')) }) const [reviewed] = await reviewWriteTxResultPromise diff --git a/backend/src/schema/resolvers/notifications.spec.ts b/backend/src/schema/resolvers/notifications.spec.ts index a10f97590..d6d22e459 100644 --- a/backend/src/schema/resolvers/notifications.spec.ts +++ b/backend/src/schema/resolvers/notifications.spec.ts @@ -1,13 +1,16 @@ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import Factory, { cleanDatabase } from '@db/factories' import { getDriver } from '@db/neo4j' -import { - markAsReadMutation, - markAllAsReadMutation, - notificationQuery, -} from '@graphql/notifications' +import { markAllAsReadMutation } from '@graphql/queries/markAllAsReadMutation' +import { markAsReadMutation } from '@graphql/queries/markAsReadMutation' +import { notificationQuery } from '@graphql/queries/notificationQuery' import createServer from '@src/server' const driver = getDriver() @@ -35,7 +38,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/schema/resolvers/notifications.ts b/backend/src/schema/resolvers/notifications.ts index 5dbbe3d40..0168558c3 100644 --- a/backend/src/schema/resolvers/notifications.ts +++ b/backend/src/schema/resolvers/notifications.ts @@ -1,9 +1,13 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { withFilter } from 'graphql-subscriptions' import { pubsub, NOTIFICATION_ADDED } from '@src/server' -import log from './helpers/databaseLogger' - export default { Subscription: { notificationAdded: { @@ -70,7 +74,6 @@ export default { `, { id: currentUser.id }, ) - log(notificationsTransactionResponse) return notificationsTransactionResponse.records.map((record) => record.get('notification')) }) try { @@ -82,7 +85,7 @@ export default { }, }, Mutation: { - markAsRead: async (parent, args, context, resolveInfo) => { + markAsRead: async (_parent, args, context, _resolveInfo) => { const { user: currentUser } = context const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -100,7 +103,6 @@ export default { `, { resourceId: args.id, id: currentUser.id }, ) - log(markNotificationAsReadTransactionResponse) return markNotificationAsReadTransactionResponse.records.map((record) => record.get('notification'), ) @@ -112,7 +114,7 @@ export default { session.close() } }, - markAllAsRead: async (parent, args, context, resolveInfo) => { + markAllAsRead: async (parent, args, context, _resolveInfo) => { const { user: currentUser } = context const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -130,7 +132,6 @@ export default { `, { id: currentUser.id }, ) - log(markAllNotificationAsReadTransactionResponse) return markAllNotificationAsReadTransactionResponse.records.map((record) => record.get('notification'), ) diff --git a/backend/src/schema/resolvers/observePosts.spec.ts b/backend/src/schema/resolvers/observePosts.spec.ts index 13fd5ccfc..76ad5b058 100644 --- a/backend/src/schema/resolvers/observePosts.spec.ts +++ b/backend/src/schema/resolvers/observePosts.spec.ts @@ -1,10 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { createPostMutation } from '@graphql/posts' +import { createPostMutation } from '@graphql/queries/createPostMutation' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false @@ -58,7 +61,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('observing posts', () => { diff --git a/backend/src/schema/resolvers/passwordReset.spec.ts b/backend/src/schema/resolvers/passwordReset.spec.ts index b5c7e10dd..d5d08265c 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.ts +++ b/backend/src/schema/resolvers/passwordReset.spec.ts @@ -1,7 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' -import CONSTANTS_REGISTRATION from '@constants/registration' +import registrationConstants from '@constants/registrationBranded' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' import createServer from '@src/server' @@ -18,6 +22,7 @@ let variables const getAllPasswordResets = async () => { const passwordResetQuery = await neode.cypher( 'MATCH (passwordReset:PasswordReset) RETURN passwordReset', + {}, ) const resets = passwordResetQuery.records.map((record) => record.get('passwordReset')) return resets @@ -40,7 +45,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(() => { @@ -113,7 +118,7 @@ describe('passwordReset', () => { const resets = await getAllPasswordResets() const [reset] = resets const { nonce } = reset.properties - expect(nonce).toHaveLength(CONSTANTS_REGISTRATION.NONCE_LENGTH) + expect(nonce).toHaveLength(registrationConstants.NONCE_LENGTH) }) }) }) @@ -121,6 +126,7 @@ describe('passwordReset', () => { }) describe('resetPassword', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const setup = async (options: any = {}) => { const { email = 'user@example.org', issuedAt = new Date(), nonce = '12345' } = options await createPasswordReset({ driver, email, issuedAt, nonce }) diff --git a/backend/src/schema/resolvers/passwordReset.ts b/backend/src/schema/resolvers/passwordReset.ts index b9d4d7f51..f806f7249 100644 --- a/backend/src/schema/resolvers/passwordReset.ts +++ b/backend/src/schema/resolvers/passwordReset.ts @@ -1,7 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import bcrypt from 'bcryptjs' import { v4 as uuid } from 'uuid' -import CONSTANTS_REGISTRATION from '@constants/registration' +import registrationConstants from '@constants/registrationBranded' import createPasswordReset from './helpers/createPasswordReset' @@ -9,13 +15,13 @@ export default { Mutation: { requestPasswordReset: async (_parent, { email }, { driver }) => { // TODO: why this is generated differntly from 'backend/src/schema/resolvers/helpers/generateNonce.js'? - const nonce = uuid().substring(0, CONSTANTS_REGISTRATION.NONCE_LENGTH) + const nonce = uuid().substring(0, registrationConstants.NONCE_LENGTH) return createPasswordReset({ driver, nonce, email }) }, resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => { const stillValid = new Date() stillValid.setDate(stillValid.getDate() - 1) - const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10) + const encryptedNewPassword = await bcrypt.hash(newPassword, 10) const session = driver.session() try { const passwordResetTxPromise = session.writeTransaction(async (transaction) => { @@ -41,7 +47,7 @@ export default { ) }) const [reset] = await passwordResetTxPromise - return !!(reset && reset.properties.usedAt) + return !!reset?.properties.usedAt } finally { session.close() } diff --git a/backend/src/schema/resolvers/posts.spec.ts b/backend/src/schema/resolvers/posts.spec.ts index 103ba98c0..0a05200fd 100644 --- a/backend/src/schema/resolvers/posts.spec.ts +++ b/backend/src/schema/resolvers/posts.spec.ts @@ -1,10 +1,16 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { createPostMutation } from '@graphql/posts' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import Image from '@models/Image' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = true @@ -41,7 +47,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -969,9 +975,13 @@ describe('UpdatePost', () => { variables = { ...variables, image: { sensitive: true } } }) it('updates the image', async () => { - await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy() + await expect( + neode.first('Image', { sensitive: true }, undefined), + ).resolves.toBeFalsy() await mutate({ mutation: updatePostMutation, variables }) - await expect(neode.first('Image', { sensitive: true })).resolves.toBeTruthy() + await expect( + neode.first('Image', { sensitive: true }, undefined), + ).resolves.toBeTruthy() }) }) @@ -991,9 +1001,13 @@ describe('UpdatePost', () => { delete variables.image }) it('keeps the image unchanged', async () => { - await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy() + await expect( + neode.first('Image', { sensitive: true }, undefined), + ).resolves.toBeFalsy() await mutate({ mutation: updatePostMutation, variables }) - await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy() + await expect( + neode.first('Image', { sensitive: true }, undefined), + ).resolves.toBeFalsy() }) }) }) @@ -1239,11 +1253,11 @@ describe('pin posts', () => { it('removes previous `pinned` attribute', async () => { const cypher = 'MATCH (post:Post) WHERE post.pinned IS NOT NULL RETURN post' - pinnedPost = await neode.cypher(cypher) + pinnedPost = await neode.cypher(cypher, {}) expect(pinnedPost.records).toHaveLength(1) variables = { ...variables, id: 'only-pinned-post' } await mutate({ mutation: pinPostMutation, variables }) - pinnedPost = await neode.cypher(cypher) + pinnedPost = await neode.cypher(cypher, {}) expect(pinnedPost.records).toHaveLength(1) }) @@ -1252,6 +1266,7 @@ describe('pin posts', () => { await mutate({ mutation: pinPostMutation, variables }) pinnedPost = await neode.cypher( `MATCH (:User)-[pinned:PINNED]->(post:Post) RETURN post, pinned`, + {}, ) expect(pinnedPost.records).toHaveLength(1) }) @@ -1463,7 +1478,7 @@ describe('DeletePost', () => { }, { image: Factory.build('image', { - url: 'path/to/some/image', + url: 'http://localhost/path/to/some/image', }), author, categoryIds, diff --git a/backend/src/schema/resolvers/posts.ts b/backend/src/schema/resolvers/posts.ts index cb48d78ea..f981662ba 100644 --- a/backend/src/schema/resolvers/posts.ts +++ b/backend/src/schema/resolvers/posts.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' import { isEmpty } from 'lodash' import { neo4jgraphql } from 'neo4j-graphql-js' @@ -48,7 +54,7 @@ export default { params = await filterForMutedUsers(params, context) return neo4jgraphql(object, params, context, resolveInfo) }, - PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { + PostsEmotionsCountByEmotion: async (_object, params, context, _resolveInfo) => { const { postId, data } = params const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (transaction) => { @@ -70,7 +76,7 @@ export default { session.close() } }, - PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => { + PostsEmotionsByCurrentUser: async (_object, params, context, _resolveInfo) => { const { postId } = params const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (transaction) => { @@ -242,7 +248,7 @@ export default { } }, - DeletePost: async (object, args, context, resolveInfo) => { + DeletePost: async (_object, args, context, _resolveInfo) => { const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { const deletePostTransactionResponse = await transaction.run( @@ -269,7 +275,7 @@ export default { session.close() } }, - AddPostEmotions: async (object, params, context, resolveInfo) => { + AddPostEmotions: async (_object, params, context, _resolveInfo) => { const { to, data } = params const { user } = context const session = context.driver.session() @@ -296,7 +302,7 @@ export default { session.close() } }, - RemovePostEmotions: async (object, params, context, resolveInfo) => { + RemovePostEmotions: async (_object, params, context, _resolveInfo) => { const { to, data } = params const { id: from } = context.user const session = context.driver.session() @@ -499,7 +505,7 @@ export default { 'MATCH (this)<-[obs:OBSERVES]-(related:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(related) >= 1', }, }), - relatedContributions: async (parent, params, context, resolveInfo) => { + relatedContributions: async (parent, _params, context, _resolveInfo) => { if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions const { id } = parent const session = context.driver.session() diff --git a/backend/src/schema/resolvers/postsInGroups.spec.ts b/backend/src/schema/resolvers/postsInGroups.spec.ts index 17d4f274e..7cb0bdc76 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.ts +++ b/backend/src/schema/resolvers/postsInGroups.spec.ts @@ -1,22 +1,22 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { signupVerificationMutation } from '@graphql/authentications' -import { createCommentMutation } from '@graphql/comments' -import { - createGroupMutation, - changeGroupMemberRoleMutation, - leaveGroupMutation, -} from '@graphql/groups' -import { - createPostMutation, - postQuery, - filterPosts, - profilePagePosts, - searchPosts, -} from '@graphql/posts' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createCommentMutation } from '@graphql/queries/createCommentMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { filterPosts } from '@graphql/queries/filterPosts' +import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation' +import { postQuery } from '@graphql/queries/postQuery' +import { profilePagePosts } from '@graphql/queries/profilePagePosts' +import { searchPosts } from '@graphql/queries/searchPosts' +import { signupVerificationMutation } from '@graphql/queries/signupVerificationMutation' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false @@ -63,7 +63,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('Posts in Groups', () => { diff --git a/backend/src/schema/resolvers/registration.spec.ts b/backend/src/schema/resolvers/registration.spec.ts index e61460786..f19c6bf01 100644 --- a/backend/src/schema/resolvers/registration.spec.ts +++ b/backend/src/schema/resolvers/registration.spec.ts @@ -1,9 +1,15 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getDriver, getNeode } from '@db/neo4j' +import EmailAddress from '@models/EmailAddress' +import User from '@models/User' import createServer from '@src/server' const neode = getNeode() @@ -30,7 +36,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -92,17 +98,27 @@ describe('Signup', () => { describe('creates a EmailAddress node', () => { it('with `createdAt` attribute', async () => { await mutate({ mutation, variables }) - let emailAddress = await neode.first('EmailAddress', { email: 'someuser@example.org' }) - emailAddress = await emailAddress.toJson() - expect(emailAddress.createdAt).toBeTruthy() - expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number)) + const emailAddress = await neode.first( + 'EmailAddress', + { email: 'someuser@example.org' }, + undefined, + ) + const emailAddressJson = await emailAddress.toJson() + expect(emailAddressJson.createdAt).toBeTruthy() + expect(Date.parse(emailAddressJson.createdAt as unknown as string)).toEqual( + expect.any(Number), + ) }) it('with a cryptographic `nonce`', async () => { await mutate({ mutation, variables }) - let emailAddress = await neode.first('EmailAddress', { email: 'someuser@example.org' }) - emailAddress = await emailAddress.toJson() - expect(emailAddress.nonce).toEqual(expect.any(String)) + const emailAddress = await neode.first( + 'EmailAddress', + { email: 'someuser@example.org' }, + undefined, + ) + const emailAddressJson = await emailAddress.toJson() + expect(emailAddressJson.nonce).toEqual(expect.any(String)) }) describe('if the email already exists', () => { @@ -242,7 +258,11 @@ describe('SignupVerification', () => { it('sets `verifiedAt` attribute of EmailAddress', async () => { await mutate({ mutation, variables }) - const email = await neode.first('EmailAddress', { email: 'john@example.org' }) + const email = await neode.first( + 'EmailAddress', + { email: 'john@example.org' }, + undefined, + ) await expect(email.toJson()).resolves.toEqual( expect.objectContaining({ verifiedAt: expect.any(String), @@ -263,7 +283,7 @@ describe('SignupVerification', () => { it('sets `about` attribute of User', async () => { variables = { ...variables, about: 'Find this description in the user profile' } await mutate({ mutation, variables }) - const user = await neode.first('User', { name: 'John Doe' }) + const user = await neode.first('User', { name: 'John Doe' }, undefined) await expect(user.toJson()).resolves.toMatchObject({ about: 'Find this description in the user profile', }) diff --git a/backend/src/schema/resolvers/registration.ts b/backend/src/schema/resolvers/registration.ts index fc3fc37bb..138a21aea 100644 --- a/backend/src/schema/resolvers/registration.ts +++ b/backend/src/schema/resolvers/registration.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' import { getNeode } from '@db/neo4j' diff --git a/backend/src/schema/resolvers/reports.spec.ts b/backend/src/schema/resolvers/reports.spec.ts index a57efc011..bcbe1df4e 100644 --- a/backend/src/schema/resolvers/reports.spec.ts +++ b/backend/src/schema/resolvers/reports.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -118,7 +122,7 @@ describe('file a report on a resource', () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 diff --git a/backend/src/schema/resolvers/reports.ts b/backend/src/schema/resolvers/reports.ts index f7945e060..b8886c48f 100644 --- a/backend/src/schema/resolvers/reports.ts +++ b/backend/src/schema/resolvers/reports.ts @@ -1,5 +1,8 @@ -import log from './helpers/databaseLogger' - +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export default { Mutation: { fileReport: async (_parent, params, context, _resolveInfo) => { @@ -28,7 +31,6 @@ export default { reasonDescription, }, ) - log(fileReportTransactionResponse) return fileReportTransactionResponse.records.map((record) => record.get('filedReport')) }) try { @@ -101,7 +103,6 @@ export default { ${offset} ${limit} `, ) - log(reportsTransactionResponse) return reportsTransactionResponse.records.map((record) => record.get('report')) }) try { @@ -126,7 +127,6 @@ export default { `, { id }, ) - log(filedReportsTransactionResponse) return filedReportsTransactionResponse.records.map((record) => ({ submitter: record.get('submitter').properties, filed: record.get('filed').properties, @@ -161,7 +161,6 @@ export default { `, { id }, ) - log(reviewedReportsTransactionResponse) return reviewedReportsTransactionResponse.records.map((record) => ({ review: record.get('review').properties, moderator: record.get('moderator').properties, diff --git a/backend/src/schema/resolvers/rewards.spec.ts b/backend/src/schema/resolvers/rewards.spec.ts deleted file mode 100644 index 2cfe122a0..000000000 --- a/backend/src/schema/resolvers/rewards.spec.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { createTestClient } from 'apollo-server-testing' -import gql from 'graphql-tag' - -import Factory, { cleanDatabase } from '@db/factories' -import { getNeode, getDriver } from '@db/neo4j' -import createServer from '@src/server' - -const driver = getDriver() -const instance = getNeode() - -let authenticatedUser, regularUser, administrator, moderator, badge, query, mutate - -describe('rewards', () => { - const variables = { - from: 'indiegogo_en_rhino', - to: 'regular-user-id', - } - - beforeAll(async () => { - await cleanDatabase() - - const { server } = createServer({ - context: () => { - return { - driver, - neode: instance, - user: authenticatedUser, - } - }, - }) - query = createTestClient(server).query - mutate = createTestClient(server).mutate - }) - - afterAll(async () => { - await cleanDatabase() - driver.close() - }) - - beforeEach(async () => { - regularUser = await Factory.build( - 'user', - { - id: 'regular-user-id', - role: 'user', - }, - { - email: 'user@example.org', - password: '1234', - }, - ) - moderator = await Factory.build( - 'user', - { - id: 'moderator-id', - role: 'moderator', - }, - { - email: 'moderator@example.org', - }, - ) - administrator = await Factory.build( - 'user', - { - id: 'admin-id', - role: 'admin', - }, - { - email: 'admin@example.org', - }, - ) - badge = await Factory.build('badge', { - id: 'indiegogo_en_rhino', - type: 'crowdfunding', - status: 'permanent', - icon: '/img/badges/indiegogo_en_rhino.svg', - }) - }) - - // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 - afterEach(async () => { - await cleanDatabase() - }) - - describe('reward', () => { - const rewardMutation = gql` - mutation ($from: ID!, $to: ID!) { - reward(badgeKey: $from, userId: $to) { - id - badges { - id - } - } - } - ` - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - authenticatedUser = null - await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject({ - data: { reward: null }, - errors: [{ message: 'Not Authorized!' }], - }) - }) - }) - - describe('authenticated admin', () => { - beforeEach(async () => { - authenticatedUser = await administrator.toJson() - }) - - describe('badge for id does not exist', () => { - it('rejects with an informative error message', async () => { - await expect( - mutate({ - mutation: rewardMutation, - variables: { to: 'regular-user-id', from: 'non-existent-badge-id' }, - }), - ).resolves.toMatchObject({ - data: { reward: null }, - errors: [{ message: "Couldn't find a badge with that id" }], - }) - }) - }) - - describe('non-existent user', () => { - it('rejects with a telling error message', async () => { - await expect( - mutate({ - mutation: rewardMutation, - variables: { to: 'non-existent-user-id', from: 'indiegogo_en_rhino' }, - }), - ).resolves.toMatchObject({ - data: { reward: null }, - errors: [{ message: "Couldn't find a user with that id" }], - }) - }) - }) - - it('rewards a badge to user', async () => { - const expected = { - data: { - reward: { - id: 'regular-user-id', - badges: [{ id: 'indiegogo_en_rhino' }], - }, - }, - errors: undefined, - } - await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - - it('rewards a second different badge to same user', async () => { - await Factory.build('badge', { - id: 'indiegogo_en_racoon', - icon: '/img/badges/indiegogo_en_racoon.svg', - }) - const badges = [{ id: 'indiegogo_en_racoon' }, { id: 'indiegogo_en_rhino' }] - const expected = { - data: { - reward: { - id: 'regular-user-id', - badges: expect.arrayContaining(badges), - }, - }, - errors: undefined, - } - await mutate({ - mutation: rewardMutation, - variables: { - to: 'regular-user-id', - from: 'indiegogo_en_rhino', - }, - }) - await expect( - mutate({ - mutation: rewardMutation, - variables: { - to: 'regular-user-id', - from: 'indiegogo_en_racoon', - }, - }), - ).resolves.toMatchObject(expected) - }) - - it('rewards the same badge as well to another user', async () => { - const expected = { - data: { - reward: { - id: 'regular-user-2-id', - badges: [{ id: 'indiegogo_en_rhino' }], - }, - }, - errors: undefined, - } - await Factory.build( - 'user', - { - id: 'regular-user-2-id', - }, - { - email: 'regular2@email.com', - }, - ) - await mutate({ - mutation: rewardMutation, - variables, - }) - await expect( - mutate({ - mutation: rewardMutation, - variables: { - to: 'regular-user-2-id', - from: 'indiegogo_en_rhino', - }, - }), - ).resolves.toMatchObject(expected) - }) - - it('creates no duplicate reward relationships', async () => { - await mutate({ - mutation: rewardMutation, - variables, - }) - await mutate({ - mutation: rewardMutation, - variables, - }) - - const userQuery = gql` - { - User(id: "regular-user-id") { - badgesCount - badges { - id - } - } - } - ` - const expected = { - data: { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] }, - errors: undefined, - } - - await expect(query({ query: userQuery })).resolves.toMatchObject(expected) - }) - }) - - describe('authenticated moderator', () => { - beforeEach(async () => { - authenticatedUser = moderator.toJson() - }) - - describe('rewards badge to user', () => { - it('throws authorization error', async () => { - await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject({ - data: { reward: null }, - errors: [{ message: 'Not Authorized!' }], - }) - }) - }) - }) - }) - - describe('unreward', () => { - beforeEach(async () => { - await regularUser.relateTo(badge, 'rewarded') - }) - const expected = { - data: { unreward: { id: 'regular-user-id', badges: [] } }, - errors: undefined, - } - - const unrewardMutation = gql` - mutation ($from: ID!, $to: ID!) { - unreward(badgeKey: $from, userId: $to) { - id - badges { - id - } - } - } - ` - - describe('check test setup', () => { - it('user has one badge', async () => { - authenticatedUser = regularUser.toJson() - const userQuery = gql` - { - User(id: "regular-user-id") { - badgesCount - badges { - id - } - } - } - ` - const expected = { - data: { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] }, - errors: undefined, - } - await expect(query({ query: userQuery })).resolves.toMatchObject(expected) - }) - }) - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - authenticatedUser = null - await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject({ - data: { unreward: null }, - errors: [{ message: 'Not Authorized!' }], - }) - }) - }) - - describe('authenticated admin', () => { - beforeEach(async () => { - authenticatedUser = await administrator.toJson() - }) - - it('removes a badge from user', async () => { - await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - - it('does not crash when unrewarding multiple times', async () => { - await mutate({ mutation: unrewardMutation, variables }) - await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - }) - - describe('authenticated moderator', () => { - beforeEach(async () => { - authenticatedUser = await moderator.toJson() - }) - - describe('removes bage from user', () => { - it('throws authorization error', async () => { - await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject({ - data: { unreward: null }, - errors: [{ message: 'Not Authorized!' }], - }) - }) - }) - }) - }) -}) diff --git a/backend/src/schema/resolvers/rewards.ts b/backend/src/schema/resolvers/rewards.ts deleted file mode 100644 index bbb889c41..000000000 --- a/backend/src/schema/resolvers/rewards.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { UserInputError } from 'apollo-server' - -import { getNeode } from '@db/neo4j' - -const neode = getNeode() - -const getUserAndBadge = async ({ badgeKey, userId }) => { - const user = await neode.first('User', 'id', userId) - const badge = await neode.first('Badge', 'id', badgeKey) - if (!user) throw new UserInputError("Couldn't find a user with that id") - if (!badge) throw new UserInputError("Couldn't find a badge with that id") - return { user, badge } -} - -export default { - Mutation: { - reward: async (_object, params, context, _resolveInfo) => { - const { user, badge } = await getUserAndBadge(params) - await user.relateTo(badge, 'rewarded') - return user.toJson() - }, - - unreward: async (_object, params, context, _resolveInfo) => { - const { badgeKey, userId } = params - const { user } = await getUserAndBadge(params) - const session = context.driver.session() - try { - await session.writeTransaction((transaction) => { - return transaction.run( - ` - MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId}) - DELETE reward - RETURN rewardedUser - `, - { - badgeKey, - userId, - }, - ) - }) - } finally { - session.close() - } - return user.toJson() - }, - }, -} diff --git a/backend/src/schema/resolvers/roles.ts b/backend/src/schema/resolvers/roles.ts index be9861e08..006d4f5ba 100644 --- a/backend/src/schema/resolvers/roles.ts +++ b/backend/src/schema/resolvers/roles.ts @@ -1,6 +1,7 @@ +/* eslint-disable @typescript-eslint/require-await */ export default { Query: { - availableRoles: async (_parent, args, context, _resolveInfo) => { + availableRoles: async (_parent, _args, _context, _resolveInfo) => { return ['admin', 'moderator', 'user'] }, }, diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index 87ebb4557..9a226a2f8 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -1,9 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { createMessageMutation } from '@graphql/messages' -import { createRoomMutation, roomQuery, unreadRoomsQuery } from '@graphql/rooms' +import { createMessageMutation } from '@graphql/queries/createMessageMutation' +import { createRoomMutation } from '@graphql/queries/createRoomMutation' +import { roomQuery } from '@graphql/queries/roomQuery' +import { unreadRoomsQuery } from '@graphql/queries/unreadRoomsQuery' import createServer from '@src/server' const driver = getDriver() @@ -35,7 +40,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('Room', () => { @@ -387,6 +392,34 @@ describe('Room', () => { }, }) }) + + it('when chattingUser is blocked has 0 unread rooms', async () => { + authenticatedUser = await otherChattingUser.toJson() + await otherChattingUser.relateTo(chattingUser, 'blocked') + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 0, + }, + }) + }) + + it('when chattingUser is muted has 0 unread rooms', async () => { + authenticatedUser = await otherChattingUser.toJson() + await otherChattingUser.relateTo(chattingUser, 'muted') + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 0, + }, + }) + }) }) describe('as not chatting user', () => { @@ -558,6 +591,7 @@ describe('Room', () => { }) describe('query single room', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let result: any = null beforeAll(async () => { diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index 0ff37b594..9c6751695 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -1,6 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { withFilter } from 'graphql-subscriptions' import { neo4jgraphql } from 'neo4j-graphql-js' +// eslint-disable-next-line import/no-cycle import { pubsub, ROOM_COUNT_UPDATED } from '@src/server' import Resolver from './helpers/Resolver' @@ -8,8 +15,10 @@ import Resolver from './helpers/Resolver' export const getUnreadRoomsCount = async (userId, session) => { return session.readTransaction(async (transaction) => { const unreadRoomsCypher = ` - MATCH (:User { id: $userId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User) + MATCH (user:User { id: $userId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User) WHERE NOT sender.id = $userId AND NOT message.seen + AND NOT (user)-[:BLOCKED]->(sender) + AND NOT (user)-[:MUTED]->(sender) RETURN toString(COUNT(DISTINCT room)) AS count ` const unreadRoomsTxResponse = await transaction.run(unreadRoomsCypher, { userId }) @@ -36,7 +45,7 @@ export default { } return neo4jgraphql(object, params, context, resolveInfo) }, - UnreadRooms: async (object, params, context, resolveInfo) => { + UnreadRooms: async (_object, _params, context, _resolveInfo) => { const { user: { id: currentUserId }, } = context diff --git a/backend/src/schema/resolvers/searches.spec.ts b/backend/src/schema/resolvers/searches.spec.ts index 5902f2746..8a94fbf21 100644 --- a/backend/src/schema/resolvers/searches.spec.ts +++ b/backend/src/schema/resolvers/searches.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -27,7 +30,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() neode.close() }) diff --git a/backend/src/schema/resolvers/searches.ts b/backend/src/schema/resolvers/searches.ts index 5f4097c17..34fc11709 100644 --- a/backend/src/schema/resolvers/searches.ts +++ b/backend/src/schema/resolvers/searches.ts @@ -1,4 +1,9 @@ -import log from './helpers/databaseLogger' +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { queryString } from './searches/queryString' // see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description @@ -127,7 +132,6 @@ const getSearchResults = async (context, setup, params, resultCallback = searchR const session = context.driver.session() try { const results = await searchResultPromise(session, setup, params) - log(results) return resultCallback(results) } finally { session.close() @@ -250,6 +254,7 @@ export default { ] params.limit = 15 + // eslint-disable-next-line @typescript-eslint/no-explicit-any const type: any = multiSearchMap.find((obj) => obj.symbol === searchType) return getSearchResults(context, type.setup, params) }, diff --git a/backend/src/schema/resolvers/searches/queryString.ts b/backend/src/schema/resolvers/searches/queryString.ts index 8f415c5e6..da8e7bffb 100644 --- a/backend/src/schema/resolvers/searches/queryString.ts +++ b/backend/src/schema/resolvers/searches/queryString.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export function queryString(str) { const normalizedString = normalizeWhitespace(str) const escapedString = escapeSpecialCharacters(normalizedString) diff --git a/backend/src/schema/resolvers/shout.spec.ts b/backend/src/schema/resolvers/shout.spec.ts index 7fe7176ab..9023284c6 100644 --- a/backend/src/schema/resolvers/shout.spec.ts +++ b/backend/src/schema/resolvers/shout.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -52,7 +56,7 @@ describe('shout and unshout posts', () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/schema/resolvers/shout.ts b/backend/src/schema/resolvers/shout.ts index 8c330cd67..f0b5885eb 100644 --- a/backend/src/schema/resolvers/shout.ts +++ b/backend/src/schema/resolvers/shout.ts @@ -1,5 +1,7 @@ -import log from './helpers/databaseLogger' - +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export default { Mutation: { shout: async (_object, params, context, _resolveInfo) => { @@ -21,7 +23,6 @@ export default { userId: context.user.id, }, ) - log(shoutTransactionResponse) return shoutTransactionResponse.records.map((record) => record.get('isShouted')) }) const [isShouted] = await shoutWriteTxResultPromise @@ -49,7 +50,6 @@ export default { userId: context.user.id, }, ) - log(unshoutTransactionResponse) return unshoutTransactionResponse.records.map((record) => record.get('isShouted')) }) const [isShouted] = await unshoutWriteTxResultPromise diff --git a/backend/src/schema/resolvers/socialMedia.spec.ts b/backend/src/schema/resolvers/socialMedia.spec.ts index 3a36e791e..168360a3b 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.ts +++ b/backend/src/schema/resolvers/socialMedia.spec.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -13,7 +18,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('SocialMedia', () => { diff --git a/backend/src/schema/resolvers/socialMedia.ts b/backend/src/schema/resolvers/socialMedia.ts index ac27eb1f9..952e4a27e 100644 --- a/backend/src/schema/resolvers/socialMedia.ts +++ b/backend/src/schema/resolvers/socialMedia.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { getNeode } from '@db/neo4j' import Resolver from './helpers/Resolver' @@ -6,7 +11,7 @@ const neode = getNeode() export default { Mutation: { - CreateSocialMedia: async (object, params, context, resolveInfo) => { + CreateSocialMedia: async (_object, params, context, _resolveInfo) => { const [user, socialMedia] = await Promise.all([ neode.find('User', context.user.id), neode.create('SocialMedia', params), @@ -16,14 +21,14 @@ export default { return response }, - UpdateSocialMedia: async (object, params, context, resolveInfo) => { + UpdateSocialMedia: async (_object, params, _context, _resolveInfo) => { const socialMedia = await neode.find('SocialMedia', params.id) await socialMedia.update({ url: params.url }) const response = await socialMedia.toJson() return response }, - DeleteSocialMedia: async (object, { id }, context, resolveInfo) => { + DeleteSocialMedia: async (_object, { id }, _context, _resolveInfo) => { const socialMedia = await neode.find('SocialMedia', id) if (!socialMedia) return null await socialMedia.delete() diff --git a/backend/src/schema/resolvers/statistics.spec.ts b/backend/src/schema/resolvers/statistics.spec.ts index 4c8c8aa01..50f124ac9 100644 --- a/backend/src/schema/resolvers/statistics.spec.ts +++ b/backend/src/schema/resolvers/statistics.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -40,7 +44,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 diff --git a/backend/src/schema/resolvers/statistics.ts b/backend/src/schema/resolvers/statistics.ts index 6bf73b0b2..f7af390bf 100644 --- a/backend/src/schema/resolvers/statistics.ts +++ b/backend/src/schema/resolvers/statistics.ts @@ -1,10 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable security/detect-object-injection */ -import log from './helpers/databaseLogger' - export default { Query: { statistics: async (_parent, _args, { driver }) => { const session = driver.session() + // eslint-disable-next-line @typescript-eslint/no-explicit-any const counts: any = {} try { const mapping = { @@ -23,7 +26,6 @@ export default { RETURN labels, relTypesCount `, ) - log(statisticsTransactionResponse) return statisticsTransactionResponse.records.map((record) => { return { ...record.get('labels'), diff --git a/backend/src/schema/resolvers/transactions/inviteCodes.ts b/backend/src/schema/resolvers/transactions/inviteCodes.ts index 554b15f86..0381893ad 100644 --- a/backend/src/schema/resolvers/transactions/inviteCodes.ts +++ b/backend/src/schema/resolvers/transactions/inviteCodes.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export async function validateInviteCode(session, inviteCode) { const readTxResultPromise = session.readTransaction(async (txc) => { const result = await txc.run( diff --git a/backend/src/schema/resolvers/userData.spec.ts b/backend/src/schema/resolvers/userData.spec.ts index 1165ec33c..17f1f4446 100644 --- a/backend/src/schema/resolvers/userData.spec.ts +++ b/backend/src/schema/resolvers/userData.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -61,7 +64,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('resolvers/userData', () => { diff --git a/backend/src/schema/resolvers/userData.ts b/backend/src/schema/resolvers/userData.ts index 3cd5f1c01..15c65b59b 100644 --- a/backend/src/schema/resolvers/userData.ts +++ b/backend/src/schema/resolvers/userData.ts @@ -1,6 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export default { Query: { - userData: async (object, args, context, resolveInfo) => { + userData: async (_object, _args, context, _resolveInfo) => { const id = context.user.id const cypher = ` MATCH (user:User { id: $id }) diff --git a/backend/src/schema/resolvers/user_management.spec.ts b/backend/src/schema/resolvers/user_management.spec.ts index 527821856..1029ab2b1 100644 --- a/backend/src/schema/resolvers/user_management.spec.ts +++ b/backend/src/schema/resolvers/user_management.spec.ts @@ -1,4 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable promise/prefer-await-to-callbacks */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable jest/unbound-method */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import jwt from 'jsonwebtoken' @@ -7,7 +14,7 @@ import CONFIG from '@config/index' import { categories } from '@constants/categories' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' -import { loginMutation } from '@graphql/userManagement' +import { loginMutation } from '@graphql/queries/loginMutation' import encode from '@jwt/encode' import createServer, { context } from '@src/server' @@ -51,7 +58,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(() => { @@ -155,8 +162,16 @@ describe('currentUser', () => { await respondsWith({ data: { currentUser: expect.objectContaining({ - activeCategories: [ + activeCategories: expect.arrayContaining([ 'cat1', + 'cat2', + 'cat3', + 'cat4', + 'cat5', + 'cat6', + 'cat7', + 'cat8', + 'cat9', 'cat10', 'cat11', 'cat12', @@ -167,15 +182,7 @@ describe('currentUser', () => { 'cat17', 'cat18', 'cat19', - 'cat2', - 'cat3', - 'cat4', - 'cat5', - 'cat6', - 'cat7', - 'cat8', - 'cat9', - ], + ]), }), }, }) @@ -267,7 +274,11 @@ describe('login', () => { describe('normalization', () => { describe('email address is a gmail address ', () => { beforeEach(async () => { - const email = await neode.first('EmailAddress', { email: 'test@example.org' }) + const email = await neode.first( + 'EmailAddress', + { email: 'test@example.org' }, + undefined, + ) await email.update({ email: 'someuser@gmail.com' }) }) diff --git a/backend/src/schema/resolvers/user_management.ts b/backend/src/schema/resolvers/user_management.ts index e9376f940..7bea1f53c 100644 --- a/backend/src/schema/resolvers/user_management.ts +++ b/backend/src/schema/resolvers/user_management.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { AuthenticationError } from 'apollo-server' import bcrypt from 'bcryptjs' import { neo4jgraphql } from 'neo4j-graphql-js' @@ -5,7 +12,6 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import { getNeode } from '@db/neo4j' import encode from '@jwt/encode' -import log from './helpers/databaseLogger' import normalizeEmail from './helpers/normalizeEmail' const neode = getNeode() @@ -16,7 +22,7 @@ export default { neo4jgraphql(object, { id: context.user.id }, context, resolveInfo), }, Mutation: { - login: async (_, { email, password }, { driver, req, user }) => { + login: async (_, { email, password }, { driver }) => { // if (user && user.id) { // throw new Error('Already logged in.') // } @@ -31,18 +37,17 @@ export default { `, { userEmail: email }, ) - log(loginTransactionResponse) return loginTransactionResponse.records.map((record) => record.get('user')) }) const [currentUser] = await loginReadTxResultPromise if ( currentUser && - (await bcrypt.compareSync(password, currentUser.encryptedPassword)) && + (await bcrypt.compare(password, currentUser.encryptedPassword)) && !currentUser.disabled ) { delete currentUser.encryptedPassword return encode(currentUser) - } else if (currentUser && currentUser.disabled) { + } else if (currentUser?.disabled) { throw new AuthenticationError('Your account has been disabled.') } else { throw new AuthenticationError('Incorrect email address or password.') @@ -51,19 +56,19 @@ export default { session.close() } }, - changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => { + changePassword: async (_, { oldPassword, newPassword }, { user }) => { const currentUser = await neode.find('User', user.id) - const encryptedPassword = currentUser.get('encryptedPassword') - if (!(await bcrypt.compareSync(oldPassword, encryptedPassword))) { + const encryptedPassword = currentUser.get('encryptedPassword') + if (!(await bcrypt.compare(oldPassword, encryptedPassword))) { throw new AuthenticationError('Old password is not correct') } - if (await bcrypt.compareSync(newPassword, encryptedPassword)) { + if (await bcrypt.compare(newPassword, encryptedPassword)) { throw new AuthenticationError('Old password and new password should be different') } - const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10) + const newEncryptedPassword = await bcrypt.hash(newPassword, 10) await currentUser.update({ encryptedPassword: newEncryptedPassword, updatedAt: new Date().toISOString(), diff --git a/backend/src/schema/resolvers/users.spec.ts b/backend/src/schema/resolvers/users.spec.ts index 0b14575db..ad37e2024 100644 --- a/backend/src/schema/resolvers/users.spec.ts +++ b/backend/src/schema/resolvers/users.spec.ts @@ -1,9 +1,15 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import { categories } from '@constants/categories' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' +import User from '@models/User' import createServer from '@src/server' const categoryIds = ['cat9'] @@ -70,6 +76,38 @@ const updateOnlineStatus = gql` } ` +const setTrophyBadgeSelected = gql` + mutation ($slot: Int!, $badgeId: ID) { + setTrophyBadgeSelected(slot: $slot, badgeId: $badgeId) { + badgeTrophiesCount + badgeTrophiesSelected { + id + isDefault + } + badgeTrophiesUnused { + id + } + badgeTrophiesUnusedCount + } + } +` + +const resetTrophyBadgesSelected = gql` + mutation { + resetTrophyBadgesSelected { + badgeTrophiesCount + badgeTrophiesSelected { + id + isDefault + } + badgeTrophiesUnused { + id + } + badgeTrophiesUnusedCount + } + } +` + beforeAll(async () => { await cleanDatabase() @@ -88,7 +126,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 @@ -1046,7 +1084,7 @@ describe('updateOnlineStatus', () => { const cypher = 'MATCH (u:User {id: $id}) RETURN u' const result = await neode.cypher(cypher, { id: authenticatedUser.id }) - const dbUser = neode.hydrateFirst(result, 'u', neode.model('User')) + const dbUser = neode.hydrateFirst(result, 'u', neode.model('User')) await expect(dbUser.toJson()).resolves.toMatchObject({ lastOnlineStatus: 'away', awaySince: expect.any(String), @@ -1070,3 +1108,499 @@ describe('updateOnlineStatus', () => { }) }) }) + +describe('setTrophyBadgeSelected', () => { + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + const badgeBear = await Factory.build('badge', { + id: 'trophy_bear', + type: 'trophy', + description: 'You earned a Bear', + icon: '/img/badges/trophy_blue_bear.svg', + }) + const badgePanda = await Factory.build('badge', { + id: 'trophy_panda', + type: 'trophy', + description: 'You earned a Panda', + icon: '/img/badges/trophy_blue_panda.svg', + }) + await Factory.build('badge', { + id: 'trophy_rabbit', + type: 'trophy', + description: 'You earned a Rabbit', + icon: '/img/badges/trophy_blue_rabbit.svg', + }) + + await user.relateTo(badgeBear, 'rewarded') + await user.relateTo(badgePanda, 'rewarded') + }) + + describe('not authenticated', () => { + beforeEach(async () => { + authenticatedUser = undefined + }) + + it('throws an error', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_bear' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + it('throws Error when slot is out of bound', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: -1, badgeId: 'trophy_bear' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Invalid slot! There is only 9 badge-slots to fill', + }), + ], + }), + ) + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 9, badgeId: 'trophy_bear' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Invalid slot! There is only 9 badge-slots to fill', + }), + ], + }), + ) + }) + + it('throws Error when badge was not rewarded to user', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_rabbit' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Error: You cannot set badges not rewarded to you.', + }), + ], + }), + ) + }) + + it('throws Error when badge is unknown', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_unknown' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Error: You cannot set badges not rewarded to you.', + }), + ], + }), + ) + }) + + it('returns the user with badges set on slots', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_bear' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + setTrophyBadgeSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'trophy_bear', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: [ + { + id: 'trophy_panda', + }, + ], + badgeTrophiesUnusedCount: 1, + }, + }, + }), + ) + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: 'trophy_panda' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + setTrophyBadgeSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'trophy_bear', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'trophy_panda', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: [], + badgeTrophiesUnusedCount: 0, + }, + }, + }), + ) + }) + + describe('set badge to null or default', () => { + beforeEach(async () => { + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_bear' }, + }) + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: 'trophy_panda' }, + }) + }) + + it('returns the user with no badge set on the selected slot when sending null', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: null }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + setTrophyBadgeSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'trophy_bear', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: [ + { + id: 'trophy_panda', + }, + ], + badgeTrophiesUnusedCount: 1, + }, + }, + }), + ) + }) + + it('returns the user with no badge set on the selected slot when sending default_trophy', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: 'default_trophy' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + setTrophyBadgeSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'trophy_bear', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: [ + { + id: 'trophy_panda', + }, + ], + badgeTrophiesUnusedCount: 1, + }, + }, + }), + ) + }) + }) + }) +}) + +describe('resetTrophyBadgesSelected', () => { + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + const badgeBear = await Factory.build('badge', { + id: 'trophy_bear', + type: 'trophy', + description: 'You earned a Bear', + icon: '/img/badges/trophy_blue_bear.svg', + }) + const badgePanda = await Factory.build('badge', { + id: 'trophy_panda', + type: 'trophy', + description: 'You earned a Panda', + icon: '/img/badges/trophy_blue_panda.svg', + }) + await Factory.build('badge', { + id: 'trophy_rabbit', + type: 'trophy', + description: 'You earned a Rabbit', + icon: '/img/badges/trophy_blue_rabbit.svg', + }) + + await user.relateTo(badgeBear, 'rewarded') + await user.relateTo(badgePanda, 'rewarded') + + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_bear' }, + }) + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: 'trophy_panda' }, + }) + }) + + describe('not authenticated', () => { + beforeEach(async () => { + authenticatedUser = undefined + }) + + it('throws an error', async () => { + await expect(mutate({ mutation: resetTrophyBadgesSelected })).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + it('returns the user with no profile badges badges set', async () => { + await expect(mutate({ mutation: resetTrophyBadgesSelected })).resolves.toEqual( + expect.objectContaining({ + data: { + resetTrophyBadgesSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: expect.arrayContaining([ + { + id: 'trophy_panda', + }, + { + id: 'trophy_bear', + }, + ]), + badgeTrophiesUnusedCount: 2, + }, + }, + }), + ) + }) + }) +}) diff --git a/backend/src/schema/resolvers/users.ts b/backend/src/schema/resolvers/users.ts index e93dffbd0..f549e79a3 100644 --- a/backend/src/schema/resolvers/users.ts +++ b/backend/src/schema/resolvers/users.ts @@ -1,9 +1,17 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError, ForbiddenError } from 'apollo-server' import { neo4jgraphql } from 'neo4j-graphql-js' +import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges' import { getNeode } from '@db/neo4j' -import log from './helpers/databaseLogger' +import { defaultTrophyBadge, defaultVerificationBadge } from './badges' import Resolver from './helpers/Resolver' import { mergeImage, deleteImage } from './images/images' import { createOrUpdateLocations } from './users/location' @@ -42,14 +50,14 @@ export const getBlockedUsers = async (context) => { export default { Query: { - mutedUsers: async (object, args, context, resolveInfo) => { + mutedUsers: async (_object, _args, context, _resolveInfo) => { try { return getMutedUsers(context) } catch (e) { throw new UserInputError(e.message) } }, - blockedUsers: async (object, args, context, resolveInfo) => { + blockedUsers: async (_object, _args, context, _resolveInfo) => { try { return getBlockedUsers(context) } catch (e) { @@ -110,7 +118,7 @@ export default { const unmutedUser = await neode.find('User', params.id) return unmutedUser.toJson() }, - blockUser: async (object, args, context, resolveInfo) => { + blockUser: async (_object, args, context, _resolveInfo) => { const { user: currentUser } = context if (currentUser.id === args.id) return null @@ -137,7 +145,7 @@ export default { session.close() } }, - unblockUser: async (object, args, context, resolveInfo) => { + unblockUser: async (_object, args, context, _resolveInfo) => { const { user: currentUser } = context if (currentUser.id === args.id) return null @@ -215,12 +223,12 @@ export default { session.close() } }, - DeleteUser: async (object, params, context, resolveInfo) => { + DeleteUser: async (_object, params, context, _resolveInfo) => { const { resource, id: userId } = params const session = context.driver.session() const deleteUserTxResultPromise = session.writeTransaction(async (transaction) => { - if (resource && resource.length) { + if (resource?.length) { await Promise.all( resource.map(async (node) => { const txResult = await transaction.run( @@ -270,7 +278,6 @@ export default { `, { userId }, ) - log(deleteUserTransactionResponse) const [user] = deleteUserTransactionResponse.records.map((record) => record.get('user')) await deleteImage(user, 'AVATAR_IMAGE', { transaction }) return user @@ -282,7 +289,7 @@ export default { session.close() } }, - switchUserRole: async (object, args, context, resolveInfo) => { + switchUserRole: async (_object, args, context, _resolveInfo) => { const { role, id } = args if (context.user.id === id) throw new Error('you-cannot-change-your-own-role') @@ -307,7 +314,7 @@ export default { session.close() } }, - saveCategorySettings: async (object, args, context, resolveInfo) => { + saveCategorySettings: async (_object, args, context, _resolveInfo) => { const { activeCategories } = args const { user: { id }, @@ -350,7 +357,7 @@ export default { session.close() } }, - updateOnlineStatus: async (object, args, context, resolveInfo) => { + updateOnlineStatus: async (_object, args, context, _resolveInfo) => { const { status } = args const { user: { id }, @@ -381,9 +388,86 @@ export default { return true }, + setTrophyBadgeSelected: async (_object, args, context, _resolveInfo) => { + const { slot, badgeId } = args + const { + user: { id: userId }, + } = context + + if (slot >= TROPHY_BADGES_SELECTED_MAX || slot < 0) { + throw new Error( + `Invalid slot! There is only ${TROPHY_BADGES_SELECTED_MAX} badge-slots to fill`, + ) + } + + const session = context.driver.session() + + const query = session.writeTransaction(async (transaction) => { + const queryBadge = ` + MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge {id: $badgeId}) + OPTIONAL MATCH (user)-[badgeRelation:SELECTED]->(badge) + OPTIONAL MATCH (user)-[slotRelation:SELECTED{slot: $slot}]->(:Badge) + DELETE badgeRelation, slotRelation + MERGE (user)-[:SELECTED{slot: toInteger($slot)}]->(badge) + RETURN user {.*} + ` + const queryEmpty = ` + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (user)-[slotRelation:SELECTED {slot: $slot}]->(:Badge) + DELETE slotRelation + RETURN user {.*} + ` + const isDefault = !badgeId || badgeId === defaultTrophyBadge.id + + const result = await transaction.run(isDefault ? queryEmpty : queryBadge, { + userId, + badgeId, + slot, + }) + return result.records.map((record) => record.get('user'))[0] + }) + try { + const user = await query + if (!user) { + throw new Error('You cannot set badges not rewarded to you.') + } + return user + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + resetTrophyBadgesSelected: async (_object, _args, context, _resolveInfo) => { + const { + user: { id: userId }, + } = context + + const session = context.driver.session() + + const query = session.writeTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (user)-[relation:SELECTED]->(:Badge) + DELETE relation + RETURN user {.*} + `, + { userId }, + ) + return result.records.map((record) => record.get('user'))[0] + }) + try { + return await query + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, }, User: { - emailNotificationSettings: async (parent, params, context, resolveInfo) => { + emailNotificationSettings: async (parent, _params, _context, _resolveInfo) => { return [ { type: 'post', @@ -438,6 +522,101 @@ export default { }, ] }, + badgeTrophiesSelected: async (parent, _params, context, _resolveInfo) => { + const session = context.driver.session() + + const query = session.readTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})-[relation:SELECTED]->(badge:Badge) + WITH relation, badge + ORDER BY relation.slot ASC + RETURN relation.slot as slot, badge {.*} + `, + { parent }, + ) + return result.records + }) + try { + const badgesSelected = await query + const result = Array(TROPHY_BADGES_SELECTED_MAX).fill(defaultTrophyBadge) + badgesSelected.map((record) => { + result[record.get('slot')] = record.get('badge') + return true + }) + return result + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + badgeTrophiesUnused: async (parent, _params, context, _resolveInfo) => { + const session = context.driver.session() + + const query = session.readTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})<-[:REWARDED]-(badge:Badge) + WHERE NOT (user)-[:SELECTED]-(badge) + RETURN badge {.*} + `, + { parent }, + ) + return result.records.map((record) => record.get('badge')) + }) + try { + return await query + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + badgeTrophiesUnusedCount: async (parent, _params, context, _resolveInfo) => { + const session = context.driver.session() + + const query = session.readTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})<-[:REWARDED]-(badge:Badge) + WHERE NOT (user)-[:SELECTED]-(badge) + RETURN toString(COUNT(badge)) as count + `, + { parent }, + ) + return result.records.map((record) => record.get('count'))[0] + }) + try { + return await query + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + badgeVerification: async (parent, _params, context, _resolveInfo) => { + const session = context.driver.session() + + const query = session.writeTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})<-[:VERIFIES]-(verification:Badge) + RETURN verification {.*} + `, + { parent }, + ) + return result.records.map((record) => record.get('verification'))[0] + }) + try { + const result = await query + return result ?? defaultVerificationBadge + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, ...Resolver('User', { undefinedToNull: [ 'actorId', @@ -471,7 +650,7 @@ export default { '-[:WROTE]->(c:Comment)-[:COMMENTS]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true', shoutedCount: '-[:SHOUTED]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true', - badgesCount: '<-[:REWARDED]-(related:Badge)', + badgeTrophiesCount: '<-[:REWARDED]-(related:Badge)', }, hasOne: { avatar: '-[:AVATAR_IMAGE]->(related:Image)', @@ -488,7 +667,7 @@ export default { comments: '-[:WROTE]->(related:Comment)', shouted: '-[:SHOUTED]->(related:Post)', categories: '-[:CATEGORIZED]->(related:Category)', - badges: '<-[:REWARDED]-(related:Badge)', + badgeTrophies: '<-[:REWARDED]-(related:Badge)', inviteCodes: '-[:GENERATED]->(related:InviteCode)', }, }), diff --git a/backend/src/schema/resolvers/users/location.spec.ts b/backend/src/schema/resolvers/users/location.spec.ts index 4f54dcc06..659c126dd 100644 --- a/backend/src/schema/resolvers/users/location.spec.ts +++ b/backend/src/schema/resolvers/users/location.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -91,7 +94,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(() => { @@ -210,6 +213,7 @@ describe('userMiddleware', () => { await mutate({ mutation: updateUserMutation, variables }) const locations = await neode.cypher( `MATCH (city:Location)-[:IS_IN]->(district:Location)-[:IS_IN]->(state:Location)-[:IS_IN]->(country:Location) return city {.*}, state {.*}, country {.*}`, + {}, ) expect( locations.records.map((record) => { diff --git a/backend/src/schema/resolvers/users/location.ts b/backend/src/schema/resolvers/users/location.ts index b663eebdf..6dfaede4e 100644 --- a/backend/src/schema/resolvers/users/location.ts +++ b/backend/src/schema/resolvers/users/location.ts @@ -1,16 +1,19 @@ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable promise/avoid-new */ /* eslint-disable promise/prefer-await-to-callbacks */ -/* eslint-disable import/no-named-as-default */ import { UserInputError } from 'apollo-server' -// eslint-disable-next-line import/no-extraneous-dependencies -import Debug from 'debug' import request from 'request' import CONFIG from '@config/index' import asyncForEach from '@helpers/asyncForEach' -const debug = Debug('human-connection:location') - const fetch = (url) => { return new Promise((resolve, reject) => { request(url, function (error, response, body) { @@ -39,8 +42,8 @@ const createLocation = async (session, mapboxData) => { nameRU: mapboxData.text_ru, type: mapboxData.id.split('.')[0].toLowerCase(), address: mapboxData.address, - lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[0] : null, - lat: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null, + lng: mapboxData.center?.length ? mapboxData.center[0] : null, + lat: mapboxData.center?.length ? mapboxData.center[1] : null, } let mutation = @@ -85,9 +88,7 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s }&types=region,place,country,address&language=${locales.join(',')}`, ) - debug(res) - - if (!res || !res.features || !res.features[0]) { + if (!res?.features?.[0]) { throw new UserInputError('locationName is invalid') } @@ -102,7 +103,7 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s data = res.features[0] } - if (!data || !data.place_type || !data.place_type.length) { + if (!data?.place_type?.length) { throw new UserInputError('locationName is invalid') } @@ -164,7 +165,7 @@ export const queryLocations = async ({ place, lang }) => { `https://api.mapbox.com/geocoding/v5/mapbox.places/${place}.json?access_token=${CONFIG.MAPBOX_TOKEN}&types=region,place,country&language=${lang}`, ) // Return empty array if no location found or error occurred - if (!res || !res.features) { + if (!res?.features) { return [] } return res.features diff --git a/backend/src/schema/resolvers/users/mutedUsers.spec.ts b/backend/src/schema/resolvers/users/mutedUsers.spec.ts index 1fda2b392..ccb6d2a87 100644 --- a/backend/src/schema/resolvers/users/mutedUsers.spec.ts +++ b/backend/src/schema/resolvers/users/mutedUsers.spec.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -19,7 +23,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(() => { @@ -60,6 +64,7 @@ describe('mutedUsers', () => { it('throws permission error', async () => { const { query } = createTestClient(server) const result = await query({ query: mutedUserQuery }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expect(result.errors![0]).toHaveProperty('message', 'Not Authorized!') }) diff --git a/backend/src/schema/resolvers/viewedTeaserCount.spec.ts b/backend/src/schema/resolvers/viewedTeaserCount.spec.ts index ebcb19c4e..f4fba31f8 100644 --- a/backend/src/schema/resolvers/viewedTeaserCount.spec.ts +++ b/backend/src/schema/resolvers/viewedTeaserCount.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -29,7 +32,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('count post teaser views', () => { diff --git a/backend/src/schema/types/type/Badge.gql b/backend/src/schema/types/type/Badge.gql deleted file mode 100644 index dff1de89a..000000000 --- a/backend/src/schema/types/type/Badge.gql +++ /dev/null @@ -1,29 +0,0 @@ -type Badge { - id: ID! - type: BadgeType! - status: BadgeStatus! - icon: String! - createdAt: String - updatedAt: String - - rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") -} - -enum BadgeStatus { - permanent - temporary -} - -enum BadgeType { - role - crowdfunding -} - -type Query { - Badge: [Badge] -} - -type Mutation { - reward(badgeKey: ID!, userId: ID!): User - unreward(badgeKey: ID!, userId: ID!): User -} diff --git a/backend/src/server.ts b/backend/src/server.ts index 117e0c3b6..372ec964b 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable import/no-named-as-default-member */ import http from 'node:http' @@ -68,7 +74,7 @@ const createServer = (options?) => { context, schema: middleware(schema), subscriptions: { - onConnect: (connectionParams, webSocket) => { + onConnect: (connectionParams, _webSocket) => { return getContext(connectionParams) }, }, diff --git a/backend/test/setup.ts b/backend/test/setup.ts index d2f24bd40..d1d32be5b 100644 --- a/backend/test/setup.ts +++ b/backend/test/setup.ts @@ -1,8 +1,2 @@ -// Polyfill missing encoders in jsdom -// https://stackoverflow.com/questions/68468203/why-am-i-getting-textencoder-is-not-defined-in-jest -import { TextEncoder, TextDecoder } from 'util' -global.TextEncoder = TextEncoder -global.TextDecoder = TextDecoder as any - // Metascraper takes longer nowadays, double time -jest.setTimeout(10000) \ No newline at end of file +jest.setTimeout(10000) diff --git a/backend/yarn.lock b/backend/yarn.lock index 9e64960ff..c702695e1 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1091,10 +1091,10 @@ dependencies: tslib "^2.4.0" -"@eslint-community/eslint-plugin-eslint-comments@^4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.4.1.tgz#dbfab6f2447c22be8758a0a9a9c80e56d2e2b93f" - integrity sha512-lb/Z/MzbTf7CaVYM9WCFNQZ4L1yi3ev2fsFPF99h31ljhSEyUoyEsKsNWiU+qD1glbYTDJdqgyaLKtyTkkqtuQ== +"@eslint-community/eslint-plugin-eslint-comments@^4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.5.0.tgz#4ffa576583bd99dfbaf74c893635e2c76acba048" + integrity sha512-MAhuTKlr4y/CE3WYX26raZjy+I/kS2PLKSzvfmDCGrBLTFHOYwqROZdr4XwPgXwX3K9rjzMr4pSmUWGnzsUyMg== dependencies: escape-string-regexp "^4.0.0" ignore "^5.2.4" @@ -1143,10 +1143,10 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== -"@faker-js/faker@9.6.0": - version "9.6.0" - resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.6.0.tgz#64235d20330b142eef3d1d1638ba56c083b4bf1d" - integrity sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw== +"@faker-js/faker@9.7.0": + version "9.7.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.7.0.tgz#1cf1fecfcad5e2da2332140bf3b5f23cc1c2a7f4" + integrity sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg== "@graphql-toolkit/common@0.10.4": version "0.10.4" @@ -1579,10 +1579,10 @@ url-regex "~4.1.1" video-extensions "~1.1.0" -"@napi-rs/wasm-runtime@^0.2.8": - version "0.2.8" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.8.tgz#642e8390ee78ed21d6b79c467aa610e249224ed6" - integrity sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg== +"@napi-rs/wasm-runtime@^0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz#7278122cf94f3b36d8170a8eee7d85356dfa6a96" + integrity sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg== dependencies: "@emnapi/core" "^1.4.0" "@emnapi/runtime" "^1.4.0" @@ -2061,6 +2061,11 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/lodash@^4.17.16": + version "4.17.16" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.16.tgz#94ae78fab4a38d73086e962d0b65c30d816bfb0a" + integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g== + "@types/long@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" @@ -2079,10 +2084,10 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>=6", "@types/node@^22.14.1": - version "22.14.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.14.1.tgz#53b54585cec81c21eee3697521e31312d6ca1e6f" - integrity sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw== +"@types/node@*", "@types/node@>=6", "@types/node@^22.15.2": + version "22.15.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.2.tgz#1db55aa64618ee93a58c8912f74beefe44aca905" + integrity sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A== dependencies: undici-types "~6.21.0" @@ -2126,6 +2131,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/uuid@~9.0.1": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== + "@types/ws@^7.0.0": version "7.2.5" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.5.tgz#513f28b04a1ea1aa9dc2cad3f26e8e37c88aae49" @@ -2289,87 +2299,92 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@unrs/resolver-binding-darwin-arm64@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.5.0.tgz#0c64ebe422a3d05ada91d8ba84e037383742c955" - integrity sha512-YmocNlEcX/AgJv8gI41bhjMOTcKcea4D2nRIbZj+MhRtSH5+vEU8r/pFuTuoF+JjVplLsBueU+CILfBPVISyGQ== +"@unrs/resolver-binding-darwin-arm64@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.0.tgz#04fe2253f2b6366ae993b1565c6495e563ad8a4c" + integrity sha512-vIWAU56r2lZAmUsljp6m9+hrTlwNkZH6pqnSPff2WxzofV+jWRSHLmZRUS+g+VE+LlyPByifmGGHpJmhWetatg== -"@unrs/resolver-binding-darwin-x64@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.5.0.tgz#57210874eca22ec3a07039c97c028fb19c0c6d57" - integrity sha512-qpUrXgH4e/0xu1LOhPEdfgSY3vIXOxDQv370NEL8npN8h40HcQDA+Pl2r4HBW6tTXezWIjxUFcP7tj529RZtDw== +"@unrs/resolver-binding-darwin-x64@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.0.tgz#8d74ee589f1c379b9b75880ea85871bdaf89766e" + integrity sha512-+bShFLgtdwuNteQbKq3X230754AouNMXSLDZ56EssgDyckDt6Ld7wRaJjZF0pY671HnY2pk9/amO4amAFzfN1A== -"@unrs/resolver-binding-freebsd-x64@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.5.0.tgz#4519371d0ad8e557a86623d8497e3abcdcb5ae43" - integrity sha512-3tX8r8vgjvZzaJZB4jvxUaaFCDCb3aWDCpZN3EjhGnnwhztslI05KSG5NY/jNjlcZ5QWZ7dEZZ/rNBFsmTaSPw== +"@unrs/resolver-binding-freebsd-x64@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.0.tgz#d0bcea8e240d54d048aa45a6c7bd7e4d4824abfb" + integrity sha512-HJjXb3aIptDZQ0saSmk2S4W1pWNVZ2iNpAbNGZOfsUXbi8xwCmHdVjErNS92hRp7djuDLup1OLrzOMtTdw5BmA== -"@unrs/resolver-binding-linux-arm-gnueabihf@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.5.0.tgz#4fc05aec9e65a6478003a0b9034a06ac0da886ab" - integrity sha512-FH+ixzBKaUU9fWOj3TYO+Yn/eO6kYvMLV9eNJlJlkU7OgrxkCmiMS6wUbyT0KA3FOZGxnEQ2z3/BHgYm2jqeLA== +"@unrs/resolver-binding-linux-arm-gnueabihf@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.0.tgz#ae56292948a47a876d894da740b8001a14c88bc3" + integrity sha512-NF3lk7KHulLD97UE+MHjH0mrOjeZG8Hz10h48YcFz2V0rlxBdRSRcMbGer8iH/1mIlLqxtvXJfGLUr4SMj0XZg== -"@unrs/resolver-binding-linux-arm-musleabihf@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.5.0.tgz#c24b35dd5818fcd25569425b1dc1a98a883e248b" - integrity sha512-pxCgXMgwB/4PfqFQg73lMhmWwcC0j5L+dNXhZoz/0ek0iS/oAWl65fxZeT/OnU7fVs52MgdP2q02EipqJJXHSg== +"@unrs/resolver-binding-linux-arm-musleabihf@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.0.tgz#4a32424660d2f0ed328297b24f46e64f4c2990d8" + integrity sha512-Gn1c/t24irDgU8yYj4vVG6qHplwUM42ti9/zYWgfmFjoXCH6L4Ab9hh6HuO7bfDSvGDRGWQt1IVaBpgbKHdh3Q== -"@unrs/resolver-binding-linux-arm64-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.5.0.tgz#07dc8478a0a356d343790208dc557d6d053689af" - integrity sha512-FX2FV7vpLE/+Z0NZX9/1pwWud5Wocm/2PgpUXbT5aSV3QEB10kBPJAzssOQylvdj8mOHoKl5pVkXpbCwww/T2g== +"@unrs/resolver-binding-linux-arm64-gnu@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.0.tgz#7b9d73558a2d85911c82314784edb89dcd0b274d" + integrity sha512-XRrVXRIUP++qyqAqgiXUpOv0GP3cHx7aA7NrzVFf6Cc8FoYuwtnmT+vctfSo4wRZN71MNU4xq2BEFxI4qvSerg== -"@unrs/resolver-binding-linux-arm64-musl@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.5.0.tgz#169e531731f7e462dffa410034a1d06a7a921aa8" - integrity sha512-+gF97xst1BZb28T3nwwzEtq2ewCoMDGKsenYsZuvpmNrW0019G1iUAunZN+FG55L21y+uP7zsGX06OXDQ/viKw== +"@unrs/resolver-binding-linux-arm64-musl@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.0.tgz#7d7305c5f5610744ef7a373d2a9022c922113568" + integrity sha512-Sligg+vTDAYTXkUtgviPjGEFIh57pkvlfdyRw21i9gkjp/eCNOAi2o5e7qLGTkoYdJHZJs5wVMViPEmAbw2/Tg== -"@unrs/resolver-binding-linux-ppc64-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.5.0.tgz#f6ad2ff47d74c8158b28a18536a71a8ecf84a17f" - integrity sha512-5bEmVcQw9js8JYM2LkUBw5SeELSIxX+qKf9bFrfFINKAp4noZ//hUxLpbF7u/3gTBN1GsER6xOzIZlw/VTdXtA== +"@unrs/resolver-binding-linux-ppc64-gnu@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.0.tgz#280e4846c3bd9b81fdda25ac3cdda203da9bfd20" + integrity sha512-Apek8/x+7Rg33zUJlQV44Bvq8/t1brfulk0veNJrk9wprF89bCYFMUHF7zQYcpf2u+m1+qs3mYQrBd43fGXhMA== -"@unrs/resolver-binding-linux-riscv64-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.5.0.tgz#2f3986cb44f285f90d27e87cee8b4059de3ffbdd" - integrity sha512-GGk/8TPUsf1Q99F+lzMdjE6sGL26uJCwQ9TlvBs8zR3cLQNw/MIumPN7zrs3GFGySjnwXc8gA6J3HKbejywmqA== +"@unrs/resolver-binding-linux-riscv64-gnu@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.0.tgz#63301787af18d158ab4e99ec5041f507da228721" + integrity sha512-kBale8CFX5clfV9VmI9EwKw2ZACMEx1ecjV92F9SeWTUoxl9d+LGzS6zMSX3kGYqcfJB3NXMwLCTwIDBLG1y4g== -"@unrs/resolver-binding-linux-s390x-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.5.0.tgz#813ea07833012bc34ecc59f023e422b421138761" - integrity sha512-5uRkFYYVNAeVaA4W/CwugjFN3iDOHCPqsBLCCOoJiMfFMMz4evBRsg+498OFa9w6VcTn2bD5aI+RRayaIgk2Sw== +"@unrs/resolver-binding-linux-riscv64-musl@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.0.tgz#b85d66b2c4d73fe335d448322c708448c4487c44" + integrity sha512-s/Q33xQjeFHSCvGl1sZztFZF6xhv7coMvFz6wa/x/ZlEArjiQoMMwGa/Aieq1Kp/6+S13iU3/IJF0ga6/451ow== -"@unrs/resolver-binding-linux-x64-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.5.0.tgz#18b0d7553268fa490db92be578ac4b0fd8cae049" - integrity sha512-j905CZH3nehYy6NimNqC2B14pxn4Ltd7guKMyPTzKehbFXTUgihQS/ZfHQTdojkMzbSwBOSgq1dOrY+IpgxDsA== +"@unrs/resolver-binding-linux-s390x-gnu@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.0.tgz#f4202f7bebd823e0744a785ac1426f07129a2f81" + integrity sha512-7PuNXAo97ydaxVNrIYJzPipvINJafDpB8pt5CoZHfu8BmqcU6d7kl6/SABTnqNffNkd6Cfhuo70jvGB2P7oJ/Q== -"@unrs/resolver-binding-linux-x64-musl@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.5.0.tgz#04541e98d16e358c695393251e365bc3d802dfa4" - integrity sha512-dmLevQTuzQRwu5A+mvj54R5aye5I4PVKiWqGxg8tTaYP2k2oTs/3Mo8mgnhPk28VoYCi0fdFYpgzCd4AJndQvQ== +"@unrs/resolver-binding-linux-x64-gnu@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.0.tgz#c3fa31d0b4cc49d54c956dec43bead5a0c4127cf" + integrity sha512-fNosEzDMYItA4It+R0tioHwKlEfx/3TkkJdP2x9B5o9R946NDC4ZZj5ZjA+Y4NQD2V/imB3QPAKmeh3vHQGQyA== -"@unrs/resolver-binding-wasm32-wasi@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.5.0.tgz#7a2ae7467c4c52d53c20ad7fc2bace1b23de8168" - integrity sha512-LtJMhwu7avhoi+kKfAZOKN773RtzLBVVF90YJbB0wyMpUj9yQPeA+mteVUI9P70OG/opH47FeV5AWeaNWWgqJg== +"@unrs/resolver-binding-linux-x64-musl@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.0.tgz#a447f7261958f688950be70a26b79a7955fb10d3" + integrity sha512-gHIw42dmnVcw7osjNPRybaXhONhggWkkzqiOZzXco1q3OKkn4KsbDylATeemnq3TP+L1BrzSqzl0H9UTJ6ji+w== + +"@unrs/resolver-binding-wasm32-wasi@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.0.tgz#fc9c486ffddef353daef71488f8f77e4de44dd8b" + integrity sha512-yq7POusv63/yTkNTaNsnXU/SAcBzckHyk1oYrDXqjS1m/goaWAaU9J9HrsovgTHkljxTcDd6PMAsJ5WZVBuGEQ== dependencies: - "@napi-rs/wasm-runtime" "^0.2.8" + "@napi-rs/wasm-runtime" "^0.2.9" -"@unrs/resolver-binding-win32-arm64-msvc@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.5.0.tgz#11deb282b8ce73fab26f1d04df0fa4d6363752c2" - integrity sha512-FTZBxLL4SO1mgIM86KykzJmPeTPisBDHQV6xtfDXbTMrentuZ6SdQKJUV5BWaoUK3p8kIULlrCcucqdCnk8Npg== +"@unrs/resolver-binding-win32-arm64-msvc@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.0.tgz#c316d889d29293faab926d1260b16a2d4c430ed4" + integrity sha512-/IPZPbdri9jglHonwB3F7EpQZvBK3ObH+g4ma/KDrqTEAECwvgE10Unvo0ox3LQFR/iMMAkVY+sGNMrMiIV/QQ== -"@unrs/resolver-binding-win32-ia32-msvc@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.5.0.tgz#2a5d414912379425bd395ea15901a5dd5febc7c1" - integrity sha512-i5bB7vJ1waUsFciU/FKLd4Zw0VnAkvhiJ4//jYQXyDUuiLKodmtQZVTcOPU7pp97RrNgCFtXfC1gnvj/DHPJTw== +"@unrs/resolver-binding-win32-ia32-msvc@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.0.tgz#14c9e08990dd0cf10d4962c40e9b368ea06b9789" + integrity sha512-NGVKbHEdrLuJdpcuGqV5zXO3v8t4CWOs0qeCGjO47RiwwufOi/yYcrtxtCzZAaMPBrffHL7c6tJ1Hxr17cPUGg== -"@unrs/resolver-binding-win32-x64-msvc@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.5.0.tgz#5768c6bba4a27833a48a8a77e50eb01b520d0962" - integrity sha512-wAvXp4k7jhioi4SebXW/yfzzYwsUCr9kIX4gCsUFKpCTUf8Mi7vScJXI3S+kupSUf0LbVHudR8qBbe2wFMSNUw== +"@unrs/resolver-binding-win32-x64-msvc@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.0.tgz#c130ae8c0ce56dd1fe952d44fe95a6f9a91cccb6" + integrity sha512-Jf14pKofg58DIwcZv4Wt9AyVVe7bSJP8ODz+EP9nG/rho08FQzan0VOJk1g6/BNE1RkoYd+lRTWK+/BgH12qoQ== "@wry/context@^0.4.0": version "0.4.4" @@ -4581,10 +4596,10 @@ eslint-compat-utils@^0.5.1: dependencies: semver "^7.5.4" -eslint-config-prettier@^10.1.1: - version "10.1.1" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz#cf0ff6e5c4e7e15f129f1f1ce2a5ecba92dec132" - integrity sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw== +eslint-config-prettier@^10.1.2: + version "10.1.2" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz#31a4b393c40c4180202c27e829af43323bf85276" + integrity sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA== eslint-config-standard@^17.1.0: version "17.1.0" @@ -4600,17 +4615,17 @@ eslint-import-resolver-node@^0.3.9: is-core-module "^2.13.0" resolve "^1.22.4" -eslint-import-resolver-typescript@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.2.tgz#1d2371be6d073bade177ee04a4548dbacdc334c0" - integrity sha512-T2LqBXj87ndEC9t1LrDiPkzalSFzD4rrXr6BTzGdgMx1jdQM4T972guQvg7Ih+LNO51GURXI/qMHS5GF3h1ilw== +eslint-import-resolver-typescript@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.4.tgz#3d04161698925b5dc9c297966442c2761a319de4" + integrity sha512-buzw5z5VtiQMysYLH9iW9BV04YyZebsw+gPi+c4FCjfS9i6COYOrEWw9t3m3wA9PFBfqcBCqWf32qrXLbwafDw== dependencies: debug "^4.4.0" get-tsconfig "^4.10.0" is-bun-module "^2.0.0" stable-hash "^0.0.5" - tinyglobby "^0.2.12" - unrs-resolver "^1.4.1" + tinyglobby "^0.2.13" + unrs-resolver "^1.6.3" eslint-module-utils@^2.12.0: version "2.12.0" @@ -5043,10 +5058,10 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fdir@^6.4.3: - version "6.4.3" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.3.tgz#011cdacf837eca9b811c89dbb902df714273db72" - integrity sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw== +fdir@^6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" + integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== file-entry-cache@^6.0.1: version "6.0.1" @@ -5942,12 +5957,7 @@ ignore-by-default@^1.0.1: resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= -ignore@^5.1.4, ignore@^5.2.0, ignore@^5.2.4: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" - integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== - -ignore@^5.3.2: +ignore@^5.1.4, ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== @@ -7561,14 +7571,7 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.1, minimatch@^9.0.4: - version "9.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" - integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.5: +minimatch@^9.0.1, minimatch@^9.0.4, minimatch@^9.0.5: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== @@ -7701,6 +7704,11 @@ nanoid@^3.3.6: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== +napi-postinstall@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.1.6.tgz#7682101f43fc66c233b625ee8ebf07826c6eedde" + integrity sha512-w1bClprmjwpybo+7M1Rd0N4QK5Ein8kH/1CQ0Wv8Q9vrLbDMakxc4rZpv8zYc8RVErUELJlFhM8UzOF3IqlYKw== + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -7858,15 +7866,15 @@ nodemailer-html-to-text@^3.2.0: dependencies: html-to-text "7.1.1" -nodemailer@^6.10.0: - version "6.10.0" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.10.0.tgz#1f24c9de94ad79c6206f66d132776b6503003912" - integrity sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA== +nodemailer@^6.10.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.10.1.tgz#cbc434c54238f83a51c07eabd04e2b3e832da623" + integrity sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA== -nodemon@~3.1.9: - version "3.1.9" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.9.tgz#df502cdc3b120e1c3c0c6e4152349019efa7387b" - integrity sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg== +nodemon@~3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.10.tgz#5015c5eb4fffcb24d98cf9454df14f4fecec9bc1" + integrity sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw== dependencies: chokidar "^3.5.2" debug "^4" @@ -8910,10 +8918,10 @@ safe-regex@^2.1.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sanitize-html@~2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.15.0.tgz#8e7f97ee1fecdec1bb1fb2e37f6d2c6acfdbabe3" - integrity sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA== +sanitize-html@~2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.16.0.tgz#2b9973b63fa42e3580020499cbda1d894b3642bc" + integrity sha512-0s4caLuHHaZFVxFTG74oW91+j6vW7gKbGD6CD2+miP73CE6z6YtOBN0ArtLd2UGyi4IC7K47v3ENUbQX4jV3Mg== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" @@ -9578,12 +9586,12 @@ timers-ext@^0.1.7: es5-ext "^0.10.64" next-tick "^1.1.0" -tinyglobby@^0.2.12: - version "0.2.12" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.12.tgz#ac941a42e0c5773bd0b5d08f32de82e74a1a61b5" - integrity sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww== +tinyglobby@^0.2.13: + version "0.2.13" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e" + integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw== dependencies: - fdir "^6.4.3" + fdir "^6.4.4" picomatch "^4.0.2" title@~3.4.1: @@ -10055,27 +10063,30 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= -unrs-resolver@^1.4.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.5.0.tgz#d0a608f08321d8e90ba8eb10a3240e7995997275" - integrity sha512-6aia3Oy7SEe0MuUGQm2nsyob0L2+g57w178K5SE/3pvSGAIp28BB2O921fKx424Ahc/gQ6v0DXFbhcpyhGZdOA== +unrs-resolver@^1.6.3: + version "1.7.0" + resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.7.0.tgz#2d1523d0a9c9271d0dc5b400520b776b947893ea" + integrity sha512-b76tVoT9KPniDY1GoYghDUQX20gjzXm/TONfHfgayLaiuo+oGyT9CsQkGCEJs+1/uryVBEOGOt3yYWDXbJhL7g== + dependencies: + napi-postinstall "^0.1.6" optionalDependencies: - "@unrs/resolver-binding-darwin-arm64" "1.5.0" - "@unrs/resolver-binding-darwin-x64" "1.5.0" - "@unrs/resolver-binding-freebsd-x64" "1.5.0" - "@unrs/resolver-binding-linux-arm-gnueabihf" "1.5.0" - "@unrs/resolver-binding-linux-arm-musleabihf" "1.5.0" - "@unrs/resolver-binding-linux-arm64-gnu" "1.5.0" - "@unrs/resolver-binding-linux-arm64-musl" "1.5.0" - "@unrs/resolver-binding-linux-ppc64-gnu" "1.5.0" - "@unrs/resolver-binding-linux-riscv64-gnu" "1.5.0" - "@unrs/resolver-binding-linux-s390x-gnu" "1.5.0" - "@unrs/resolver-binding-linux-x64-gnu" "1.5.0" - "@unrs/resolver-binding-linux-x64-musl" "1.5.0" - "@unrs/resolver-binding-wasm32-wasi" "1.5.0" - "@unrs/resolver-binding-win32-arm64-msvc" "1.5.0" - "@unrs/resolver-binding-win32-ia32-msvc" "1.5.0" - "@unrs/resolver-binding-win32-x64-msvc" "1.5.0" + "@unrs/resolver-binding-darwin-arm64" "1.7.0" + "@unrs/resolver-binding-darwin-x64" "1.7.0" + "@unrs/resolver-binding-freebsd-x64" "1.7.0" + "@unrs/resolver-binding-linux-arm-gnueabihf" "1.7.0" + "@unrs/resolver-binding-linux-arm-musleabihf" "1.7.0" + "@unrs/resolver-binding-linux-arm64-gnu" "1.7.0" + "@unrs/resolver-binding-linux-arm64-musl" "1.7.0" + "@unrs/resolver-binding-linux-ppc64-gnu" "1.7.0" + "@unrs/resolver-binding-linux-riscv64-gnu" "1.7.0" + "@unrs/resolver-binding-linux-riscv64-musl" "1.7.0" + "@unrs/resolver-binding-linux-s390x-gnu" "1.7.0" + "@unrs/resolver-binding-linux-x64-gnu" "1.7.0" + "@unrs/resolver-binding-linux-x64-musl" "1.7.0" + "@unrs/resolver-binding-wasm32-wasi" "1.7.0" + "@unrs/resolver-binding-win32-arm64-msvc" "1.7.0" + "@unrs/resolver-binding-win32-ia32-msvc" "1.7.0" + "@unrs/resolver-binding-win32-x64-msvc" "1.7.0" update-browserslist-db@^1.1.0: version "1.1.0" diff --git a/deployment/helm/charts/ocelot-neo4j/Chart.yaml b/deployment/helm/charts/ocelot-neo4j/Chart.yaml index 83fb561dc..e9f8e2354 100644 --- a/deployment/helm/charts/ocelot-neo4j/Chart.yaml +++ b/deployment/helm/charts/ocelot-neo4j/Chart.yaml @@ -21,4 +21,4 @@ version: 0.1.0 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "3.3.0" +appVersion: "3.4.0" diff --git a/deployment/helm/charts/ocelot-social/Chart.yaml b/deployment/helm/charts/ocelot-social/Chart.yaml index d15c18a4a..2a872480e 100644 --- a/deployment/helm/charts/ocelot-social/Chart.yaml +++ b/deployment/helm/charts/ocelot-social/Chart.yaml @@ -21,4 +21,4 @@ version: 0.1.0 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "3.3.0" +appVersion: "3.4.0" diff --git a/docker-compose.yml b/docker-compose.yml index d46b5cd29..8397c4e47 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,8 +43,6 @@ services: # Application only envs #- HOST=0.0.0.0 # This is nuxt specific, alternative value is HOST=webapp #- GRAPHQL_URI=http://backend:4000 - env_file: - - ./frontend/.env backend: image: ghcr.io/ocelot-social-community/ocelot-social/backend:${OCELOT_VERSION:-latest} diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 000000000..5871e601c --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +v20.12.1 \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 63b54a127..4a8a0442c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ocelot-social-frontend", - "version": "3.2.1", + "version": "3.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ocelot-social-frontend", - "version": "3.2.1", + "version": "3.4.0", "license": "Apache-2.0", "dependencies": { "@intlify/unplugin-vue-i18n": "^2.0.0", diff --git a/frontend/package.json b/frontend/package.json index b4e46da30..5e2235be5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social-frontend", - "version": "3.3.0", + "version": "3.4.0", "description": "ocelot.social new Frontend (in development and not fully implemented) by IT4C Boilerplate for frontends", "main": "build/index.js", "type": "module", diff --git a/package-lock.json b/package-lock.json index 4e02f555e..17de00757 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,11 @@ "@badeball/cypress-cucumber-preprocessor": "^22.0.1", "@cucumber/cucumber": "11.2.0", "@cypress/browserify-preprocessor": "^3.0.2", - "@faker-js/faker": "9.6.0", + "@faker-js/faker": "9.7.0", "auto-changelog": "^2.5.0", "bcryptjs": "^3.0.2", "cross-env": "^7.0.3", - "cypress": "^14.3.0", + "cypress": "^14.3.2", "cypress-network-idle": "^1.15.0", "date-fns": "^3.6.0", "dotenv": "^16.5.0", @@ -2786,9 +2786,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.6.0.tgz", - "integrity": "sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw==", + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.7.0.tgz", + "integrity": "sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==", "dev": true, "funding": [ { @@ -7762,9 +7762,9 @@ "optional": true }, "node_modules/cypress": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.3.0.tgz", - "integrity": "sha512-rRfPl9Z0/CczuYybBEoLbDVuT1OGkhYaJ0+urRCshgiDRz6QnoA0KQIQnPx7MJ3zy+VCsbUU1pV74n+6cbJEdg==", + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.3.2.tgz", + "integrity": "sha512-n+yGD2ZFFKgy7I3YtVpZ7BcFYrrDMcKj713eOZdtxPttpBjCyw/R8dLlFSsJPouneGN7A/HOSRyPJ5+3/gKDoA==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/package.json b/package.json index 7ddc60707..58d4fd768 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social", - "version": "3.3.0", + "version": "3.4.0", "description": "Free and open source software program code available to run social networks.", "author": "ocelot.social Community", "license": "MIT", @@ -39,11 +39,11 @@ "@badeball/cypress-cucumber-preprocessor": "^22.0.1", "@cucumber/cucumber": "11.2.0", "@cypress/browserify-preprocessor": "^3.0.2", - "@faker-js/faker": "9.6.0", + "@faker-js/faker": "9.7.0", "auto-changelog": "^2.5.0", "bcryptjs": "^3.0.2", "cross-env": "^7.0.3", - "cypress": "^14.3.0", + "cypress": "^14.3.2", "cypress-network-idle": "^1.15.0", "date-fns": "^3.6.0", "dotenv": "^16.5.0", diff --git a/webapp/assets/_new/styles/export.scss b/webapp/assets/_new/styles/export.scss index e29c014e2..c8f16b750 100644 --- a/webapp/assets/_new/styles/export.scss +++ b/webapp/assets/_new/styles/export.scss @@ -20,6 +20,10 @@ backgroundColorPrimary: $background-color-primary; colorNeutral30: $color-neutral-30; + + chatSidemenuBg: $chat-sidemenu-bg; + chatSidemenuBackgroundOver: $chat-sidemenu-background-over; + chatSidemenuBackgroundActive: $chat-sidemenu-background-active; chatMessageColor: $chat-message-color; @@ -34,4 +38,8 @@ chatRoomBackgroundCounterBadge: $chat-room-background-counter-badge; chatRoomColorCounterBadge: $chat-room-color-counter-badge; + + chatIconAdd: $chat-icon-add; + chatIconSend: $chat-icon-send; + chatIconEmoji: $chat-icon-emoji; } \ No newline at end of file diff --git a/webapp/assets/_new/styles/tokens.scss b/webapp/assets/_new/styles/tokens.scss index dd3a042d1..578484355 100644 --- a/webapp/assets/_new/styles/tokens.scss +++ b/webapp/assets/_new/styles/tokens.scss @@ -403,10 +403,12 @@ $color-toast-green: $color-success; * @tokens Ribbon Color */ -$color-ribbon-event: $background-color-third; -$color-ribbon-event-active: $background-color-third-active; -$color-ribbon-article: $background-color-secondary; -$color-ribbon-article-active: $background-color-secondary-active; +$color-ribbon-announcement: $background-color-warning; +$color-ribbon-announcement-shadow: color.adjust($color-ribbon-announcement, $lightness: -20%,); +$color-ribbon-article: $background-color-secondary-active; +$color-ribbon-article-shadow: color.adjust($color-ribbon-article, $lightness: -20%); +$color-ribbon-event: $background-color-third-active; +$color-ribbon-event-shadow: color.adjust($color-ribbon-event, $lightness: -20%); /** * @tokens Chat Color @@ -415,10 +417,15 @@ $color-ribbon-article-active: $background-color-secondary-active; $chat-message-bg-me: $color-primary-light; $chat-message-color: $text-color-base; $chat-message-bg-others: $color-neutral-80; -$chat-sidemenu-bg: $color-secondary-active; +$chat-sidemenu-bg: white; +$chat-sidemenu-background-over: '#f6f6f6'; +$chat-sidemenu-background-active: $color-primary-light; $chat-new-message-color: $color-secondary-active; $chat-message-timestamp: $text-color-soft; $chat-message-checkmark-seen: $text-color-secondary; $chat-message-checkmark: $text-color-soft; $chat-room-color-counter-badge: $text-color-inverse; $chat-room-background-counter-badge: $color-secondary; +$chat-icon-add: $color-primary; +$chat-icon-send: $color-primary; +$chat-icon-emoji: $color-primary; diff --git a/webapp/assets/_new/styles/uses.scss b/webapp/assets/_new/styles/uses.scss new file mode 100644 index 000000000..37ad2e69c --- /dev/null +++ b/webapp/assets/_new/styles/uses.scss @@ -0,0 +1,4 @@ +// we can not use the command '@use' in 'tokens.scss' because of the compiler error 'SassError: @use rules must be written before any other rules.' +// therefore we integrate this file at first in 'nuxt.config.js' + +@use 'sass:color'; diff --git a/webapp/components/AvatarMenu/AvatarMenu.vue b/webapp/components/AvatarMenu/AvatarMenu.vue index ac583ed4b..d19f0bf95 100644 --- a/webapp/components/AvatarMenu/AvatarMenu.vue +++ b/webapp/components/AvatarMenu/AvatarMenu.vue @@ -168,10 +168,10 @@ export default { background-color: $color-neutral-90; } .logout-link { - color: $text-color-base; + color: $text-color-danger; padding-top: $space-xx-small; &:hover { - color: $text-color-link-active; + color: color.adjust($text-color-danger, $lightness: -10%); } } } diff --git a/webapp/components/BadgeSelection.spec.js b/webapp/components/BadgeSelection.spec.js new file mode 100644 index 000000000..78f00b87a --- /dev/null +++ b/webapp/components/BadgeSelection.spec.js @@ -0,0 +1,76 @@ +import { render, screen, fireEvent } from '@testing-library/vue' +import BadgeSelection from './BadgeSelection.vue' + +const localVue = global.localVue + +describe('Badges.vue', () => { + const Wrapper = (propsData) => { + return render(BadgeSelection, { + propsData, + localVue, + }) + } + + describe('without badges', () => { + it('renders', () => { + const wrapper = Wrapper({ badges: [] }) + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('with badges', () => { + const badges = [ + { + id: '1', + icon: '/path/to/some/icon', + isDefault: false, + description: 'Some description', + }, + { + id: '2', + icon: '/path/to/another/icon', + isDefault: true, + description: 'Another description', + }, + { + id: '3', + icon: '/path/to/third/icon', + isDefault: false, + description: 'Third description', + }, + ] + + let wrapper + + beforeEach(() => { + wrapper = Wrapper({ badges }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('clicking on a badge', () => { + beforeEach(async () => { + const badge = screen.getByText(badges[1].description) + await fireEvent.click(badge) + }) + + it('emits badge-selected with badge', async () => { + expect(wrapper.emitted()['badge-selected']).toEqual([[badges[1]]]) + }) + }) + + describe('clicking twice on a badge', () => { + beforeEach(async () => { + const badge = screen.getByText(badges[1].description) + await fireEvent.click(badge) + await fireEvent.click(badge) + }) + + it('emits badge-selected with null', async () => { + expect(wrapper.emitted()['badge-selected']).toEqual([[badges[1]], [null]]) + }) + }) + }) +}) diff --git a/webapp/components/BadgeSelection.vue b/webapp/components/BadgeSelection.vue new file mode 100644 index 000000000..a6554d779 --- /dev/null +++ b/webapp/components/BadgeSelection.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/webapp/components/Badges.spec.js b/webapp/components/Badges.spec.js index d19c2beb2..ae15e0b0a 100644 --- a/webapp/components/Badges.spec.js +++ b/webapp/components/Badges.spec.js @@ -1,29 +1,114 @@ -import { shallowMount } from '@vue/test-utils' +import { render, screen, fireEvent } from '@testing-library/vue' import Badges from './Badges.vue' +const localVue = global.localVue + describe('Badges.vue', () => { - let propsData + const Wrapper = (propsData) => { + return render(Badges, { + propsData, + localVue, + }) + } - beforeEach(() => { - propsData = {} - }) - - describe('shallowMount', () => { - const Wrapper = () => { - return shallowMount(Badges, { propsData }) - } - - it('has class "hc-badges"', () => { - expect(Wrapper().find('.hc-badges').exists()).toBe(true) + describe('without badges', () => { + it('renders in presentation mode', () => { + const wrapper = Wrapper({ badges: [], selectionMode: false }) + expect(wrapper.container).toMatchSnapshot() }) - describe('given a badge', () => { + it('renders in selection mode', () => { + const wrapper = Wrapper({ badges: [], selectionMode: true }) + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('with badges', () => { + const badges = [ + { + id: '1', + icon: '/path/to/some/icon', + isDefault: false, + description: 'Some description', + }, + { + id: '2', + icon: '/path/to/another/icon', + isDefault: true, + description: 'Another description', + }, + { + id: '3', + icon: '/path/to/third/icon', + isDefault: false, + description: 'Third description', + }, + ] + + describe('in presentation mode', () => { + let wrapper + beforeEach(() => { - propsData.badges = [{ id: '1', icon: '/path/to/some/icon' }] + wrapper = Wrapper({ badges, scale: 1.2, selectionMode: false }) }) - it('proxies badge icon, which is just a URL without metadata', () => { - expect(Wrapper().find('img[src="/api/path/to/some/icon"]').exists()).toBe(true) + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('clicking on second badge does nothing', async () => { + const badge = screen.getByTitle(badges[1].description) + await fireEvent.click(badge) + expect(wrapper.emitted()).toEqual({}) + }) + }) + + describe('in selection mode', () => { + let wrapper + + beforeEach(() => { + wrapper = Wrapper({ badges, scale: 1.2, selectionMode: true }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('clicking on first badge does nothing', async () => { + const badge = screen.getByTitle(badges[0].description) + await fireEvent.click(badge) + expect(wrapper.emitted()).toEqual({}) + }) + + describe('clicking on second badge', () => { + beforeEach(async () => { + const badge = screen.getByTitle(badges[1].description) + await fireEvent.click(badge) + }) + + it('selects badge', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('emits badge-selected with index', async () => { + expect(wrapper.emitted()['badge-selected']).toEqual([[1]]) + }) + }) + + describe('clicking twice on second badge', () => { + beforeEach(async () => { + const badge = screen.getByTitle(badges[1].description) + await fireEvent.click(badge) + await fireEvent.click(badge) + }) + + it('deselects badge', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('emits badge-selected with null', async () => { + expect(wrapper.emitted()['badge-selected']).toEqual([[1], [null]]) + }) }) }) }) diff --git a/webapp/components/Badges.vue b/webapp/components/Badges.vue index d569452c7..0fdf065c3 100644 --- a/webapp/components/Badges.vue +++ b/webapp/components/Badges.vue @@ -1,69 +1,173 @@ diff --git a/webapp/components/CommentCard/CommentCard.story.js b/webapp/components/CommentCard/CommentCard.story.js index 75078657e..9a5cff436 100644 --- a/webapp/components/CommentCard/CommentCard.story.js +++ b/webapp/components/CommentCard/CommentCard.story.js @@ -32,8 +32,8 @@ const comment = { location: null, badges: [ { - id: 'indiegogo_en_bear', - icon: '/img/badges/indiegogo_en_bear.svg', + id: 'trophy_bear', + icon: '/img/badges/trophy_blue_bear.svg', __typename: 'Badge', }, ], diff --git a/webapp/components/LoginButton/LoginButton.vue b/webapp/components/LoginButton/LoginButton.vue index 39f0fa4ae..c2c154f34 100644 --- a/webapp/components/LoginButton/LoginButton.vue +++ b/webapp/components/LoginButton/LoginButton.vue @@ -40,7 +40,7 @@ export default { background-color: $color-neutral-90; } .login-link { - color: $text-color-base; + color: $text-color-link; padding-top: $space-xx-small; &:hover { color: $text-color-link-active; diff --git a/webapp/components/PostTeaser/PostTeaser.story.js b/webapp/components/PostTeaser/PostTeaser.story.js index e77e85585..41db98c91 100644 --- a/webapp/components/PostTeaser/PostTeaser.story.js +++ b/webapp/components/PostTeaser/PostTeaser.story.js @@ -33,8 +33,8 @@ export const post = { badges: [ { id: 'b4', - key: 'indiegogo_en_bear', - icon: '/img/badges/indiegogo_en_bear.svg', + key: 'trophy_bear', + icon: '/img/badges/trophy_blue_bear.svg', __typename: 'Badge', }, ], diff --git a/webapp/components/PostTeaser/PostTeaser.vue b/webapp/components/PostTeaser/PostTeaser.vue index ad43a9d31..32a07d5a3 100644 --- a/webapp/components/PostTeaser/PostTeaser.vue +++ b/webapp/components/PostTeaser/PostTeaser.vue @@ -72,22 +72,34 @@ import gql from 'graphql-tag' -import CONSTANTS_REGISTRATION from './../../constants/registration' +import registrationConstants from '~/constants/registration' export const isValidInviteCodeQuery = gql` query ($code: ID!) { @@ -43,11 +43,11 @@ export default { formSchema: { inviteCode: { type: 'string', - min: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH, - max: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH, + min: registrationConstants.INVITE_CODE_LENGTH, + max: registrationConstants.INVITE_CODE_LENGTH, required: true, message: this.$t('components.registration.invite-code.form.validations.length', { - inviteCodeLength: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH, + inviteCodeLength: registrationConstants.INVITE_CODE_LENGTH, }), }, }, diff --git a/webapp/components/Registration/RegistrationSlideNonce.vue b/webapp/components/Registration/RegistrationSlideNonce.vue index d7bda28d6..844298611 100644 --- a/webapp/components/Registration/RegistrationSlideNonce.vue +++ b/webapp/components/Registration/RegistrationSlideNonce.vue @@ -29,7 +29,7 @@ + + diff --git a/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap b/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap new file mode 100644 index 000000000..a78f44edc --- /dev/null +++ b/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Admin/BadgesSection with badges renders 1`] = ` + +
+
+

+ +

+ +
+ + +
+
+
+ +`; + +exports[`Admin/BadgesSection without badges renders 1`] = ` + +
+
+

+ +

+ +
+ + admin.badges.noBadges + +
+
+
+ +`; diff --git a/webapp/components/_new/generic/BaseCard/BaseCard.vue b/webapp/components/_new/generic/BaseCard/BaseCard.vue index 86b1a6652..e6fcd5e49 100644 --- a/webapp/components/_new/generic/BaseCard/BaseCard.vue +++ b/webapp/components/_new/generic/BaseCard/BaseCard.vue @@ -64,7 +64,7 @@ export default { } &.--highlight { - border: $border-size-base solid $color-warning; + border: $border-size-base solid $color-ribbon-announcement; } &.--wide-content { diff --git a/webapp/config/index.js b/webapp/config/index.js index 5da17010b..fb275a8ec 100644 --- a/webapp/config/index.js +++ b/webapp/config/index.js @@ -35,6 +35,7 @@ const options = { COOKIE_EXPIRE_TIME: process.env.COOKIE_EXPIRE_TIME || 730, // Two years by default COOKIE_HTTPS_ONLY: process.env.COOKIE_HTTPS_ONLY || process.env.NODE_ENV === 'production', // ensure true in production if not set explicitly CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false, + BADGES_ENABLED: process.env.BADGES_ENABLED === 'true' || false, } const CONFIG = { diff --git a/webapp/constants/badges.js b/webapp/constants/badges.js new file mode 100644 index 000000000..bccebb39a --- /dev/null +++ b/webapp/constants/badges.js @@ -0,0 +1,2 @@ +// this file is duplicated in `backend/src/constants/badges` and `webapp/constants/badges.js` +export const TROPHY_BADGES_SELECTED_MAX = 9 diff --git a/webapp/constants/chat.js b/webapp/constants/chat.js index c278dfd62..d471b85b8 100644 --- a/webapp/constants/chat.js +++ b/webapp/constants/chat.js @@ -44,9 +44,9 @@ const STYLE = { }, sidemenu: { - background: '#fff', - backgroundHover: '#f6f6f6', - backgroundActive: styleData.colorPrimaryLight, + background: styleData.chatSidemenuBg, + backgroundHover: styleData.chatSidemenuBackgroundOver, + backgroundActive: styleData.chatSidemenuBackgroundActive, colorActive: '#1976d2', borderColorSearch: '#e1e5e8', }, @@ -114,12 +114,12 @@ const STYLE = { }, emoji: { - background: '#fff', + background: 'white', }, icons: { search: '#9ca6af', - add: styleData.colorPrimary, + add: styleData.chatIconAdd, toggle: styleData.colorNeutral30, menu: styleData.colorNeutral30, close: '#9ca6af', @@ -128,9 +128,9 @@ const STYLE = { paperclip: styleData.colorPrimary, closeOutline: '#000', closePreview: '#fff', - send: styleData.colorPrimary, + send: styleData.chatIconSend, sendDisabled: '#9ca6af', - emoji: styleData.colorPrimary, + emoji: styleData.chatIconEmoji, emojiReaction: 'rgba(0, 0, 0, 0.3)', document: styleData.colorPrimary, pencil: '#9e9e9e', diff --git a/webapp/constants/login.js b/webapp/constants/login.js new file mode 100644 index 000000000..b1c6ea436 --- /dev/null +++ b/webapp/constants/login.js @@ -0,0 +1 @@ +export default {} diff --git a/webapp/constants/loginBranded.js b/webapp/constants/loginBranded.js new file mode 100644 index 000000000..41f4cafdc --- /dev/null +++ b/webapp/constants/loginBranded.js @@ -0,0 +1,8 @@ +import { merge } from 'lodash' +import login from '~/constants/login.js' + +const defaultLogin = { + LAYOUT: 'no-header', +} + +export default merge(defaultLogin, login) diff --git a/webapp/constants/registration.js b/webapp/constants/registration.js index 9e63e478e..8ebb40573 100644 --- a/webapp/constants/registration.js +++ b/webapp/constants/registration.js @@ -1,5 +1,2 @@ -// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js` -export default { - NONCE_LENGTH: 5, - INVITE_CODE_LENGTH: 6, -} +// this file is duplicated in `backend/src/config/registration.ts` and `webapp/constants/registration.js` +export default {} diff --git a/webapp/constants/registrationBranded.js b/webapp/constants/registrationBranded.js new file mode 100644 index 000000000..8082a41e4 --- /dev/null +++ b/webapp/constants/registrationBranded.js @@ -0,0 +1,12 @@ +// this file is duplicated in `backend/src/config/registrationBranded.ts` and `webapp/constants/registrationBranded.js` +import { merge } from 'lodash' + +import registration from '~/constants/registration.js' + +const defaultRegistration = { + NONCE_LENGTH: 5, + INVITE_CODE_LENGTH: 6, + LAYOUT: 'no-header', +} + +export default merge(defaultRegistration, registration) diff --git a/webapp/graphql/CommentMutations.js b/webapp/graphql/CommentMutations.js index dd00527be..f413a4496 100644 --- a/webapp/graphql/CommentMutations.js +++ b/webapp/graphql/CommentMutations.js @@ -29,7 +29,7 @@ export default (i18n) => { commentedCount followedByCount followedByCurrentUser - badges { + badgeTrophies { id icon } diff --git a/webapp/graphql/Fragments.js b/webapp/graphql/Fragments.js index 32337230b..77af830e8 100644 --- a/webapp/graphql/Fragments.js +++ b/webapp/graphql/Fragments.js @@ -26,9 +26,15 @@ export const locationFragment = (lang) => gql` export const badgesFragment = gql` fragment badges on User { - badges { + badgeTrophiesSelected { id icon + description + } + badgeVerification { + id + icon + description } } ` diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index 8ad247ad1..75342ef2a 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -90,6 +90,23 @@ export const adminUserQuery = () => { ` } +export const adminUserBadgesQuery = () => { + return gql` + query User($id: ID!) { + User(id: $id) { + id + name + badgeTrophies { + id + } + badgeVerification { + id + } + } + } + ` +} + export const mapUserQuery = (i18n) => { const lang = i18n.locale().toUpperCase() return gql` @@ -388,6 +405,22 @@ export const currentUserQuery = gql` query { currentUser { ...user + badgeTrophiesSelected { + id + icon + description + isDefault + } + badgeTrophiesUnused { + id + icon + description + } + badgeVerification { + id + icon + description + } email role about @@ -449,3 +482,43 @@ export const userDataQuery = (i18n) => { } ` } + +export const setTrophyBadgeSelected = gql` + mutation ($slot: Int!, $badgeId: ID) { + setTrophyBadgeSelected(slot: $slot, badgeId: $badgeId) { + badgeTrophiesCount + badgeTrophiesSelected { + id + icon + description + isDefault + } + badgeTrophiesUnused { + id + icon + description + } + badgeTrophiesUnusedCount + } + } +` + +export const resetTrophyBadgesSelected = gql` + mutation { + resetTrophyBadgesSelected { + badgeTrophiesCount + badgeTrophiesSelected { + id + icon + description + isDefault + } + badgeTrophiesUnused { + id + icon + description + } + badgeTrophiesUnusedCount + } + } +` diff --git a/webapp/graphql/admin/Badges.js b/webapp/graphql/admin/Badges.js new file mode 100644 index 000000000..2c037f2f3 --- /dev/null +++ b/webapp/graphql/admin/Badges.js @@ -0,0 +1,54 @@ +import gql from 'graphql-tag' + +export const queryBadges = () => gql` + query { + Badge { + id + type + icon + description + } + } +` + +export const setVerificationBadge = () => gql` + mutation ($badgeId: ID!, $userId: ID!) { + setVerificationBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + } + badgeTrophies { + id + } + } + } +` + +export const rewardTrophyBadge = () => gql` + mutation ($badgeId: ID!, $userId: ID!) { + rewardTrophyBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + } + badgeTrophies { + id + } + } + } +` + +export const revokeBadge = () => gql` + mutation ($badgeId: ID!, $userId: ID!) { + revokeBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + } + badgeTrophies { + id + } + } + } +` diff --git a/webapp/jsconfig.json b/webapp/jsconfig.json index 7e3695e4e..98874805a 100644 --- a/webapp/jsconfig.json +++ b/webapp/jsconfig.json @@ -3,11 +3,20 @@ "baseUrl": ".", "paths": { "~/*": [ - "./*" + "*" ], "~*": [ + "*" + ], + "~@": [ + "*" + ], + "@": [ + "*" + ], + "@@/*": [ "./*" ], } } -} \ No newline at end of file +} diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 19d0896a9..df050b191 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -10,6 +10,29 @@ "saveCategories": "Themen speichern" }, "admin": { + "badges": { + "description": "Stelle die verfügbaren Auszeichnungen für diesen Nutzer ein.", + "noBadges": "Keine Auszeichnungen vorhanden.", + "revokeTrophy": { + "error": "Trophäe konnte nicht widerrufen werden!", + "success": "Trophäe erfolgreich widerrufen" + }, + "revokeVerification": { + "error": "Verifizierung konnte nicht gesetzt werden!", + "success": "Verifizierung erfolgreich widerrufen" + }, + "rewardTrophy": { + "error": "Trophäe konnte nicht vergeben werden!", + "success": "Trophäe erfolgreich vergeben!" + }, + "setVerification": { + "error": "Verifizierung konnte nicht gesetzt werden!", + "success": "Verifizierung erfolgreich gesetzt" + }, + "title": "Auszeichnungen", + "trophyBadges": "Trophäen", + "verificationBadges": "Verifizierungen" + }, "categories": { "categoryName": "Name", "name": "Themen", @@ -68,6 +91,7 @@ "roleChanged": "Rolle erfolgreich geändert!", "table": { "columns": { + "badges": "Auszeichnungen", "createdAt": "Erstellt am", "email": "E-Mail", "name": "Name", @@ -242,10 +266,10 @@ } }, "contribution": { - "amount-clicks": "{amount} clicks", - "amount-comments": "{amount} comments", - "amount-shouts": "{amount} recommendations", - "amount-views": "{amount} views", + "amount-clicks": "{amount} Klicks", + "amount-comments": "{amount} Kommentare", + "amount-shouts": "{amount} Empfehlungen", + "amount-views": "{amount} Aurufe", "categories": { "infoSelectedNoOfMaxCategories": "{chosen} von {max} Themen ausgewählt" }, @@ -593,7 +617,7 @@ "tooltip": "Landkarte" }, "notifications": { - "tooltip": "Banachrichtigungen" + "tooltip": "Benachrichtigungen" } }, "index": { @@ -933,6 +957,16 @@ "title": "Suchergebnisse" }, "settings": { + "badges": { + "click-to-select": "Klicke auf einen freien Platz, um eine Badge hinzufügen.", + "click-to-use": "Klicke auf eine Badge, um sie zu platzieren.", + "description": "Hier hast du die Möglichkeit zu entscheiden, wie deine bereits erworbenen Badges in deinem Profil gezeigt werden sollen.", + "name": "Badges", + "no-badges-available": "Im Moment stehen dir keine Badges zur Verfügung, die du hinzufügen könntest.", + "remove": "Badge entfernen", + "success-update": "Deine Badges wurden erfolgreich gespeichert.", + "verification": "Dies ist deine Verifikations-Badge und kann nicht geändert werden." + }, "blocked-users": { "block": "Nutzer blockieren", "columns": { @@ -1047,20 +1081,20 @@ "name": "Einstellungen", "notifications": { "chat": "Chat", - "chatMessage": "Nachricht erhalten während Abwesenheit", + "chatMessage": "Nachricht erhalten während Abwesenheit.", "checkAll": "Alle auswählen", - "commentOnObservedPost": "Kommentare zu beobachteten Beiträgen", - "followingUsers": "Ein Nutzer dem ich folge veröffentlichte einen neuen Beitrag", + "commentOnObservedPost": "Kommentare zu beobachteten Beiträgen.", + "followingUsers": "Ein Nutzer dem ich folge veröffentlichte einen neuen Beitrag.", "group": "Gruppen", - "groupMemberJoined": "Ein Mitglied ist deiner Gruppe beigetreten", - "groupMemberLeft": "Ein Mitglied hat deine Gruppe verlassen", - "groupMemberRemoved": "Du wurdest aus einer Gruppe entfernt", - "groupMemberRoleChanged": "Deine Rolle in einer Gruppe wurde geändert", - "mention": "Ich wurde erwähnt", - "name": "Benachrichtigungen per Email", + "groupMemberJoined": "Ein Mitglied ist deiner Gruppe beigetreten.", + "groupMemberLeft": "Ein Mitglied hat deine Gruppe verlassen.", + "groupMemberRemoved": "Du wurdest aus einer Gruppe entfernt.", + "groupMemberRoleChanged": "Deine Rolle in einer Gruppe wurde geändert oder du wurdest eingeladen.", + "mention": "Ich wurde erwähnt.", + "name": "Benachrichtigungen per E-Mail", "post": "Beiträge und Kommentare", - "postByFollowedUser": "Beitrag von einem Nutzer, dem ich folge", - "postInGroup": "Beitrag in einer Gruppe, die ich beobachte", + "postByFollowedUser": "Beitrag von einem Nutzer, dem ich folge.", + "postInGroup": "Beitrag in einer Gruppe, die ich beobachte.", "send-email-notifications": "Sende E-Mail-Benachrichtigungen", "success-update": "Benachrichtigungs-Einstellungen gespeichert!", "uncheckAll": "Alle abwählen" diff --git a/webapp/locales/en.json b/webapp/locales/en.json index b4c1125f3..ecd0ec18d 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -10,6 +10,29 @@ "saveCategories": "Save topics" }, "admin": { + "badges": { + "description": "Configure the available badges for this user", + "noBadges": "There are no badges available", + "revokeTrophy": { + "error": "Trophy could not be revoked!", + "success": "Trophy successfully revoked!" + }, + "revokeVerification": { + "error": "Verification could not be revoked!", + "success": "Verification succesfully revoked" + }, + "rewardTrophy": { + "error": "Trophy could not be rewarded!", + "success": "Trophy successfully rewarded!" + }, + "setVerification": { + "error": "Verification could not be set!", + "success": "Verification successfully set!" + }, + "title": "Badges", + "trophyBadges": "Trophies", + "verificationBadges": "Verifications" + }, "categories": { "categoryName": "Name", "name": "Topics", @@ -68,6 +91,7 @@ "roleChanged": "Role changed successfully!", "table": { "columns": { + "badges": "Badges", "createdAt": "Created at", "email": "E-mail", "name": "Name", @@ -242,10 +266,10 @@ } }, "contribution": { - "amount-clicks": "{amount} clicks", - "amount-comments": "{amount} comments", - "amount-shouts": "{amount} recommendations", - "amount-views": "{amount} views", + "amount-clicks": "{amount} Clicks", + "amount-comments": "{amount} Comments", + "amount-shouts": "{amount} Recommendations", + "amount-views": "{amount} Views", "categories": { "infoSelectedNoOfMaxCategories": "{chosen} of {max} topics selected" }, @@ -933,6 +957,16 @@ "title": "Search Results" }, "settings": { + "badges": { + "click-to-select": "Click on an empty space to add a badge.", + "click-to-use": "Click on a badge to use it in the selected slot.", + "description": "Here you can choose how to display your earned badges on your profile.", + "name": "Badges", + "no-badges-available": "You currently don't have any badges available to add.", + "remove": "Remove Badge", + "success-update": "Your badges have been updated successfully.", + "verification": "This is your verification badge and cannot be changed." + }, "blocked-users": { "block": "Block user", "columns": { @@ -1047,20 +1081,20 @@ "name": "Settings", "notifications": { "chat": "Chat", - "chatMessage": "Message received while absent", + "chatMessage": "Message received while absent.", "checkAll": "Check all", - "commentOnObservedPost": "Comments on observed posts", - "followingUsers": "User I follow published a new post", + "commentOnObservedPost": "Comments on observed posts.", + "followingUsers": "User I follow published a new post.", "group": "Groups", - "groupMemberJoined": "Member joined a group I own", - "groupMemberLeft": "Member left a group I own", - "groupMemberRemoved": "I was removed from a group", - "groupMemberRoleChanged": "My role in a group was changed", - "mention": "I was mentioned", - "name": "Email Notifications", + "groupMemberJoined": "Member joined a group I own.", + "groupMemberLeft": "Member left a group I own.", + "groupMemberRemoved": "I was removed from a group.", + "groupMemberRoleChanged": "Your role in a group changed or you were invited.", + "mention": "I was mentioned.", + "name": "E-Mail Notifications", "post": "Posts and comments", - "postByFollowedUser": "Posts by users I follow", - "postInGroup": "Post in a group I am a member of", + "postByFollowedUser": "Posts by users I follow.", + "postInGroup": "Post in a group I am a member of.", "send-email-notifications": "Send e-mail notifications", "success-update": "Notifications settings saved!", "uncheckAll": "Uncheck all" @@ -1123,7 +1157,7 @@ "bank": "bank account", "code-of-conduct": "Code of Conduct", "contact": "Contact", - "data-privacy": "Data privacy", + "data-privacy": "Data Privacy", "donate": "Donate", "error-occurred": "An error occurred.", "faq": "FAQ", @@ -1132,7 +1166,7 @@ "made": "Made with ❤️", "register": "Registry number", "support": "Support", - "termsAndConditions": "Terms and conditions", + "termsAndConditions": "Terms and Conditions", "thanks": "Thanks!" }, "termsAndConditions": { diff --git a/webapp/locales/es.json b/webapp/locales/es.json index 7184a327a..15096b9d8 100644 --- a/webapp/locales/es.json +++ b/webapp/locales/es.json @@ -10,6 +10,29 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "noBadges": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Nombre", "name": "Categorías", @@ -68,6 +91,7 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": "Creado el", "email": "Correo electrónico", "name": "Nombre", @@ -933,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": "Bloquear usuario", "columns": { diff --git a/webapp/locales/fr.json b/webapp/locales/fr.json index 851743e63..2da2a9801 100644 --- a/webapp/locales/fr.json +++ b/webapp/locales/fr.json @@ -10,6 +10,29 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "noBadges": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Nom", "name": "Catégories", @@ -68,6 +91,7 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": "Créé à", "email": "Mail", "name": "Nom", @@ -933,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": "Bloquer l'utilisateur", "columns": { diff --git a/webapp/locales/it.json b/webapp/locales/it.json index 0c693ca43..485abff3a 100644 --- a/webapp/locales/it.json +++ b/webapp/locales/it.json @@ -10,6 +10,29 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "noBadges": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Nome", "name": "Categorie", @@ -68,6 +91,7 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": null, "email": null, "name": null, @@ -933,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": null, "columns": { diff --git a/webapp/locales/nl.json b/webapp/locales/nl.json index 433adf8e8..40f9aca2e 100644 --- a/webapp/locales/nl.json +++ b/webapp/locales/nl.json @@ -10,6 +10,29 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "noBadges": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Naam", "name": "Categorieën", @@ -68,6 +91,7 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": null, "email": null, "name": null, @@ -933,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": null, "columns": { diff --git a/webapp/locales/pl.json b/webapp/locales/pl.json index c0ab9d09c..ee332b84b 100644 --- a/webapp/locales/pl.json +++ b/webapp/locales/pl.json @@ -10,6 +10,29 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "noBadges": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Nazwa", "name": "Kategorie", @@ -68,6 +91,7 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": null, "email": null, "name": null, @@ -933,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": null, "columns": { diff --git a/webapp/locales/pt.json b/webapp/locales/pt.json index 02f8fb2cc..54f9b5d99 100644 --- a/webapp/locales/pt.json +++ b/webapp/locales/pt.json @@ -10,6 +10,29 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "noBadges": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Nome", "name": "Categorias", @@ -68,6 +91,7 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": "Criado em", "email": "E-mail", "name": "Nome", @@ -933,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": "Bloquear usuário", "columns": { diff --git a/webapp/locales/ru.json b/webapp/locales/ru.json index ea0279450..4d2e2a357 100644 --- a/webapp/locales/ru.json +++ b/webapp/locales/ru.json @@ -10,6 +10,29 @@ "saveCategories": null }, "admin": { + "badges": { + "description": null, + "noBadges": null, + "revokeTrophy": { + "error": null, + "success": null + }, + "revokeVerification": { + "error": null, + "success": null + }, + "rewardTrophy": { + "error": null, + "success": null + }, + "setVerification": { + "error": null, + "success": null + }, + "title": null, + "trophyBadges": null, + "verificationBadges": null + }, "categories": { "categoryName": "Имя", "name": "Категории", @@ -68,6 +91,7 @@ "roleChanged": null, "table": { "columns": { + "badges": null, "createdAt": "Дата создания", "email": "Эл. почта", "name": "Имя", @@ -933,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": "Блокировать", "columns": { diff --git a/webapp/maintenance/source/package.json b/webapp/maintenance/source/package.json index 1aa029eb4..df76a77ca 100644 --- a/webapp/maintenance/source/package.json +++ b/webapp/maintenance/source/package.json @@ -1,6 +1,6 @@ { "name": "@ocelot-social/maintenance", - "version": "3.3.0", + "version": "3.4.0", "description": "Maintenance page for ocelot.social", "repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social", "author": "ocelot.social Community", diff --git a/webapp/nuxt.config.js b/webapp/nuxt.config.js index 9adacd4cc..263c3f149 100644 --- a/webapp/nuxt.config.js +++ b/webapp/nuxt.config.js @@ -102,6 +102,7 @@ export default { */ styleResources: { scss: [ + '~assets/_new/styles/uses.scss', styleguideStyles, '~assets/_new/styles/tokens.scss', '~assets/styles/imports/_branding.scss', @@ -260,6 +261,9 @@ export default { }, } + config.resolve.alias['~@'] = path.resolve(__dirname, '/') + config.resolve.alias['@@'] = path.resolve(__dirname, '/') + if (CONFIG.STYLEGUIDE_DEV) { config.resolve.alias['@@'] = path.resolve(__dirname, `${styleguidePath}/src/system`) config.module.rules.push({ diff --git a/webapp/package.json b/webapp/package.json index c56684325..574c94378 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social-webapp", - "version": "3.3.0", + "version": "3.4.0", "description": "ocelot.social Frontend", "repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social", "author": "ocelot.social Community", @@ -49,7 +49,7 @@ "nuxt": "~2.12.1", "nuxt-dropzone": "^1.0.4", "nuxt-env": "~0.1.0", - "sass": "^1.86.3", + "sass": "1.77.6", "stack-utils": "^2.0.3", "tippy.js": "^4.3.5", "tiptap": "~1.26.6", @@ -74,11 +74,12 @@ "@babel/core": "^7.25.8", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/preset-env": "^7.25.8", - "@faker-js/faker": "9.6.0", + "@faker-js/faker": "9.7.0", "@storybook/addon-a11y": "^8.0.8", "@storybook/addon-actions": "^5.3.21", "@storybook/addon-notes": "^5.3.18", "@storybook/vue": "~7.4.0", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/vue": "5", "@vue/cli-shared-utils": "~4.3.1", "@vue/eslint-config-prettier": "~6.0.0", diff --git a/webapp/pages/__snapshots__/settings.spec.js.snap b/webapp/pages/__snapshots__/settings.spec.js.snap new file mode 100644 index 000000000..6672e41af --- /dev/null +++ b/webapp/pages/__snapshots__/settings.spec.js.snap @@ -0,0 +1,427 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`settings.vue given badges are disabled renders 1`] = ` +
+
+
+

+ +

+
+ +
+ +
+ + +
+ + + +
+
+
+
+`; + +exports[`settings.vue given badges are enabled renders 1`] = ` +
+
+
+

+ +

+
+ +
+ +
+ + +
+ + + +
+
+
+
+`; diff --git a/webapp/pages/admin/users.spec.js b/webapp/pages/admin/users.spec.js deleted file mode 100644 index 43c51fb52..000000000 --- a/webapp/pages/admin/users.spec.js +++ /dev/null @@ -1,153 +0,0 @@ -import { mount } from '@vue/test-utils' -import Vuex from 'vuex' -import Users from './users.vue' - -const localVue = global.localVue - -const stubs = { - 'nuxt-link': true, -} - -describe('Users', () => { - let wrapper - let Wrapper - let getters - - const mocks = { - $t: jest.fn(), - $apollo: { - loading: false, - mutate: jest - .fn() - .mockRejectedValue({ message: 'Ouch!' }) - .mockResolvedValue({ - data: { - switchUserRole: { - id: 'user', - email: 'user@example.org', - name: 'User', - role: 'moderator', - slug: 'user', - }, - }, - }), - }, - $toast: { - error: jest.fn(), - success: jest.fn(), - }, - } - - describe('mount', () => { - getters = { - 'auth/isAdmin': () => true, - 'auth/user': () => { - return { id: 'admin' } - }, - } - - Wrapper = () => { - const store = new Vuex.Store({ getters }) - return mount(Users, { - mocks, - localVue, - store, - stubs, - }) - } - - it('renders', () => { - wrapper = Wrapper() - expect(wrapper.element.tagName).toBe('DIV') - }) - - describe('search', () => { - let searchAction - beforeEach(() => { - searchAction = (wrapper, { query }) => { - wrapper.find('input').setValue(query) - wrapper.find('form').trigger('submit') - return wrapper - } - }) - - describe('query looks like an email address', () => { - it('searches users for exact email address', async () => { - const wrapper = await searchAction(Wrapper(), { query: 'email@example.org' }) - expect(wrapper.vm.email).toEqual('email@example.org') - expect(wrapper.vm.filter).toBe(null) - }) - - it('email address is case-insensitive', async () => { - const wrapper = await searchAction(Wrapper(), { query: 'eMaiL@example.org' }) - expect(wrapper.vm.email).toEqual('email@example.org') - expect(wrapper.vm.filter).toBe(null) - }) - }) - - describe('query is just text', () => { - it('tries to find matching users by `name`, `slug` or `about`', async () => { - const wrapper = await searchAction(await Wrapper(), { query: 'Find me' }) - const expected = { - OR: [ - { name_contains: 'Find me' }, - { slug_contains: 'Find me' }, - { about_contains: 'Find me' }, - ], - } - expect(wrapper.vm.email).toBe(null) - expect(wrapper.vm.filter).toEqual(expected) - }) - }) - }) - - describe('change roles', () => { - beforeAll(() => { - wrapper = Wrapper() - wrapper.setData({ - User: [ - { - id: 'admin', - email: 'admin@example.org', - name: 'Admin', - role: 'admin', - slug: 'admin', - }, - { - id: 'user', - email: 'user@example.org', - name: 'User', - role: 'user', - slug: 'user', - }, - ], - userRoles: ['user', 'moderator', 'admin'], - }) - }) - - it('cannot change own role', () => { - const adminRow = wrapper.findAll('tr').at(1) - expect(adminRow.find('select').exists()).toBe(false) - }) - - it('changes the role of another user', () => { - const userRow = wrapper.findAll('tr').at(2) - userRow.findAll('option').at(1).setSelected() - expect(mocks.$apollo.mutate).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - id: 'user', - role: 'moderator', - }, - }), - ) - }) - - it('toasts a success message after role has changed', () => { - const userRow = wrapper.findAll('tr').at(2) - userRow.findAll('option').at(1).setSelected() - expect(mocks.$toast.success).toHaveBeenCalled() - }) - }) - }) -}) diff --git a/webapp/pages/admin/users/__snapshots__/_id.spec.js.snap b/webapp/pages/admin/users/__snapshots__/_id.spec.js.snap new file mode 100644 index 000000000..2c5ddc686 --- /dev/null +++ b/webapp/pages/admin/users/__snapshots__/_id.spec.js.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`.vue renders 1`] = ` + +
+
+
+
+
+

+ + User1 + - + admin.badges.title + +

+ +

+ admin.badges.description +

+
+ +
+
+

+ admin.badges.verificationBadges +

+ +
+ + +
+
+ +
+

+ admin.badges.trophyBadges +

+ +
+ + +
+
+ + +
+
+
+
+
+ +`; diff --git a/webapp/pages/admin/users/__snapshots__/index.spec.js.snap b/webapp/pages/admin/users/__snapshots__/index.spec.js.snap new file mode 100644 index 000000000..0fff4016b --- /dev/null +++ b/webapp/pages/admin/users/__snapshots__/index.spec.js.snap @@ -0,0 +1,867 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Users given badges are disabled renders 1`] = ` +
+
+

+ admin.users.name +

+ +
+
+
+
+ +
+
+ + + + + +
+ + +
+ + + +
+
+ +
+ +
+
+
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + admin.users.table.columns.number + + + + admin.users.table.columns.name + + + + admin.users.table.columns.email + + + + admin.users.table.columns.slug + + + + admin.users.table.columns.createdAt + + + + 🖉 + + + + 🗨 + + + + ❤ + + + + admin.users.table.columns.role + +
+ NaN. + + + + User + + + + + + user@example.org + + + + + + user + + + + + + + + + + + + + + + + + + + +
+ NaN. + + + + User + + + + + + user2@example.org + + + + + + user + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + +
+ + +
+
+`; + +exports[`Users given badges are enabled renders 1`] = ` +
+
+

+ admin.users.name +

+ +
+
+
+
+ +
+
+ + + + + +
+ + +
+ + + +
+
+ +
+ +
+
+
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + admin.users.table.columns.number + + + + admin.users.table.columns.name + + + + admin.users.table.columns.email + + + + admin.users.table.columns.slug + + + + admin.users.table.columns.createdAt + + + + 🖉 + + + + 🗨 + + + + ❤ + + + + admin.users.table.columns.role + + + + admin.users.table.columns.badges + +
+ NaN. + + + + User + + + + + + user@example.org + + + + + + user + + + + + + + + + + + + + + + + + + + + + + + +
+ NaN. + + + + User + + + + + + user2@example.org + + + + + + user + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + +
+ + +
+
+`; diff --git a/webapp/pages/admin/users/_id.spec.js b/webapp/pages/admin/users/_id.spec.js new file mode 100644 index 000000000..d38b13022 --- /dev/null +++ b/webapp/pages/admin/users/_id.spec.js @@ -0,0 +1,331 @@ +import { render, fireEvent, screen } from '@testing-library/vue' +import BadgesPage from './_id.vue' + +const localVue = global.localVue + +const availableBadges = [ + { + id: 'verification-badge-1', + icon: 'icon1', + type: 'verification', + description: 'description-v-1', + }, + { + id: 'verification-badge-2', + icon: 'icon2', + type: 'verification', + description: 'description-v-2', + }, + { + id: 'trophy-badge-1', + icon: 'icon3', + type: 'trophy', + description: 'description-t-1', + }, + { + id: 'trophy-badge-2', + icon: 'icon4', + type: 'trophy', + description: 'description-t-2', + }, +] + +const user = { + id: 'user1', + name: 'User1', + badgeVerification: { + id: 'verification-badge-1', + }, + badgeTrophies: [ + { + id: 'trophy-badge-2', + }, + ], +} + +describe('.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn((v) => v), + $apollo: { + User: { + query: jest.fn(), + }, + badges: { + query: jest.fn(), + }, + mutate: jest.fn(), + queries: { + Badge: { + loading: false, + }, + }, + }, + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + } + }) + const Wrapper = () => { + return render(BadgesPage, { + mocks, + localVue, + data: () => ({ + user, + badges: availableBadges, + }), + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.baseElement).toMatchSnapshot() + }) + + describe('after clicking an inactive verification badge', () => { + let button + beforeEach(() => { + button = screen.getByAltText(availableBadges[1].description) + }) + + describe('and successful server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockResolvedValue({ + data: { + setVerificationBadge: { + id: 'user1', + badgeVerification: { + id: availableBadges[1].id, + }, + badgeTrophies: [], + }, + }, + }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[1].id, + userId: 'user1', + }, + }) + }) + + it('shows success message', async () => { + expect(mocks.$toast.success).toHaveBeenCalledWith('admin.badges.setVerification.success') + }) + }) + + describe('and failed server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockRejectedValue({ message: 'Ouch!' }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[1].id, + userId: 'user1', + }, + }) + }) + + it('shows error message', async () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('admin.badges.setVerification.error') + }) + }) + + describe('after clicking an inactive trophy badge', () => { + let button + beforeEach(() => { + button = screen.getByAltText(availableBadges[2].description) + }) + + describe('and successful server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockResolvedValue({ + data: { + setTrophyBadge: { + id: 'user1', + badgeVerification: null, + badgeTrophies: [ + { + id: availableBadges[2].id, + }, + ], + }, + }, + }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[2].id, + userId: 'user1', + }, + }) + }) + + it('shows success message', async () => { + expect(mocks.$toast.success).toHaveBeenCalledWith('admin.badges.rewardTrophy.success') + }) + }) + + describe('and failed server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockRejectedValue({ message: 'Ouch!' }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[2].id, + userId: 'user1', + }, + }) + }) + + it('shows error message', async () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('admin.badges.rewardTrophy.error') + }) + }) + }) + + describe('after clicking an active verification badge', () => { + let button + beforeEach(() => { + button = screen.getByAltText(availableBadges[0].description) + }) + + describe('and successful server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockResolvedValue({ + data: { + setVerificationBadge: { + id: 'user1', + badgeVerification: null, + badgeTrophies: [], + }, + }, + }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[0].id, + userId: 'user1', + }, + }) + }) + + it('shows success message', async () => { + expect(mocks.$toast.success).toHaveBeenCalledWith( + 'admin.badges.revokeVerification.success', + ) + }) + }) + + describe('and failed server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockRejectedValue({ message: 'Ouch!' }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[0].id, + userId: 'user1', + }, + }) + }) + + it('shows error message', async () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('admin.badges.revokeVerification.error') + }) + }) + }) + }) + + describe('after clicking an active trophy badge', () => { + let button + beforeEach(() => { + button = screen.getByAltText(availableBadges[3].description) + }) + + describe('and successful server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockResolvedValue({ + data: { + setTrophyBadge: { + id: 'user1', + badgeVerification: null, + badgeTrophies: [ + { + id: availableBadges[3].id, + }, + ], + }, + }, + }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[3].id, + userId: 'user1', + }, + }) + }) + + it('shows success message', async () => { + expect(mocks.$toast.success).toHaveBeenCalledWith('admin.badges.revokeTrophy.success') + }) + }) + + describe('and failed server response', () => { + beforeEach(async () => { + mocks.$apollo.mutate.mockRejectedValue({ message: 'Ouch!' }) + await fireEvent.click(button) + }) + + it('calls the mutation', async () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + variables: { + badgeId: availableBadges[3].id, + userId: 'user1', + }, + }) + }) + + it('shows error message', async () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('admin.badges.revokeTrophy.error') + }) + }) + }) +}) diff --git a/webapp/pages/admin/users/_id.vue b/webapp/pages/admin/users/_id.vue new file mode 100644 index 000000000..a6c4dafaa --- /dev/null +++ b/webapp/pages/admin/users/_id.vue @@ -0,0 +1,166 @@ + + + diff --git a/webapp/pages/admin/users/index.spec.js b/webapp/pages/admin/users/index.spec.js new file mode 100644 index 000000000..85e8789b8 --- /dev/null +++ b/webapp/pages/admin/users/index.spec.js @@ -0,0 +1,189 @@ +import { mount } from '@vue/test-utils' +import Vuex from 'vuex' +import Users from './index.vue' + +const localVue = global.localVue + +const stubs = { + 'nuxt-link': true, +} + +describe('Users', () => { + let wrapper + + const mocks = { + $t: jest.fn((t) => t), + $apollo: { + loading: false, + mutate: jest + .fn() + .mockRejectedValue({ message: 'Ouch!' }) + .mockResolvedValue({ + data: { + switchUserRole: { + id: 'user', + email: 'user@example.org', + name: 'User', + role: 'moderator', + slug: 'user', + }, + }, + }), + }, + $toast: { + error: jest.fn(), + success: jest.fn(), + }, + } + + const getters = { + 'auth/isAdmin': () => true, + 'auth/user': () => { + return { id: 'admin' } + }, + } + + const Wrapper = () => { + const store = new Vuex.Store({ getters }) + return mount(Users, { + mocks, + localVue, + store, + stubs, + data: () => ({ + User: [ + { + id: 'user', + email: 'user@example.org', + name: 'User', + role: 'moderator', + slug: 'user', + }, + { + id: 'user2', + email: 'user2@example.org', + name: 'User', + role: 'moderator', + slug: 'user', + }, + ], + }), + }) + } + + describe('given badges are enabled', () => { + beforeEach(() => { + mocks.$env = { + BADGES_ENABLED: true, + } + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.element).toMatchSnapshot() + }) + }) + + describe('given badges are disabled', () => { + beforeEach(() => { + mocks.$env = { + BADGES_ENABLED: false, + } + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.element).toMatchSnapshot() + }) + }) + + describe('search', () => { + let searchAction + beforeEach(() => { + wrapper = Wrapper() + searchAction = (wrapper, { query }) => { + wrapper.find('input').setValue(query) + wrapper.find('form').trigger('submit') + return wrapper + } + }) + + describe('query looks like an email address', () => { + it('searches users for exact email address', async () => { + const wrapper = await searchAction(Wrapper(), { query: 'email@example.org' }) + expect(wrapper.vm.email).toEqual('email@example.org') + expect(wrapper.vm.filter).toBe(null) + }) + + it('email address is case-insensitive', async () => { + const wrapper = await searchAction(Wrapper(), { query: 'eMaiL@example.org' }) + expect(wrapper.vm.email).toEqual('email@example.org') + expect(wrapper.vm.filter).toBe(null) + }) + }) + + describe('query is just text', () => { + it('tries to find matching users by `name`, `slug` or `about`', async () => { + const wrapper = await searchAction(await Wrapper(), { query: 'Find me' }) + const expected = { + OR: [ + { name_contains: 'Find me' }, + { slug_contains: 'Find me' }, + { about_contains: 'Find me' }, + ], + } + expect(wrapper.vm.email).toBe(null) + expect(wrapper.vm.filter).toEqual(expected) + }) + }) + }) + + describe('change roles', () => { + beforeAll(() => { + wrapper = Wrapper() + wrapper.setData({ + User: [ + { + id: 'admin', + email: 'admin@example.org', + name: 'Admin', + role: 'admin', + slug: 'admin', + }, + { + id: 'user', + email: 'user@example.org', + name: 'User', + role: 'user', + slug: 'user', + }, + ], + userRoles: ['user', 'moderator', 'admin'], + }) + }) + + it('cannot change own role', () => { + const adminRow = wrapper.findAll('tr').at(1) + expect(adminRow.find('select').exists()).toBe(false) + }) + + it('changes the role of another user', () => { + const userRow = wrapper.findAll('tr').at(2) + userRow.findAll('option').at(1).setSelected() + expect(mocks.$apollo.mutate).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { + id: 'user', + role: 'moderator', + }, + }), + ) + }) + + it('toasts a success message after role has changed', () => { + const userRow = wrapper.findAll('tr').at(2) + userRow.findAll('option').at(1).setSelected() + expect(mocks.$toast.success).toHaveBeenCalled() + }) + }) +}) diff --git a/webapp/pages/admin/users.vue b/webapp/pages/admin/users/index.vue similarity index 91% rename from webapp/pages/admin/users.vue rename to webapp/pages/admin/users/index.vue index 44f162c77..0bd592bad 100644 --- a/webapp/pages/admin/users.vue +++ b/webapp/pages/admin/users/index.vue @@ -63,6 +63,16 @@ {{ scope.row.role }} + @@ -110,7 +120,7 @@ export default { currentUser: 'auth/user', }), fields() { - return { + const fields = { index: this.$t('admin.users.table.columns.number'), name: this.$t('admin.users.table.columns.name'), email: this.$t('admin.users.table.columns.email'), @@ -133,6 +143,15 @@ export default { align: 'right', }, } + + if (this.$env.BADGES_ENABLED) { + fields.badges = { + label: this.$t('admin.users.table.columns.badges'), + align: 'right', + } + } + + return fields }, }, apollo: { @@ -205,4 +224,8 @@ export default { .admin-users > .base-card:first-child { margin-bottom: $space-small; } + +.ds-table-col { + vertical-align: middle; +} diff --git a/webapp/pages/login.vue b/webapp/pages/login.vue index c90b29146..799957bea 100644 --- a/webapp/pages/login.vue +++ b/webapp/pages/login.vue @@ -1,16 +1,19 @@ + + diff --git a/webapp/pages/settings/embeds.vue b/webapp/pages/settings/embeds.vue index 53db65e5e..aee47dfe7 100644 --- a/webapp/pages/settings/embeds.vue +++ b/webapp/pages/settings/embeds.vue @@ -17,20 +17,29 @@ {{ $t('settings.embeds.status.change.question') }} - - {{ $t('settings.embeds.status.change.deny') }} - - - {{ $t('settings.embeds.status.change.allow') }} - - -

{{ $t('settings.embeds.info-description') }}

-
    -
  • - {{ provider.provider_name }}, - {{ provider.provider_url }} -
  • -
+ + + {{ $t('settings.embeds.status.change.deny') }} + + + {{ $t('settings.embeds.status.change.allow') }} + + +

{{ $t('settings.embeds.info-description') }}

+ +
    +
  • + + {{ provider.provider_name }}, + {{ provider.provider_url }} + +
  • +
+
@@ -93,3 +102,13 @@ export default { }, } + + diff --git a/webapp/pages/settings/notifications.vue b/webapp/pages/settings/notifications.vue index 36e0d9081..8a383dc4f 100644 --- a/webapp/pages/settings/notifications.vue +++ b/webapp/pages/settings/notifications.vue @@ -1,8 +1,13 @@ @@ -138,4 +145,7 @@ export default { .label { margin-left: $space-xx-small; } +button + button { + margin-left: $space-x-small; +} diff --git a/webapp/static/img/badges/stars.svg b/webapp/static/img/badges/stars.svg new file mode 100644 index 000000000..44d64a5f4 --- /dev/null +++ b/webapp/static/img/badges/stars.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 1c3212c89..e17834008 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@adobe/css-tools@^4.4.0": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.2.tgz#c836b1bd81e6d62cd6cdf3ee4948bcdce8ea79c8" + integrity sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A== + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -2454,10 +2459,10 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@faker-js/faker@9.6.0": - version "9.6.0" - resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.6.0.tgz#64235d20330b142eef3d1d1638ba56c083b4bf1d" - integrity sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw== +"@faker-js/faker@9.7.0": + version "9.7.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.7.0.tgz#1cf1fecfcad5e2da2332140bf3b5f23cc1c2a7f4" + integrity sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg== "@hapi/address@2.x.x": version "2.0.0" @@ -3423,95 +3428,6 @@ resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493" integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw== -"@parcel/watcher-android-arm64@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz#e32d3dda6647791ee930556aee206fcd5ea0fb7a" - integrity sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ== - -"@parcel/watcher-darwin-arm64@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz#0d9e680b7e9ec1c8f54944f1b945aa8755afb12f" - integrity sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw== - -"@parcel/watcher-darwin-x64@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz#f9f1d5ce9d5878d344f14ef1856b7a830c59d1bb" - integrity sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA== - -"@parcel/watcher-freebsd-x64@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz#2b77f0c82d19e84ff4c21de6da7f7d096b1a7e82" - integrity sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw== - -"@parcel/watcher-linux-arm-glibc@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz#92ed322c56dbafa3d2545dcf2803334aee131e42" - integrity sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA== - -"@parcel/watcher-linux-arm-musl@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz#cd48e9bfde0cdbbd2ecd9accfc52967e22f849a4" - integrity sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA== - -"@parcel/watcher-linux-arm64-glibc@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz#7b81f6d5a442bb89fbabaf6c13573e94a46feb03" - integrity sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA== - -"@parcel/watcher-linux-arm64-musl@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz#dcb8ff01077cdf59a18d9e0a4dff7a0cfe5fd732" - integrity sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q== - -"@parcel/watcher-linux-x64-glibc@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz#2e254600fda4e32d83942384d1106e1eed84494d" - integrity sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw== - -"@parcel/watcher-linux-x64-musl@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz#01fcea60fedbb3225af808d3f0a7b11229792eef" - integrity sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA== - -"@parcel/watcher-win32-arm64@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz#87cdb16e0783e770197e52fb1dc027bb0c847154" - integrity sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig== - -"@parcel/watcher-win32-ia32@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz#778c39b56da33e045ba21c678c31a9f9d7c6b220" - integrity sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA== - -"@parcel/watcher-win32-x64@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz#33873876d0bbc588aacce38e90d1d7480ce81cb7" - integrity sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw== - -"@parcel/watcher@^2.4.1": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.0.tgz#5c88818b12b8de4307a9d3e6dc3e28eba0dfbd10" - integrity sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ== - dependencies: - detect-libc "^1.0.3" - is-glob "^4.0.3" - micromatch "^4.0.5" - node-addon-api "^7.0.0" - optionalDependencies: - "@parcel/watcher-android-arm64" "2.5.0" - "@parcel/watcher-darwin-arm64" "2.5.0" - "@parcel/watcher-darwin-x64" "2.5.0" - "@parcel/watcher-freebsd-x64" "2.5.0" - "@parcel/watcher-linux-arm-glibc" "2.5.0" - "@parcel/watcher-linux-arm-musl" "2.5.0" - "@parcel/watcher-linux-arm64-glibc" "2.5.0" - "@parcel/watcher-linux-arm64-musl" "2.5.0" - "@parcel/watcher-linux-x64-glibc" "2.5.0" - "@parcel/watcher-linux-x64-musl" "2.5.0" - "@parcel/watcher-win32-arm64" "2.5.0" - "@parcel/watcher-win32-ia32" "2.5.0" - "@parcel/watcher-win32-x64" "2.5.0" - "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -4342,6 +4258,19 @@ lz-string "^1.5.0" pretty-format "^27.0.2" +"@testing-library/jest-dom@^6.6.3": + version "6.6.3" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz#26ba906cf928c0f8172e182c6fe214eb4f9f2bd2" + integrity sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA== + dependencies: + "@adobe/css-tools" "^4.4.0" + aria-query "^5.0.0" + chalk "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.6.3" + lodash "^4.17.21" + redent "^3.0.0" + "@testing-library/vue@5": version "5.9.0" resolved "https://registry.yarnpkg.com/@testing-library/vue/-/vue-5.9.0.tgz#d33c52ae89e076808abe622f70dcbccb1b5d080c" @@ -6147,6 +6076,11 @@ aria-query@5.1.3: dependencies: deep-equal "^2.0.5" +aria-query@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" + integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -6892,13 +6826,6 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -braces@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -7421,6 +7348,21 @@ cheerio@^1.0.0-rc.2: lodash "^4.15.0" parse5 "^3.0.1" +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.3.1, chokidar@^3.4.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -7440,28 +7382,6 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.3.1, chokidar@^3.4.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chokidar@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" - integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== - dependencies: - readdirp "^4.0.1" - chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" @@ -8333,6 +8253,11 @@ css-what@2.1, css-what@^2.1.2: resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + csscolorparser@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" @@ -8817,7 +8742,7 @@ detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= -detect-libc@^1.0.2, detect-libc@^1.0.3: +detect-libc@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= @@ -8896,6 +8821,11 @@ dom-accessibility-api@^0.5.9: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-converter@^0.2: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -10353,13 +10283,6 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - finalhandler@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -11812,10 +11735,10 @@ ignore@^5.1.1, ignore@^5.1.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== -immutable@^5.0.2: - version "5.0.3" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1" - integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw== +immutable@^4.0.0: + version "4.3.7" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" + integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== import-cwd@^2.0.0: version "2.1.0" @@ -14353,14 +14276,6 @@ micromatch@^4.0.2, micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -micromatch@^4.0.5: - version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -14718,11 +14633,6 @@ no-case@^2.2.0, no-case@^2.3.2: dependencies: lower-case "^1.1.1" -node-addon-api@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" - integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== - node-ask@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/node-ask/-/node-ask-1.0.1.tgz#caaa1076cc58e0364267a0903e3eadfac158396b" @@ -17246,11 +17156,6 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" -readdirp@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a" - integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -17866,16 +17771,14 @@ sass-resources-loader@^2.2.1: glob "^7.1.6" loader-utils "^2.0.0" -sass@^1.86.3: - version "1.86.3" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.86.3.tgz#0a0d9ea97cb6665e73f409639f8533ce057464c9" - integrity sha512-iGtg8kus4GrsGLRDLRBRHY9dNVA78ZaS7xr01cWnS7PEMQyFtTqBiyCrfpTYTZXRWM94akzckYjh8oADfFNTzw== +sass@1.77.6: + version "1.77.6" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.6.tgz#898845c1348078c2e6d1b64f9ee06b3f8bd489e4" + integrity sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q== dependencies: - chokidar "^4.0.0" - immutable "^5.0.2" + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" - optionalDependencies: - "@parcel/watcher" "^2.4.1" sax@^1.2.4, sax@~1.2.4: version "1.2.4" diff --git a/yarn.lock b/yarn.lock index 768fbb1a7..1e3983be3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1543,10 +1543,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz#81fd50d11e2c32b2d6241470e3185b70c7b30699" integrity sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg== -"@faker-js/faker@9.6.0": - version "9.6.0" - resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.6.0.tgz#64235d20330b142eef3d1d1638ba56c083b4bf1d" - integrity sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw== +"@faker-js/faker@9.7.0": + version "9.7.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.7.0.tgz#1cf1fecfcad5e2da2332140bf3b5f23cc1c2a7f4" + integrity sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg== "@fastify/busboy@^2.0.0": version "2.1.1" @@ -4074,10 +4074,10 @@ cypress-network-idle@^1.15.0: resolved "https://registry.yarnpkg.com/cypress-network-idle/-/cypress-network-idle-1.15.0.tgz#e249f08695a46f1ddce18a95d5293937f277cbb3" integrity sha512-8zU16zhc7S3nMl1NTEEcNsZYlJy/ZzP2zPTTrngGxyXH32Ipake/xfHLZsgrzeWCieiS2AVhQsakhWqFzO3hpw== -cypress@^14.3.0: - version "14.3.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-14.3.0.tgz#720923501ca0e371d344b810572cea58d5b50aec" - integrity sha512-rRfPl9Z0/CczuYybBEoLbDVuT1OGkhYaJ0+urRCshgiDRz6QnoA0KQIQnPx7MJ3zy+VCsbUU1pV74n+6cbJEdg== +cypress@^14.3.2: + version "14.3.2" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-14.3.2.tgz#04a6ea66c1715119ef41dda5851d75801cc1e226" + integrity sha512-n+yGD2ZFFKgy7I3YtVpZ7BcFYrrDMcKj713eOZdtxPttpBjCyw/R8dLlFSsJPouneGN7A/HOSRyPJ5+3/gKDoA== dependencies: "@cypress/request" "^3.0.8" "@cypress/xvfb" "^1.2.4"