diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20922c5fb5..f6f26e9922 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ env: CTF_SPACE_ID: ${{ secrets.CTF_SPACE_ID }} DOCKER_REPOSITORY: ${{ vars.DOCKER_REPOSITORY }} EUROPEANA_API_KEY: ${{ secrets.EUROPEANA_API_KEY }} - OAUTH_CLIENT: ${{ secrets.OAUTH_CLIENT }} + KEYCLOAK_CLIENT: ${{ secrets.KEYCLOAK_CLIENT }} jobs: annotate: diff --git a/.github/workflows/support/ci/.env b/.github/workflows/support/ci/.env index f7828657dd..d61a8ea004 100644 --- a/.github/workflows/support/ci/.env +++ b/.github/workflows/support/ci/.env @@ -9,7 +9,7 @@ CTF_GRAPHQL_ORIGIN=${CTF_GRAPHQL_ORIGIN} CTF_SPACE_ID=${CTF_SPACE_ID} ENABLE_JIRA_SERVICE_DESK_FEEDBACK_FORM=${ENABLE_JIRA_SERVICE_DESK_FEEDBACK_FORM} EUROPEANA_API_KEY=${EUROPEANA_API_KEY} -OAUTH_CLIENT=${OAUTH_CLIENT} +KEYCLOAK_CLIENT=${KEYCLOAK_CLIENT} PERCY_TOKEN=${PERCY_TOKEN} PERCY_BRANCH=${PERCY_BRANCH} PERCY_COMMIT=${PERCY_COMMIT} diff --git a/README.md b/README.md index e0b9b5d9cf..0a5274d41a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ or via ENV variables on your machine. Some core features such as authentication and editorial content require the relevant configuration options to be specified. In particular, pay attention to -the Europeana APIs, Contentful, Redis and oAuth sections in the example .env file. +the Europeana APIs, Contentful, Redis and Keycloak sections in the example .env file. ## Build diff --git a/package-lock.json b/package-lock.json index cb5e60c5bf..d8a735113f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7319,24 +7319,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/@nuxt/telemetry/node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/@nuxt/telemetry/node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -7733,26 +7715,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/@nuxtjs/auth": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@nuxtjs/auth/-/auth-4.9.1.tgz", - "integrity": "sha512-h5VZanq2+P47jq3t0EnsZv800cg/ufOPC6JqvcyeDFJM99p58jHSODAjDuePo3PrZxd8hovMk7zusU5lOHgjvQ==", - "dependencies": { - "@nuxtjs/axios": "^5.9.5", - "body-parser": "^1.19.0", - "consola": "^2.11.3", - "cookie": "^0.4.0", - "is-https": "^1.0.0", - "js-cookie": "^2.2.1", - "lodash": "^4.17.15", - "nanoid": "^2.1.11" - } - }, - "node_modules/@nuxtjs/auth/node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" - }, "node_modules/@nuxtjs/axios": { "version": "5.13.6", "resolved": "https://registry.npmjs.org/@nuxtjs/axios/-/axios-5.13.6.tgz", @@ -7793,27 +7755,6 @@ "node": ">=14.16" } }, - "node_modules/@nuxtjs/i18n/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@nuxtjs/i18n/node_modules/is-https": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-https/-/is-https-4.0.0.tgz", - "integrity": "sha512-FeMLiqf8E5g6SdiVJsPcNZX8k4h2fBs1wp5Bb6uaNxn58ufK1axBqQZdmAQsqh0t9BuwFObybrdVJh6MKyPlyg==" - }, - "node_modules/@nuxtjs/i18n/node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "engines": { - "node": ">=14" - } - }, "node_modules/@nuxtjs/i18n/node_modules/ufo": { "version": "0.8.6", "resolved": "https://registry.npmjs.org/ufo/-/ufo-0.8.6.tgz", @@ -10655,12 +10596,12 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.5", + "content-type": "~1.0.4", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -10668,7 +10609,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.2", + "raw-body": "2.5.1", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -10677,6 +10618,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -11266,9 +11215,9 @@ } }, "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "engines": { "node": ">= 0.8" } @@ -12471,14 +12420,6 @@ "node": ">= 0.8.0" } }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -13516,9 +13457,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "engines": { "node": ">= 0.6" } @@ -13546,6 +13487,14 @@ "cookie-universal": "^2.2.2" } }, + "node_modules/cookie-universal/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -15646,14 +15595,6 @@ "node": ">=8.6.0" } }, - "node_modules/elastic-apm-node/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/elastic-apm-node/node_modules/error-stack-parser": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", @@ -16871,37 +16812,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -16946,20 +16856,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -20617,9 +20513,9 @@ } }, "node_modules/is-https": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-https/-/is-https-1.0.0.tgz", - "integrity": "sha512-1adLLwZT9XEXjzhQhZxd75uxf0l+xI9uTSFaZeSESjL3E1eXSPpO+u5RcgqtzeZ1KCaNvtEwZSTO2P4U5erVqQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-https/-/is-https-4.0.0.tgz", + "integrity": "sha512-FeMLiqf8E5g6SdiVJsPcNZX8k4h2fBs1wp5Bb6uaNxn58ufK1axBqQZdmAQsqh0t9BuwFObybrdVJh6MKyPlyg==" }, "node_modules/is-in-browser": { "version": "1.1.3", @@ -22954,9 +22850,17 @@ } }, "node_modules/js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" }, "node_modules/js-stringify": { "version": "1.0.2", @@ -23355,6 +23259,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/keycloak-js": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-21.1.1.tgz", + "integrity": "sha512-Viyhf0SOpu2jM/A33vpigSCFLo8l4yg8lqzaGyxXoZ3nGO9lo68B2LwJBDtgpzqDUh8DK//yCOzdWuR2CT4keA==", + "dependencies": { + "base64-js": "^1.5.1", + "js-sha256": "^0.9.0" + } + }, "node_modules/keyv": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", @@ -25964,9 +25877,21 @@ "optional": true }, "node_modules/nanoid": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, "node_modules/nanomatch": { "version": "1.2.13", @@ -30705,23 +30630,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -31387,9 +31295,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -31400,6 +31308,14 @@ "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/rc9": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.1.tgz", @@ -42494,7 +42410,6 @@ "@nuxt/cli": "2.17.0", "@nuxt/core": "2.17.0", "@nuxt/vue-app": "2.17.0", - "@nuxtjs/auth": "^4.9.1", "@nuxtjs/axios": "^5.13.6", "@nuxtjs/i18n": "^7.2.0", "autosuggest-highlight": "^3.2.0", @@ -42511,6 +42426,7 @@ "elastic-apm-node": "^3.24.0", "express": "^4.17.1", "http-errors": "^2.0.0", + "keycloak-js": "^21.1.1", "lodash": "^4.17.21", "marked": "^4.0.10", "md5": "^2.3.0", diff --git a/packages/portal/.env.example b/packages/portal/.env.example index 92237ce439..284cb4eae0 100644 --- a/packages/portal/.env.example +++ b/packages/portal/.env.example @@ -63,20 +63,10 @@ PORTAL_BASE_URL=https://www.europeana.eu # EUROPEANA_THUMBNAIL_API_URL_PRIVATE= -# OAUTH Configuration settings for user authentication. -# The example values here are the defaults and don't need to be set, with exception of OAUTH_CLIENT. -# The Europeana auth endpoint may deny access based on the domain name of your app and OAUTH_CLIENT settings. -# To make full use of user/login features you'll need to have a compatible oAuth endpoint. -OAUTH_CLIENT="YOUR_CLIENT" -# Keycloak server -# OAUTH_ORIGIN=https://auth.europeana.eu -# Auth module config options for Keycloak -# OAUTH_REALM=europeana -# OAUTH_SCOPE=openid,profile,email,usersets -# OAUTH_RESPONSE_TYPE=code -# OAUTH_ACCESS_TYPE=online -# OAUTH_GRANT_TYPE=authorization_code -# OAUTH_TOKEN_TYPE=Bearer +# Keycloak configuration settings for user authentication & authorization +KEYCLOAK_CLIENT_ID=YOUR_CLIENT +# KEYCLOAK_REALM=europeana +# KEYCLOAK_URL=https://auth.europeana.eu/auth # Hotjar tracking ID and snippet version # HOTJAR_ID= diff --git a/packages/portal/nuxt.config.js b/packages/portal/nuxt.config.js index e4e045439e..3e6e4a82fb 100644 --- a/packages/portal/nuxt.config.js +++ b/packages/portal/nuxt.config.js @@ -42,20 +42,6 @@ export default { translateLocales: (process.env.APP_SEARCH_TRANSLATE_LOCALES || '').split(',') } }, - auth: { - strategies: { - keycloak: { - client_id: process.env.OAUTH_CLIENT, - origin: process.env.OAUTH_ORIGIN || 'https://auth.europeana.eu', - scope: (process.env.OAUTH_SCOPE || 'openid,profile,email,usersets').split(','), - realm: process.env.OAUTH_REALM || 'europeana', - response_type: process.env.OAUTH_RESPONSE_TYPE || 'code', - access_type: process.env.OAUTH_ACCESS_TYPE || 'online', - grant_type: process.env.OAUTH_GRANT_TYPE || 'authorization_code', - token_type: process.env.OAUTH_TOKEN_TYPE || 'Bearer' - } - } - }, axios: { baseURL: process.env.PORTAL_BASE_URL }, @@ -93,6 +79,11 @@ export default { id: process.env.HOTJAR_ID, sv: process.env.HOTJAR_SNIPPET_VERSION }, + keycloak: { + clientId: process.env.KEYCLOAK_CLIENT_ID, + realm: process.env.KEYCLOAK_REALM || 'europeana', + url: process.env.KEYCLOAK_URL || 'https://auth.europeana.eu/auth' + }, matomo: { host: process.env.MATOMO_HOST, siteId: process.env.MATOMO_SITE_ID, @@ -100,16 +91,6 @@ export default { delay: process.env.MATOMO_LOAD_WAIT_DELAY, retries: process.env.MATOMO_LOAD_WAIT_RETRIES } - }, - oauth: { - origin: process.env.OAUTH_ORIGIN, - realm: process.env.OAUTH_REALM, - client: process.env.OAUTH_CLIENT, - scope: process.env.OAUTH_SCOPE, - responseType: process.env.OAUTH_RESPONSE_TYPE, - accessType: process.env.OAUTH_ACCESS_TYPE, - grantType: process.env.OAUTH_GRANT_TYPE, - tokenType: process.env.OAUTH_TOKEN_TYPE } }, @@ -255,6 +236,8 @@ export default { '~/plugins/vue-matomo.client', '~/plugins/i18n/iso-locale', '~/plugins/hotjar.client', + '~/plugins/keycloak', + '~/plugins/apis', '~/plugins/error', '~/plugins/link', '~/plugins/axios.server', @@ -272,7 +255,6 @@ export default { '~/modules/axios-logger', '~/modules/query-sanitiser', '@nuxtjs/axios', - '@nuxtjs/auth', ['@nuxtjs/i18n', { locales: i18nLocales, baseUrl: ({ $config }) => $config.app.baseUrl, @@ -285,12 +267,6 @@ export default { silentFallbackWarn: true, dateTimeFormats: i18nDateTime }, - // Disable redirects to account pages - parsePages: false, - pages: { - 'account/callback': false, - 'account/logout': false - }, // Enable browser language detection to automatically redirect user // to their preferred language as they visit your app for the first time // Set to false to disable @@ -314,32 +290,6 @@ export default { 'cookie-universal-nuxt' ], - auth: { - // Redirect routes: 'callback' option for keycloak redirects, - // 'login' option for unauthorised redirection - // 'home' option for redirection after login - // no redirect on logout - redirect: { - login: '/account/login', - logout: false, - callback: '/account/callback', - home: '/account' - }, - fullPathRedirect: true, - strategies: { - local: false, - // Include oauth2 so that ~/plugins/authScheme can extend it - _oauth2: { - _scheme: 'oauth2' - }, - keycloak: { - _scheme: '~/plugins/authScheme' - } - }, - defaultStrategy: 'keycloak', - plugins: ['~/plugins/apis', '~/plugins/user-likes.client'] - }, - axios: { proxyHeadersIgnore: [ // module defaults diff --git a/packages/portal/package.json b/packages/portal/package.json index a866547ecd..d6c8e254df 100644 --- a/packages/portal/package.json +++ b/packages/portal/package.json @@ -21,7 +21,6 @@ "@nuxt/cli": "2.17.0", "@nuxt/core": "2.17.0", "@nuxt/vue-app": "2.17.0", - "@nuxtjs/auth": "^4.9.1", "@nuxtjs/axios": "^5.13.6", "@nuxtjs/i18n": "^7.2.0", "autosuggest-highlight": "^3.2.0", @@ -38,6 +37,7 @@ "elastic-apm-node": "^3.24.0", "express": "^4.17.1", "http-errors": "^2.0.0", + "keycloak-js": "^21.1.1", "lodash": "^4.17.21", "marked": "^4.0.10", "md5": "^2.3.0", diff --git a/packages/portal/src/components/generic/LanguageSelector.vue b/packages/portal/src/components/generic/LanguageSelector.vue index 4dbd8d48c7..b7b7f0a667 100644 --- a/packages/portal/src/components/generic/LanguageSelector.vue +++ b/packages/portal/src/components/generic/LanguageSelector.vue @@ -19,11 +19,12 @@ diff --git a/packages/portal/src/pages/account/index.vue b/packages/portal/src/pages/account/index.vue index c981f0341c..b716b57891 100644 --- a/packages/portal/src/pages/account/index.vue +++ b/packages/portal/src/pages/account/index.vue @@ -7,12 +7,12 @@

- @{{ loggedInUser && loggedInUser.preferred_username }} + @{{ loggedInUser && loggedInUser.username }}

{{ $t('account.editProfile') }} @@ -163,7 +163,6 @@ import { BNav } from 'bootstrap-vue'; import { mapState } from 'vuex'; - import keycloak from '../../mixins/keycloak'; import pageMetaMixin from '@/mixins/pageMeta'; import ItemPreviewCardGroup from '../../components/item/ItemPreviewCardGroup'; import UserSets from '../../components/user/UserSets'; @@ -182,7 +181,6 @@ }, mixins: [ - keycloak, pageMetaMixin ], @@ -190,7 +188,6 @@ data() { return { - loggedInUser: this.$store.state.auth.user, tabHashes: { likes: '#likes', publicGalleries: '#public-galleries', @@ -214,12 +211,13 @@ }; }, userIsEditor() { - return this.$auth.userHasClientRole('entities', 'editor') && - this.$auth.userHasClientRole('usersets', 'editor'); + return this.$store.getters['keycloak/userHasClientRole']('entities', 'editor') && + this.$store.getters['keycloak/userHasClientRole']('usersets', 'editor'); }, ...mapState({ likesId: state => state.set.likesId, likedItems: state => state.set.likedItems, + loggedInUser: state => state.keycloak.profile, curations: state => state.set.curations }), activeTab() { diff --git a/packages/portal/src/pages/account/login.vue b/packages/portal/src/pages/account/login.vue index cb870c873f..00a2fde77d 100644 --- a/packages/portal/src/pages/account/login.vue +++ b/packages/portal/src/pages/account/login.vue @@ -3,19 +3,14 @@ diff --git a/packages/portal/src/pages/account/logout.vue b/packages/portal/src/pages/account/logout.vue index f68e5daa0a..04f00f0e06 100644 --- a/packages/portal/src/pages/account/logout.vue +++ b/packages/portal/src/pages/account/logout.vue @@ -6,27 +6,11 @@ export default { name: 'AccountLogoutPage', - beforeRouteEnter(to, from, next) { - next(vm => { - const redirectPath = /^account___[a-z]{2}$/.test(from.name) ? `/${vm.$i18n.locale}` : from.fullPath; - vm.$auth.$storage.setUniversal('redirect', redirectPath); - }); - }, - layout: 'minimal', - created() { - this.$auth.$storage.setUniversal('portalLoggingOut', true); - }, - mounted() { - this.$auth.logout({ params: { 'ui_locales': this.$i18n.locale } }); - localStorage.setItem('logout-event', `logout-${Math.random()}`); - - const path = this.$auth.strategies.keycloak.options.end_session_endpoint; - const redirect = window.location.origin + this.$auth.$storage.getUniversal('redirect'); - - window.location.assign(`${path}?redirect_uri=${encodeURIComponent(redirect)}`); + localStorage['kc.logout'] = 'true'; + this.$keycloak.logout(); } }; diff --git a/packages/portal/src/pages/collections/_type/_.vue b/packages/portal/src/pages/collections/_type/_.vue index bad31fb0c8..69fa4b56e1 100644 --- a/packages/portal/src/pages/collections/_type/_.vue +++ b/packages/portal/src/pages/collections/_type/_.vue @@ -229,10 +229,10 @@ ['topic', 'organisation'].includes(this.collectionType); }, userIsEntitiesEditor() { - return this.$auth.userHasClientRole('entities', 'editor'); + return this.$store.getters['keycloak/userHasClientRole']('entities', 'editor'); }, userIsSetsEditor() { - return this.$auth.userHasClientRole('usersets', 'editor'); + return this.$store.getters['keycloak/userHasClientRole']('usersets', 'editor'); }, route() { return { diff --git a/packages/portal/src/pages/galleries/_.vue b/packages/portal/src/pages/galleries/_.vue index 0a2ed686a8..adf151c151 100644 --- a/packages/portal/src/pages/galleries/_.vue +++ b/packages/portal/src/pages/galleries/_.vue @@ -272,15 +272,15 @@ return this.set.creator && typeof this.set.creator === 'string' ? this.set.creator : this.set.creator.id; }, userIsOwner() { - return this.$auth.loggedIn && this.$auth.user && - this.setCreatorId?.endsWith(`/${this.$auth.user.sub}`); + return this.$store.state.keycloak.loggedIn && this.$store.state.keycloak.profile && + this.setCreatorId?.endsWith(`/${this.$store.state.keycloak.profile.id}`); }, userIsEntityEditor() { - return this.$auth.userHasClientRole('entities', 'editor') && - this.$auth.userHasClientRole('usersets', 'editor'); + return this.$store.getters['keycloak/userHasClientRole']('entities', 'editor') && + this.$store.getters['keycloak/userHasClientRole']('usersets', 'editor'); }, userIsPublisher() { - return this.$auth.userHasClientRole('usersets', 'publisher'); + return this.$store.getters['keycloak/userHasClientRole']('usersets', 'publisher'); }, userCanHandleRecommendations() { return this.userIsOwner || (this.setIsEntityBestItems && this.userIsEntityEditor); @@ -300,7 +300,7 @@ return this.set.type === 'EntityBestItemsSet'; }, displayRecommendations() { - return this.enableRecommendations && this.$auth.loggedIn && this.userCanHandleRecommendations; + return this.enableRecommendations && this.$store.state.keycloak.loggedIn && this.userCanHandleRecommendations; }, enableRecommendations() { if (this.setIsEntityBestItems) { diff --git a/packages/portal/src/plugins/authScheme.js b/packages/portal/src/plugins/authScheme.js deleted file mode 100644 index 8a4f68be67..0000000000 --- a/packages/portal/src/plugins/authScheme.js +++ /dev/null @@ -1,37 +0,0 @@ -// Custom Nuxt auth scheme extending oAuth2 scheme to support Nuxt runtime config - -// TODO: delete once auth module supports Nuxt runtime config -// @see https://github.com/nuxt-community/auth-module/issues/713 - -// When Nuxt is built, this custom auth plugin will end up in .nuxt/auth/schemes, -// as will @nuxtjs/auth/lib/schemes/oauth2.js if it's also a registered strategy -// in the auth module config (in nuxt.config.js). -import Oauth2Scheme from './oauth2'; - -const keycloakOpenIDConnectEndpoint = (method, { realm, origin }) => - `${origin}/auth/realms/${realm}/protocol/openid-connect/${method}`; - -export function userHasClientRole(client, role) { - return this.user?.resource_access?.[client]?.roles?.includes(role) || false; -} - -// Inspired by https://github.com/nuxt-community/auth-module/issues/713#issuecomment-724031930 -export default class RuntimeConfigurableOauth2Scheme extends Oauth2Scheme { - constructor($auth, options) { - const configOptions = { - ...options, - ...$auth.ctx?.$config?.auth?.strategies[options['_name']] - }; - - configOptions['authorization_endpoint'] = keycloakOpenIDConnectEndpoint('auth', configOptions); - configOptions['access_token_endpoint'] = keycloakOpenIDConnectEndpoint('token', configOptions); - configOptions['userinfo_endpoint'] = keycloakOpenIDConnectEndpoint('userinfo', configOptions); - configOptions['end_session_endpoint'] = keycloakOpenIDConnectEndpoint('logout', configOptions); - - if (typeof $auth.userHasClientRole !== 'function') { - $auth.userHasClientRole = userHasClientRole; - } - - super($auth, configOptions); - } -} diff --git a/packages/portal/src/plugins/europeana/auth.js b/packages/portal/src/plugins/europeana/auth.js deleted file mode 100644 index 9931138c6a..0000000000 --- a/packages/portal/src/plugins/europeana/auth.js +++ /dev/null @@ -1,104 +0,0 @@ -// @see https://github.com/nuxt-community/auth-module/blob/v4.9.1/lib/schemes/oauth2.js#L157-L201 -const refreshAccessToken = async({ $auth, $axios, redirect, route }, requestConfig) => { - let refreshAccessTokenResponse; - try { - refreshAccessTokenResponse = await $auth.request(refreshAccessTokenRequestOptions($auth)); - } catch { - // Refresh token is no longer valid; clear tokens and try again - $auth.logout(); - delete requestConfig.headers['Authorization']; - return $axios.request(requestConfig); - } - - if (!updateAccessToken($auth, requestConfig, refreshAccessTokenResponse)) { - // No new access token; redirect to login URL - return redirect($auth.options.redirect.login, { redirect: route.path }); - } - - updateRefreshToken($auth, refreshAccessTokenResponse); - - // Retry request with new access token - return $axios.request(requestConfig); -}; - -const updateRefreshToken = ($auth, refreshAccessTokenResponse) => { - const options = $auth.strategy.options; - - let newRefreshToken = refreshAccessTokenResponse[options.refresh_token_key]; - if (!newRefreshToken) { - return false; - } - - if (options.token_type) { - newRefreshToken = `${options.token_type} ${newRefreshToken}`; - } - - // Store refresh token - $auth.setRefreshToken($auth.strategy.name, newRefreshToken); - - return newRefreshToken; -}; - -const updateAccessToken = ($auth, requestConfig, refreshAccessTokenResponse) => { - const options = $auth.strategy.options; - - let newAccessToken = refreshAccessTokenResponse[options.token_key]; - if (!newAccessToken) { - return false; - } - - if (options.token_type) { - newAccessToken = `${options.token_type} ${newAccessToken}`; - } - - // Store token - $auth.setToken($auth.strategy.name, newAccessToken); - - // Set axios token - $auth.strategy._setToken(newAccessToken); // eslint-disable-line no-underscore-dangle - - delete requestConfig.headers['Authorization']; - - return newAccessToken; -}; - -const refreshAccessTokenRequestOptions = ($auth) => { - const refreshToken = $auth.getRefreshToken($auth.strategy.name); - const options = $auth.strategy.options; - // Nuxt Auth stores token type e.g. "Bearer " with token, but refresh_token - // grant does not need it; remove it before sending to OIDC. - const refreshTokenWithoutType = refreshToken.replace(new RegExp(`^${options.token_type} `), ''); - - return { - method: 'post', - url: options.access_token_endpoint, - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - data: new URLSearchParams({ - 'client_id': options.client_id, - 'refresh_token': refreshTokenWithoutType, - 'grant_type': 'refresh_token' - }).toString() - }; -}; - -export const keycloakResponseErrorHandler = (context, error) => { - if (error.response?.status === 401) { - return keycloakUnauthorizedResponseErrorHandler(context, error); - } else { - return Promise.reject(error); - } -}; - -const keycloakUnauthorizedResponseErrorHandler = ({ $auth, $axios, redirect, route }, error) => { - if ($auth.getRefreshToken($auth.strategy.name)) { - // User has previously logged in, and we have a refresh token, e.g. - // access token has expired - return refreshAccessToken({ $auth, $axios, redirect, route }, error.config); - } else { - // User has not already logged in, or we have no refresh token: - // redirect to OIDC login URL - return redirect($auth.options.redirect.login, { redirect: route.path }); - } -}; diff --git a/packages/portal/src/plugins/europeana/utils.js b/packages/portal/src/plugins/europeana/utils.js index ec8e71c129..b4038b2dfe 100644 --- a/packages/portal/src/plugins/europeana/utils.js +++ b/packages/portal/src/plugins/europeana/utils.js @@ -2,7 +2,6 @@ import axios from 'axios'; import qs from 'qs'; import locales from '../i18n/locales.js'; -import { keycloakResponseErrorHandler } from './auth.js'; export const createAxios = ({ id, baseURL, $axios } = {}, context = {}) => { const axiosOptions = axiosInstanceOptions({ id, baseURL }, context); @@ -20,9 +19,7 @@ export const createAxios = ({ id, baseURL, $axios } = {}, context = {}) => { export const createKeycloakAuthAxios = ({ id, baseURL, $axios }, context) => { const axiosInstance = createAxios({ id, baseURL, $axios }, context); - if (typeof axiosInstance.onResponseError === 'function') { - axiosInstance.onResponseError(error => keycloakResponseErrorHandler(context, error)); - } + context.$keycloak?.axios?.(axiosInstance); return axiosInstance; }; diff --git a/packages/portal/src/plugins/keycloak.js b/packages/portal/src/plugins/keycloak.js new file mode 100644 index 0000000000..071e2370e4 --- /dev/null +++ b/packages/portal/src/plugins/keycloak.js @@ -0,0 +1,211 @@ +// TODO: move to new workspace pkg? + +// docs: https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter +import Keycloak from 'keycloak-js'; + +const keycloakAxios = (ctx) => (axiosInstance) => { + axiosInstance.interceptors.request.use((requestConfig) => { + if (ctx.$keycloak.keycloak?.token) { + requestConfig.headers.authorization = `Bearer ${ctx.$keycloak.keycloak.token}`; + } + return requestConfig; + }); + + if (typeof axiosInstance.onResponseError === 'function') { + axiosInstance.onResponseError(error => keycloakResponseErrorHandler(ctx, error)); + } +}; + +const keycloakResponseErrorHandler = (ctx, error) => { + if (error.response?.status === 401) { + return keycloakUnauthorizedResponseErrorHandler(ctx, error); + } else { + return Promise.reject(error); + } +}; + +const keycloakUnauthorizedResponseErrorHandler = (ctx, error) => { + if (ctx.$keycloak.keycloak.refreshToken) { + // User has previously logged in, and we have a refresh token, e.g. + // access token has expired + return keycloakRefreshAccessToken(ctx, error.config); + } else { + // User has not already logged in, or we have no refresh token: + // redirect to OIDC login URL + return ctx.redirect('/account/login', { redirect: ctx.route.path }); + } +}; + +const keycloakRefreshAccessToken = async(ctx, requestConfig) => { + const updated = await ctx.$keycloak.keycloak.updateToken(-1); + if (updated) { + ctx.$cookies.set('kc.token', ctx.$keycloak.keycloak.token); + ctx.$cookies.set('kc.idToken', ctx.$keycloak.keycloak.idToken); + ctx.$cookies.set('kc.refreshToken', ctx.$keycloak.keycloak.refreshToken); + } else { + // Refresh token is no longer valid; clear tokens and try again in case it + // doesn't require auth anyway + ctx.$keycloak.keycloak.clearToken(); + } + + // Retry request with new access token + return ctx.$axios.request(requestConfig); +}; + +const storeModule = { + namespaced: true, + + state: () => ({ + loggedIn: false, + profile: {}, + resourceAccess: {} + }), + + mutations: { + setLoggedIn(state, value) { + state.loggedIn = value; + }, + + setProfile(state, value) { + state.profile = value; + }, + + setResourceAccess(state, value) { + state.resourceAccess = value; + } + }, + + getters: { + userHasClientRole: (state) => (client, role) => { + return state.resourceAccess[client]?.roles?.includes(role); + } + } +}; + +const plugin = (ctx) => ({ + // TODO: use this.keycloak.createLoginUrl instead + get accountUrl() { + const keycloakAccountUrl = new URL(ctx.$config.keycloak.url); + + keycloakAccountUrl.pathname = `${keycloakAccountUrl.pathname}/realms/${ctx.$config.keycloak.realm}/account`; + if (keycloakAccountUrl.pathname.startsWith('//')) { + keycloakAccountUrl.pathname = keycloakAccountUrl.pathname.slice(1); + } + + const referrerUri = new URL(ctx.$config.app.baseUrl); + referrerUri.pathname = ctx.route.path; + referrerUri.search = new URLSearchParams(ctx.route.query).toString(); + referrerUri.hash = ctx.route.hash; + + keycloakAccountUrl.search = new URLSearchParams({ + referrer: ctx.$config.keycloak.clientId, + 'referrer_uri': referrerUri.toString() + }).toString(); + + return keycloakAccountUrl.toString(); + }, + axios: keycloakAxios(ctx), + callback() { + let redirect = '/'; + + if (ctx.route.query.redirect?.startsWith('/')) { + redirect = ctx.route.query.redirect; + } + + ctx.app.router.replace(redirect); + }, + async init() { + try { + await this.keycloak.init({ + checkLoginIframe: false, + token: ctx.$cookies.get('kc.token'), + idToken: ctx.$cookies.get('kc.idToken'), + refreshToken: ctx.$cookies.get('kc.refreshToken') + }); + } catch (e) { + ctx.$cookies.remove('kc.token'); + ctx.$cookies.remove('kc.idToken'); + ctx.$cookies.remove('kc.refreshToken'); + await this.keycloak.init({ + checkLoginIframe: false + }); + } + + ctx.store.commit('keycloak/setLoggedIn', this.keycloak.authenticated); + + ctx.$cookies.set('kc.token', this.keycloak.token); + ctx.$cookies.set('kc.idToken', this.keycloak.idToken); + ctx.$cookies.set('kc.refreshToken', this.keycloak.refreshToken); + + if (this.keycloak.authenticated) { + const profile = await this.keycloak.loadUserProfile(); + ctx.store.commit('keycloak/setProfile', profile); + ctx.store.commit('keycloak/setResourceAccess', this.keycloak.resourceAccess); + } + }, + keycloak: process.client && new Keycloak(ctx.$config.keycloak), + login() { + this.keycloak.login({ + locale: ctx.i18n.locale, + redirectUri: this.loginRedirect + }); + }, + get loginRedirect() { + let redirectPath = ctx.localePath('/account'); + + if (ctx.route) { + if ((ctx.route.query?.redirect || '').startsWith('/')) { + redirectPath = ctx.route.query.redirect; + } else if (ctx.route.path === ctx.localePath('/account/login')) { + redirectPath = ctx.localePath('/account'); + } else { + redirectPath = ctx.route.fullPath; + } + } + + const redirectUrl = new URL(`${ctx.$config.app.baseUrl}${ctx.localePath('/account/callback')}`); + redirectUrl.searchParams.set('redirect', redirectPath); + + return redirectUrl.toString(); + }, + logout() { + this.keycloak.logout({ + redirectUri: this.logoutRedirect, + 'ui_locales': ctx.i18n.locale + }); + }, + get logoutRedirect() { + let redirectPath = ctx.localePath('/'); + + if ((ctx.route.query?.redirect || '').startsWith('/')) { + redirectPath = ctx.route.query.redirect; + } else if (ctx.route.fullPath) { + redirectPath = ctx.route.fullPath; + } + + const redirectUrl = new URL(`${ctx.$config.app.baseUrl}${ctx.localePath('/account/callback')}`); + redirectUrl.searchParams.set('redirect', redirectPath); + + return redirectUrl.toString(); + }, + get logoutRoute() { + let redirect = '/'; + if (!ctx.route.name.startsWith('account')) { + redirect = ctx.route.fullPath; + } + return { + name: 'account-logout', + query: { + redirect + } + }; + } +}); + +export default async(ctx, inject) => { + ctx.store.registerModule('keycloak', storeModule); + + ctx.store.commit('keycloak/setLoggedIn', !!ctx.$cookies.get('kc.token')); + + inject('keycloak', await plugin(ctx)); +}; diff --git a/packages/portal/src/plugins/oauth2.js b/packages/portal/src/plugins/oauth2.js deleted file mode 100644 index 7dbe84fdea..0000000000 --- a/packages/portal/src/plugins/oauth2.js +++ /dev/null @@ -1,5 +0,0 @@ -export default class Oauth2Scheme { - constructor() { - // Dummy class to support testing of ./authScheme.js - } -} diff --git a/packages/portal/src/plugins/user-likes.client.js b/packages/portal/src/plugins/user-likes.client.js deleted file mode 100644 index 6772c8c1b4..0000000000 --- a/packages/portal/src/plugins/user-likes.client.js +++ /dev/null @@ -1,12 +0,0 @@ -export default async({ $auth, store }) => { - if ($auth?.loggedIn) { - try { - // TODO: assess whether there is a more efficient way to do this with fewer - // API requests - await store.dispatch('set/setLikes'); - await store.dispatch('set/fetchLikes'); - } catch (e) { - // Don't cause everything to break if the Set API is down... - } - } -}; diff --git a/packages/portal/src/store/set.js b/packages/portal/src/store/set.js index 84a2a86371..cde48d4053 100644 --- a/packages/portal/src/store/set.js +++ b/packages/portal/src/store/set.js @@ -82,8 +82,8 @@ export default { async removeItem(ctx, { setId, itemId }) { await this.$apis.set.modifyItems('delete', setId, itemId); }, - async setLikes({ commit }) { - const likesId = await this.$apis.set.getLikes(this.$auth.user ? this.$auth.user.sub : null); + async setLikes({ commit, rootState }) { + const likesId = await this.$apis.set.getLikes(rootState.keycloak?.profile?.id || null); commit('setLikesId', likesId); }, async createLikes({ commit }) {