From 5e8f5fc34b47e9f60ad7dc2aebfe9e5699c44c1e Mon Sep 17 00:00:00 2001 From: JRaams <47593569+JRaams@users.noreply.github.com> Date: Sun, 14 Apr 2024 18:03:50 +0200 Subject: [PATCH 1/3] feat: basic multi tenancy support with extra token decode --- package.json | 1 + .../runtime/auth/api.session-verification.ts | 8 +- .../auth/plugin-authenticate-user.server.ts | 7 +- .../runtime/auth/plugin-user-token.server.ts | 2 +- pnpm-lock.yaml | 472 +++++++++++++++++- src/auth/index.ts | 24 + src/index.ts | 1 + src/server/auth.ts | 10 +- 8 files changed, 505 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 2336d985..b21b7a35 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ ], "license": "MIT", "dependencies": { + "jwt-decode": "^4.0.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/nuxt/src/runtime/auth/api.session-verification.ts b/packages/nuxt/src/runtime/auth/api.session-verification.ts index 6de46341..6cb3317a 100644 --- a/packages/nuxt/src/runtime/auth/api.session-verification.ts +++ b/packages/nuxt/src/runtime/auth/api.session-verification.ts @@ -10,6 +10,7 @@ import { import { ensureAdminApp } from 'vuefire/server' import { logger } from '../logging' import { useRuntimeConfig } from '#imports' +import { parseTenantFromFirebaseJwt } from 'vuefire' /** * Setups an API endpoint to be used by the client to mint a cookie based auth session. @@ -27,7 +28,12 @@ export default defineEventHandler(async (event) => { }, 'session-verification' ) - const adminAuth = getAdminAuth(adminApp) + + const tenant = parseTenantFromFirebaseJwt(token) + + const adminAuth = tenant + ? getAdminAuth(adminApp).tenantManager().authForTenant(tenant) + : getAdminAuth(adminApp) logger.debug(token ? 'Verifying the token' : 'Deleting the session cookie') const verifiedIdToken = token ? await adminAuth.verifyIdToken(token) : null diff --git a/packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts b/packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts index 5f7fdac6..101b6272 100644 --- a/packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts +++ b/packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts @@ -17,7 +17,6 @@ export default defineNuxtPlugin(async (nuxtApp) => { const event = useRequestEvent() const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp const firebaseAdminApp = nuxtApp.$firebaseAdminApp as AdminApp - const adminAuth = getAdminAuth(firebaseAdminApp) const auth = nuxtApp.$firebaseAuth as Auth const decodedToken = nuxtApp[ @@ -27,6 +26,12 @@ export default defineNuxtPlugin(async (nuxtApp) => { const uid = decodedToken?.uid + const tenant = decodedToken?.firebase?.tenant + + const adminAuth = tenant + ? getAdminAuth(firebaseAdminApp).tenantManager().authForTenant(tenant) + : getAdminAuth(firebaseAdminApp) + // this is also undefined if the user hasn't enabled the session cookie option if (uid) { // reauthenticate if the user is not the same (e.g. invalidated) diff --git a/packages/nuxt/src/runtime/auth/plugin-user-token.server.ts b/packages/nuxt/src/runtime/auth/plugin-user-token.server.ts index 5d8d56fd..07f602b1 100644 --- a/packages/nuxt/src/runtime/auth/plugin-user-token.server.ts +++ b/packages/nuxt/src/runtime/auth/plugin-user-token.server.ts @@ -12,7 +12,7 @@ export default defineNuxtPlugin(async (nuxtApp) => { const adminApp = nuxtApp.$firebaseAdminApp as AdminApp const decodedToken = await decodeSessionCookie( - getCookie(event, AUTH_COOKIE_NAME), + event && getCookie(event, AUTH_COOKIE_NAME), adminApp ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 192a5a7a..5dff1025 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + jwt-decode: + specifier: ^4.0.0 + version: 4.0.0 vue-demi: specifier: latest version: 0.14.7(vue@3.4.19) @@ -117,7 +120,7 @@ importers: version: 3.10.3(rollup@3.29.4) '@nuxt/test-utils': specifier: ^3.11.0 - version: 3.11.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19) + version: 3.11.0(h3@1.10.2)(rollup@3.29.4)(vite@5.1.4)(vue-router@4.3.0)(vue@3.4.19) eslint: specifier: ^8.56.0 version: 8.56.0 @@ -154,10 +157,10 @@ importers: devDependencies: '@nuxt/devtools': specifier: ^1.0.8 - version: 1.0.8(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.4) + version: 1.0.8(nuxt@3.10.3)(vite@5.1.4) nuxt: specifier: ^3.10.3 - version: 3.10.3(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.1.4) + version: 3.10.3(vite@5.1.4) playground: dependencies: @@ -200,7 +203,7 @@ importers: version: 5.3.3 unplugin-vue-router: specifier: ^0.7.0 - version: 0.7.0(rollup@3.29.4)(vue-router@4.3.0)(vue@3.4.19) + version: 0.7.0(vue-router@4.3.0)(vue@3.4.19) vite: specifier: ^5.1.4 version: 5.1.4(@types/node@20.11.20) @@ -2139,6 +2142,22 @@ packages: - supports-color dev: true + /@nuxt/devtools-kit@1.0.8(nuxt@3.10.3)(vite@5.1.4): + resolution: {integrity: sha512-j7bNZmoAXQ1a8qv6j6zk4c/aekrxYqYVQM21o/Hy4XHCUq4fajSgpoc8mjyWJSTfpkOmuLyEzMexpDWiIVSr6A==} + peerDependencies: + nuxt: ^3.9.0 + vite: '*' + dependencies: + '@nuxt/kit': 3.10.3 + '@nuxt/schema': 3.10.3 + execa: 7.2.0 + nuxt: 3.10.3(vite@5.1.4) + vite: 5.1.4(@types/node@20.11.20) + transitivePeerDependencies: + - rollup + - supports-color + dev: true + /@nuxt/devtools-wizard@1.0.8: resolution: {integrity: sha512-RxyOlM7Isk5npwXwDJ/rjm9ekX5sTNG0LS0VOBMdSx+D5nlRPMRr/r9yO+9WQDyzPLClLzHaXRHBWLPlRX3IMw==} hasBin: true @@ -2206,6 +2225,57 @@ packages: - utf-8-validate dev: true + /@nuxt/devtools@1.0.8(nuxt@3.10.3)(vite@5.1.4): + resolution: {integrity: sha512-o6aBFEBxc8OgVHV4OPe2g0q9tFIe9HiTxRiJnlTJ+jHvOQsBLS651ArdVtwLChf9UdMouFlpLLJ1HteZqTbtsQ==} + hasBin: true + peerDependencies: + nuxt: ^3.9.0 + vite: '*' + dependencies: + '@antfu/utils': 0.7.7 + '@nuxt/devtools-kit': 1.0.8(nuxt@3.10.3)(vite@5.1.4) + '@nuxt/devtools-wizard': 1.0.8 + '@nuxt/kit': 3.10.3 + birpc: 0.2.17 + consola: 3.2.3 + destr: 2.0.3 + error-stack-parser-es: 0.1.1 + execa: 7.2.0 + fast-glob: 3.3.2 + flatted: 3.3.1 + get-port-please: 3.1.2 + hookable: 5.5.3 + image-meta: 0.2.0 + is-installed-globally: 1.0.0 + launch-editor: 2.6.1 + local-pkg: 0.5.0 + magicast: 0.3.3 + nuxt: 3.10.3(vite@5.1.4) + nypm: 0.3.6 + ohash: 1.1.3 + pacote: 17.0.6 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.0.3 + rc9: 2.1.1 + scule: 1.3.0 + semver: 7.6.0 + simple-git: 3.22.0 + sirv: 2.0.4 + unimport: 3.7.1 + vite: 5.1.4(@types/node@20.11.20) + vite-plugin-inspect: 0.8.3(@nuxt/kit@3.10.3)(vite@5.1.4) + vite-plugin-vue-inspector: 4.0.2(vite@5.1.4) + which: 3.0.1 + ws: 8.16.0 + transitivePeerDependencies: + - bluebird + - bufferutil + - rollup + - supports-color + - utf-8-validate + dev: true + /@nuxt/eslint-config@0.2.0(eslint@8.56.0): resolution: {integrity: sha512-NeJX8TLcnNAjQFiDs3XhP+9CHKK8jaKsP7eUyCSrQdgY7nqWe7VJx64lwzx5FTT4cW3RHMEyH+Y0qzLGYYoa/A==} peerDependencies: @@ -2221,6 +2291,33 @@ packages: - supports-color dev: true + /@nuxt/kit@3.10.3: + resolution: {integrity: sha512-PUjYB9Mvx0qD9H1QZBwwtY4fLlCLET+Mm9BVqUOtXCaGoXd6u6BE4e/dGFPk2UEKkIcDGrUMSbqkHYvsEuK9NQ==} + engines: {node: ^14.18.0 || >=16.10.0} + dependencies: + '@nuxt/schema': 3.10.3 + c12: 1.9.0 + consola: 3.2.3 + defu: 6.1.4 + globby: 14.0.1 + hash-sum: 2.0.0 + ignore: 5.3.1 + jiti: 1.21.0 + knitwork: 1.0.0 + mlly: 1.6.0 + pathe: 1.1.2 + pkg-types: 1.0.3 + scule: 1.3.0 + semver: 7.6.0 + ufo: 1.4.0 + unctx: 2.3.1 + unimport: 3.7.1 + untyped: 1.4.2 + transitivePeerDependencies: + - rollup + - supports-color + dev: true + /@nuxt/kit@3.10.3(rollup@3.29.4): resolution: {integrity: sha512-PUjYB9Mvx0qD9H1QZBwwtY4fLlCLET+Mm9BVqUOtXCaGoXd6u6BE4e/dGFPk2UEKkIcDGrUMSbqkHYvsEuK9NQ==} engines: {node: ^14.18.0 || >=16.10.0} @@ -2267,6 +2364,26 @@ packages: - typescript dev: true + /@nuxt/schema@3.10.3: + resolution: {integrity: sha512-a4cYbeskEVBPazgAhvUGkL/j7ho/iPWMK3vCEm6dRMjSqHVEITRosrj0aMfLbRrDpTrMjlRs0ZitxiaUfE/p5Q==} + engines: {node: ^14.18.0 || >=16.10.0} + dependencies: + '@nuxt/ui-templates': 1.3.1 + consola: 3.2.3 + defu: 6.1.4 + hookable: 5.5.3 + pathe: 1.1.2 + pkg-types: 1.0.3 + scule: 1.3.0 + std-env: 3.7.0 + ufo: 1.4.0 + unimport: 3.7.1 + untyped: 1.4.2 + transitivePeerDependencies: + - rollup + - supports-color + dev: true + /@nuxt/schema@3.10.3(rollup@3.29.4): resolution: {integrity: sha512-a4cYbeskEVBPazgAhvUGkL/j7ho/iPWMK3vCEm6dRMjSqHVEITRosrj0aMfLbRrDpTrMjlRs0ZitxiaUfE/p5Q==} engines: {node: ^14.18.0 || >=16.10.0} @@ -2286,6 +2403,32 @@ packages: - rollup - supports-color + /@nuxt/telemetry@2.5.3: + resolution: {integrity: sha512-Ghv2MgWbJcUM9G5Dy3oQP0cJkUwEgaiuQxEF61FXJdn0a69Q4StZEP/hLF0MWPM9m6EvAwI7orxkJHM7MrmtVg==} + hasBin: true + dependencies: + '@nuxt/kit': 3.10.3 + ci-info: 4.0.0 + consola: 3.2.3 + create-require: 1.1.1 + defu: 6.1.4 + destr: 2.0.3 + dotenv: 16.4.5 + git-url-parse: 13.1.1 + is-docker: 3.0.0 + jiti: 1.21.0 + mri: 1.2.0 + nanoid: 4.0.2 + ofetch: 1.3.3 + parse-git-config: 3.0.0 + pathe: 1.1.2 + rc9: 2.1.1 + std-env: 3.7.0 + transitivePeerDependencies: + - rollup + - supports-color + dev: true + /@nuxt/telemetry@2.5.3(rollup@3.29.4): resolution: {integrity: sha512-Ghv2MgWbJcUM9G5Dy3oQP0cJkUwEgaiuQxEF61FXJdn0a69Q4StZEP/hLF0MWPM9m6EvAwI7orxkJHM7MrmtVg==} hasBin: true @@ -2312,7 +2455,7 @@ packages: - supports-color dev: true - /@nuxt/test-utils@3.11.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19): + /@nuxt/test-utils@3.11.0(h3@1.10.2)(rollup@3.29.4)(vite@5.1.4)(vue-router@4.3.0)(vue@3.4.19): resolution: {integrity: sha512-9ovgpQZkZpVg/MhYVVn2169WjH/IL0XUqwGryTa/lkx0/BCi1LMVEp3HTPkmt4qbRcxitO+kL4vFqqrFGVaSVg==} engines: {node: ^14.18.0 || >=16.10.0} peerDependencies: @@ -2351,7 +2494,6 @@ packages: dependencies: '@nuxt/kit': 3.10.3(rollup@3.29.4) '@nuxt/schema': 3.10.3(rollup@3.29.4) - '@vue/test-utils': 2.4.4(vue@3.4.19) c12: 1.8.0 consola: 3.2.3 defu: 6.1.4 @@ -2361,7 +2503,6 @@ packages: fake-indexeddb: 5.0.2 get-port-please: 3.1.2 h3: 1.10.2 - happy-dom: 13.4.1 local-pkg: 0.5.0 magic-string: 0.30.7 node-fetch-native: 1.6.2 @@ -2375,8 +2516,7 @@ packages: unenv: 1.9.0 unplugin: 1.7.1 vite: 5.1.4(@types/node@20.11.20) - vitest: 1.3.1(happy-dom@13.4.1) - vitest-environment-nuxt: 1.0.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19) + vitest-environment-nuxt: 1.0.0(h3@1.10.2)(rollup@3.29.4)(vite@5.1.4)(vue-router@4.3.0)(vue@3.4.19) vue: 3.4.19(typescript@5.3.3) vue-router: 4.3.0(vue@3.4.19) transitivePeerDependencies: @@ -2448,6 +2588,66 @@ packages: - vue-tsc dev: true + /@nuxt/vite-builder@3.10.3(vue@3.4.19): + resolution: {integrity: sha512-BqkbrYkEk1AVUJleofbqTRV+ltf2p1CDjGDK78zENPCgrSABlj4F4oK8rze8vmRY5qoH7kMZxgMa2dXVXCp6OA==} + engines: {node: ^14.18.0 || >=16.10.0} + peerDependencies: + vue: ^3.3.4 + dependencies: + '@nuxt/kit': 3.10.3 + '@rollup/plugin-replace': 5.0.5 + '@vitejs/plugin-vue': 5.0.4(vite@5.1.4)(vue@3.4.19) + '@vitejs/plugin-vue-jsx': 3.1.0(vite@5.1.4)(vue@3.4.19) + autoprefixer: 10.4.17(postcss@8.4.35) + clear: 0.1.0 + consola: 3.2.3 + cssnano: 6.0.4(postcss@8.4.35) + defu: 6.1.4 + esbuild: 0.20.1 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + externality: 1.0.2 + fs-extra: 11.2.0 + get-port-please: 3.1.2 + h3: 1.10.2 + knitwork: 1.0.0 + magic-string: 0.30.7 + mlly: 1.6.0 + ohash: 1.1.3 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.0.3 + postcss: 8.4.35 + rollup-plugin-visualizer: 5.12.0(rollup@3.29.4) + std-env: 3.7.0 + strip-literal: 2.0.0 + ufo: 1.4.0 + unenv: 1.9.0 + unplugin: 1.7.1 + vite: 5.1.4(@types/node@20.11.20) + vite-node: 1.3.1 + vite-plugin-checker: 0.6.4(eslint@8.56.0)(typescript@5.3.3)(vite@5.1.4) + vue: 3.4.19(typescript@5.3.3) + vue-bundle-renderer: 2.0.0 + transitivePeerDependencies: + - '@types/node' + - eslint + - less + - lightningcss + - meow + - optionator + - rollup + - sass + - stylelint + - stylus + - sugarss + - supports-color + - terser + - vls + - vti + - vue-tsc + dev: true + /@one-ini/wasm@0.1.1: resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} dev: true @@ -2791,6 +2991,19 @@ packages: rollup: 4.12.0 dev: true + /@rollup/plugin-replace@5.0.5: + resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.12.0) + magic-string: 0.30.7 + dev: true + /@rollup/plugin-replace@5.0.5(rollup@3.29.4): resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==} engines: {node: '>=14.0.0'} @@ -3586,6 +3799,26 @@ packages: - rollup dev: true + /@vue-macros/common@1.8.0(vue@3.4.19): + resolution: {integrity: sha512-auDJJzE0z3uRe3867e0DsqcseKImktNf5ojCZgUKqiVxb2yTlwlgOVAYCgoep9oITqxkXQymSvFeKhedi8PhaA==} + engines: {node: '>=16.14.0'} + peerDependencies: + vue: ^2.7.0 || ^3.2.25 + peerDependenciesMeta: + vue: + optional: true + dependencies: + '@babel/types': 7.23.9 + '@rollup/pluginutils': 5.1.0(rollup@4.12.0) + '@vue/compiler-sfc': 3.4.19 + ast-kit: 0.11.2 + local-pkg: 0.4.3 + magic-string-ast: 0.3.0 + vue: 3.4.19(typescript@5.3.3) + transitivePeerDependencies: + - rollup + dev: true + /@vue/babel-helper-vue-transform-on@1.2.1: resolution: {integrity: sha512-jtEXim+pfyHWwvheYwUwSXm43KwQo8nhOBDyjrUITV6X2tB7lJm6n/+4sqR8137UVZZul5hBzWHdZ2uStYpyRQ==} dev: true @@ -4103,6 +4336,17 @@ packages: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true + /ast-kit@0.11.2: + resolution: {integrity: sha512-Q0DjXK4ApbVoIf9GLyCo252tUH44iTnD/hiJ2TQaJeydYWSpKk0sI34+WMel8S9Wt5pbLgG02oJ+gkgX5DV3sQ==} + engines: {node: '>=16.14.0'} + dependencies: + '@babel/parser': 7.23.9 + '@rollup/pluginutils': 5.1.0(rollup@4.12.0) + pathe: 1.1.2 + transitivePeerDependencies: + - rollup + dev: true + /ast-kit@0.11.2(rollup@3.29.4): resolution: {integrity: sha512-Q0DjXK4ApbVoIf9GLyCo252tUH44iTnD/hiJ2TQaJeydYWSpKk0sI34+WMel8S9Wt5pbLgG02oJ+gkgX5DV3sQ==} engines: {node: '>=16.14.0'} @@ -4114,6 +4358,17 @@ packages: - rollup dev: true + /ast-kit@0.9.5: + resolution: {integrity: sha512-kbL7ERlqjXubdDd+szuwdlQ1xUxEz9mCz1+m07ftNVStgwRb2RWw+U6oKo08PAvOishMxiqz1mlJyLl8yQx2Qg==} + engines: {node: '>=16.14.0'} + dependencies: + '@babel/parser': 7.23.9 + '@rollup/pluginutils': 5.1.0(rollup@4.12.0) + pathe: 1.1.2 + transitivePeerDependencies: + - rollup + dev: true + /ast-kit@0.9.5(rollup@3.29.4): resolution: {integrity: sha512-kbL7ERlqjXubdDd+szuwdlQ1xUxEz9mCz1+m07ftNVStgwRb2RWw+U6oKo08PAvOishMxiqz1mlJyLl8yQx2Qg==} engines: {node: '>=16.14.0'} @@ -4125,6 +4380,16 @@ packages: - rollup dev: true + /ast-walker-scope@0.5.0: + resolution: {integrity: sha512-NsyHMxBh4dmdEHjBo1/TBZvCKxffmZxRYhmclfu0PP6Aftre47jOHYaYaNqJcV0bxihxFXhDkzLHUwHc0ocd0Q==} + engines: {node: '>=16.14.0'} + dependencies: + '@babel/parser': 7.23.9 + ast-kit: 0.9.5 + transitivePeerDependencies: + - rollup + dev: true + /ast-walker-scope@0.5.0(rollup@3.29.4): resolution: {integrity: sha512-NsyHMxBh4dmdEHjBo1/TBZvCKxffmZxRYhmclfu0PP6Aftre47jOHYaYaNqJcV0bxihxFXhDkzLHUwHc0ocd0Q==} engines: {node: '>=16.14.0'} @@ -7279,6 +7544,11 @@ packages: dev: true optional: true + /jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -8374,6 +8644,110 @@ packages: - xml2js dev: true + /nuxt@3.10.3(vite@5.1.4): + resolution: {integrity: sha512-NchGNiiz9g/ErJAb462W/lpX2NqcXYb9hugySKWvLXNdrjeAPiJ2/7mhgwUSiZA9MpjuQg3saiEajr1zlRIOCg==} + engines: {node: ^14.18.0 || >=16.10.0} + hasBin: true + peerDependencies: + '@parcel/watcher': ^2.1.0 + '@types/node': ^14.18.0 || >=16.10.0 + peerDependenciesMeta: + '@parcel/watcher': + optional: true + '@types/node': + optional: true + dependencies: + '@nuxt/devalue': 2.0.2 + '@nuxt/devtools': 1.0.8(nuxt@3.10.3)(vite@5.1.4) + '@nuxt/kit': 3.10.3 + '@nuxt/schema': 3.10.3 + '@nuxt/telemetry': 2.5.3 + '@nuxt/ui-templates': 1.3.1 + '@nuxt/vite-builder': 3.10.3(vue@3.4.19) + '@unhead/dom': 1.8.10 + '@unhead/ssr': 1.8.10 + '@unhead/vue': 1.8.10(vue@3.4.19) + '@vue/shared': 3.4.19 + acorn: 8.11.3 + c12: 1.9.0 + chokidar: 3.6.0 + cookie-es: 1.0.0 + defu: 6.1.4 + destr: 2.0.3 + devalue: 4.3.2 + esbuild: 0.20.1 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fs-extra: 11.2.0 + globby: 14.0.1 + h3: 1.10.2 + hookable: 5.5.3 + jiti: 1.21.0 + klona: 2.0.6 + knitwork: 1.0.0 + magic-string: 0.30.7 + mlly: 1.6.0 + nitropack: 2.8.1 + nuxi: 3.10.1 + nypm: 0.3.6 + ofetch: 1.3.3 + ohash: 1.1.3 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.0.3 + radix3: 1.1.0 + scule: 1.3.0 + std-env: 3.7.0 + strip-literal: 2.0.0 + ufo: 1.4.0 + ultrahtml: 1.5.3 + uncrypto: 0.1.3 + unctx: 2.3.1 + unenv: 1.9.0 + unimport: 3.7.1 + unplugin: 1.7.1 + unplugin-vue-router: 0.7.0(vue-router@4.3.0)(vue@3.4.19) + untyped: 1.4.2 + vue: 3.4.19(typescript@5.3.3) + vue-bundle-renderer: 2.0.0 + vue-devtools-stub: 0.1.0 + vue-router: 4.3.0(vue@3.4.19) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/kv' + - bluebird + - bufferutil + - encoding + - eslint + - idb-keyval + - less + - lightningcss + - meow + - optionator + - rollup + - sass + - stylelint + - stylus + - sugarss + - supports-color + - terser + - utf-8-validate + - vite + - vls + - vti + - vue-tsc + - xml2js + dev: true + /nypm@0.3.6: resolution: {integrity: sha512-2CATJh3pd6CyNfU5VZM7qSwFu0ieyabkEdnogE30Obn1czrmOYiZ8DOZLe1yBdLKWoyD3Mcy2maUs+0MR3yVjQ==} engines: {node: ^14.16.0 || >=16.10.0} @@ -10555,6 +10929,26 @@ packages: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} + /unimport@3.7.1: + resolution: {integrity: sha512-V9HpXYfsZye5bPPYUgs0Otn3ODS1mDUciaBlXljI4C2fTwfFpvFZRywmlOu943puN9sncxROMZhsZCjNXEpzEQ==} + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.12.0) + acorn: 8.11.3 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.7 + mlly: 1.6.0 + pathe: 1.1.2 + pkg-types: 1.0.3 + scule: 1.3.0 + strip-literal: 1.3.0 + unplugin: 1.7.1 + transitivePeerDependencies: + - rollup + dev: true + /unimport@3.7.1(rollup@3.29.4): resolution: {integrity: sha512-V9HpXYfsZye5bPPYUgs0Otn3ODS1mDUciaBlXljI4C2fTwfFpvFZRywmlOu943puN9sncxROMZhsZCjNXEpzEQ==} dependencies: @@ -10645,6 +11039,33 @@ packages: - vue dev: true + /unplugin-vue-router@0.7.0(vue-router@4.3.0)(vue@3.4.19): + resolution: {integrity: sha512-ddRreGq0t5vlSB7OMy4e4cfU1w2AwBQCwmvW3oP/0IHQiokzbx4hd3TpwBu3eIAFVuhX2cwNQwp1U32UybTVCw==} + peerDependencies: + vue-router: ^4.1.0 + peerDependenciesMeta: + vue-router: + optional: true + dependencies: + '@babel/types': 7.23.9 + '@rollup/pluginutils': 5.1.0(rollup@4.12.0) + '@vue-macros/common': 1.8.0(vue@3.4.19) + ast-walker-scope: 0.5.0 + chokidar: 3.6.0 + fast-glob: 3.3.2 + json5: 2.2.3 + local-pkg: 0.4.3 + mlly: 1.5.0 + pathe: 1.1.2 + scule: 1.3.0 + unplugin: 1.7.1 + vue-router: 4.3.0(vue@3.4.19) + yaml: 2.3.4 + transitivePeerDependencies: + - rollup + - vue + dev: true + /unplugin@1.7.1: resolution: {integrity: sha512-JqzORDAPxxs8ErLV4x+LL7bk5pk3YlcWqpSNsIkAZj972KzFZLClc/ekppahKkOczGkwIG6ElFgdOgOlK4tXZw==} dependencies: @@ -10910,6 +11331,32 @@ packages: - supports-color dev: true + /vite-plugin-inspect@0.8.3(@nuxt/kit@3.10.3)(vite@5.1.4): + resolution: {integrity: sha512-SBVzOIdP/kwe6hjkt7LSW4D0+REqqe58AumcnCfRNw4Kt3mbS9pEBkch+nupu2PBxv2tQi69EQHQ1ZA1vgB/Og==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^3.1.0 || ^4.0.0 || ^5.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + dependencies: + '@antfu/utils': 0.7.7 + '@nuxt/kit': 3.10.3 + '@rollup/pluginutils': 5.1.0(rollup@4.12.0) + debug: 4.3.4 + error-stack-parser-es: 0.1.1 + fs-extra: 11.2.0 + open: 10.0.3 + perfect-debounce: 1.0.0 + picocolors: 1.0.0 + sirv: 2.0.4 + vite: 5.1.4(@types/node@20.11.20) + transitivePeerDependencies: + - rollup + - supports-color + dev: true + /vite-plugin-vue-inspector@4.0.2(vite@5.1.4): resolution: {integrity: sha512-KPvLEuafPG13T7JJuQbSm5PwSxKFnVS965+MP1we2xGw9BPkkc/+LPix5MMWenpKWqtjr0ws8THrR+KuoDC8hg==} peerDependencies: @@ -11020,24 +11467,21 @@ packages: - universal-cookie dev: true - /vitest-environment-nuxt@1.0.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19): + /vitest-environment-nuxt@1.0.0(h3@1.10.2)(rollup@3.29.4)(vite@5.1.4)(vue-router@4.3.0)(vue@3.4.19): resolution: {integrity: sha512-AWMO9h4HdbaFdPWZw34gALFI8gbBiOpvfbyeZwHIPfh4kWg/TwElYHvYMQ61WPUlCGaS5LebfHkaI0WPyb//Iw==} dependencies: - '@nuxt/test-utils': 3.11.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19) + '@nuxt/test-utils': 3.11.0(h3@1.10.2)(rollup@3.29.4)(vite@5.1.4)(vue-router@4.3.0)(vue@3.4.19) transitivePeerDependencies: - '@cucumber/cucumber' - '@jest/globals' - '@testing-library/vue' - '@vitest/ui' - - '@vue/test-utils' - h3 - - happy-dom - jsdom - playwright-core - rollup - supports-color - vite - - vitest - vue - vue-router dev: true diff --git a/src/auth/index.ts b/src/auth/index.ts index bac5b1b8..68bbe2f0 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -14,6 +14,8 @@ import { getGlobalScope } from '../globals' import { isClient, _Nullable } from '../shared' import { authUserMap, setupOnAuthStateChanged } from './user' import { type VueFireModule } from '..' +import { jwtDecode } from 'jwt-decode' +import type { DecodedIdToken } from 'firebase-admin/auth' export { useCurrentUser, @@ -181,3 +183,25 @@ export function useFirebaseAuth(name?: string) { } return isClient ? inject(_VueFireAuthKey) : null } + +export function parseTenantFromFirebaseJwt( + jwt?: string | undefined | null +): string | null { + if (!jwt) return null + + try { + const decodedToken = jwtDecode(jwt) + if (!decodedToken) return null + + const firebase = decodedToken.firebase + if (!firebase) return null + + return firebase.tenant ?? null + } catch (error) { + if (process.env.NODE_ENV !== 'production') { + console.warn('[VueFire] could not parse tenant from jwt: ', error) + } + } + + return null +} diff --git a/src/index.ts b/src/index.ts index abf9c7bc..68249df1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -96,6 +96,7 @@ export { _VueFireAuthKey, getCurrentUser, updateCurrentUserProfile, + parseTenantFromFirebaseJwt, } from './auth' /** diff --git a/src/server/auth.ts b/src/server/auth.ts index 650378fd..1164a39b 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -9,7 +9,7 @@ import type { App as AdminApp } from 'firebase-admin/app' import { getAuth as getAdminAuth } from 'firebase-admin/auth' import { logger } from './logging' import { isFirebaseError } from './utils' -import { _VueFireAuthKey } from '../auth' +import { _VueFireAuthKey, parseTenantFromFirebaseJwt } from '../auth' // MUST be named `__session` to be kept in Firebase context, therefore this name is hardcoded // https://firebase.google.com/docs/hosting/manage-cache#using_cookies @@ -112,9 +112,13 @@ export async function decodeSessionCookie( adminApp: AdminApp ): Promise { if (sessionCookie) { - const adminAuth = getAdminAuth(adminApp) - try { + const tenant = parseTenantFromFirebaseJwt(sessionCookie) + + const adminAuth = tenant + ? getAdminAuth(adminApp).tenantManager().authForTenant(tenant) + : getAdminAuth(adminApp) + // TODO: should we check for the revoked status of the token here? // we await to try/catch // return await adminAuth.verifyIdToken(token /*, checkRevoked */) From 37a1f12e94c58547c9b9c24b57341b86eb7bfe82 Mon Sep 17 00:00:00 2001 From: JRaams <47593569+JRaams@users.noreply.github.com> Date: Tue, 16 Apr 2024 22:55:38 +0200 Subject: [PATCH 2/3] feat: browser tests for tenant based auth; add tenants to playground --- packages/nuxt/package.json | 1 + .../nuxt/playground/pages/authentication.vue | 39 +- packages/nuxt/tests/auth.spec.ts | 96 ++++ pnpm-lock.yaml | 484 ++---------------- tests/auth/parse-tenants-from-token.spec.ts | 53 ++ 5 files changed, 202 insertions(+), 471 deletions(-) create mode 100644 packages/nuxt/tests/auth.spec.ts create mode 100644 tests/auth/parse-tenants-from-token.spec.ts diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index cf8e1b9d..12b3942d 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -71,6 +71,7 @@ "firebase-admin": "^12.0.0", "firebase-functions": "^4.7.0", "nuxt": "^3.10.3", + "playwright-core": "^1.43.1", "vuefire": "workspace:*" } } diff --git a/packages/nuxt/playground/pages/authentication.vue b/packages/nuxt/playground/pages/authentication.vue index f21ce0ec..4bad16cc 100644 --- a/packages/nuxt/playground/pages/authentication.vue +++ b/packages/nuxt/playground/pages/authentication.vue @@ -45,7 +45,9 @@ watch(user, (user) => { // new user const email = ref('') const password = ref('') -function signUp() { +const tenant = ref(null) + +async function signUp() { // link to an existing anonymous account if (user.value?.isAnonymous) { credential = EmailAuthProvider.credential(email.value, password.value) @@ -59,7 +61,7 @@ function signUp() { return createUserWithEmailAndPassword(auth, email.value, password.value) } -function signinPopup() { +async function signinPopup() { return signInWithPopup(auth, googleAuthProvider).then((result) => { const googleCredential = GoogleAuthProvider.credentialFromResult(result) credential = googleCredential @@ -96,37 +98,42 @@ onMounted(() => { diff --git a/packages/nuxt/tests/auth.spec.ts b/packages/nuxt/tests/auth.spec.ts new file mode 100644 index 00000000..535c3238 --- /dev/null +++ b/packages/nuxt/tests/auth.spec.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest' +import { createPage, setup } from '@nuxt/test-utils/e2e' +import { createResolver } from '@nuxt/kit' + +const { resolve } = createResolver(import.meta.url) + +await setup({ + rootDir: resolve('../playground'), + build: true, + server: true, + browser: true, + dev: true, + + browserOptions: { + type: 'chromium', + launch: { + headless: false, + }, + }, +}) + +describe.only('auth/multi-tenancy', async () => { + it('should create a default tenant token if no tenant is specified', async () => { + const page = await createPage('/authentication') + + // 1. Sign out, clear tenant to start clean + await page.getByTestId('sign-out').click() + await page.getByTestId('tenant').clear() + + // 2. Ensure test account exists + const signupResponse = page.waitForResponse((r) => + r.url().includes('accounts:signUp') + ) + await page.getByTestId('email-signup').fill('test@test.com') + await page.getByTestId('password-signup').fill('testtest') + await page.getByTestId('submit-signup').click() + await signupResponse + + // 3. Log in with test account, check tenant + // Call to sign in is 'accounts:signInWithPassword', but we need __session call to get user info + const signinResponse = page.waitForResponse((r) => + r.url().includes('/api/__session') + ) + await page.getByTestId('email-signin').fill('test@test.com') + await page.getByTestId('password-signin').fill('testtest') + await page.getByTestId('submit-signin').click() + await signinResponse + + // 4. Assert user does in fact not have a tenant id + const userData = await page.getByTestId('user-data').textContent() + + expect(userData).toBeTruthy() + if (!userData) return + + const user = JSON.parse(userData) + expect(user.tenantId).toBeUndefined() + }) + + it('should create token with tenantId if tenant name is specified', async () => { + const page = await createPage('/authentication') + const tenantName = 'tenant A' + + // 1. Sign out, clear tenant to start clean + await page.getByTestId('sign-out').click() + await page.getByTestId('tenant').clear() + await page.getByTestId('tenant').fill(tenantName) + + // 2. Ensure test account exists + const signupResponse = page.waitForResponse((r) => + r.url().includes('accounts:signUp') + ) + await page.getByTestId('email-signup').fill('test@test.com') + await page.getByTestId('password-signup').fill('testtest') + await page.getByTestId('submit-signup').click() + await signupResponse + + // 3. Log in with test account, check tenant + // Call to sign in is 'accounts:signInWithPassword', but we need __session call to get user info + const signinResponse = page.waitForResponse((r) => + r.url().includes('/api/__session') + ) + await page.getByTestId('email-signin').fill('test@test.com') + await page.getByTestId('password-signin').fill('testtest') + await page.getByTestId('submit-signin').click() + await signinResponse + + // 4. Assert user does in fact not have a tenant id + const userData = await page.getByTestId('user-data').textContent() + + expect(userData).toBeTruthy() + if (!userData) return + + const user = JSON.parse(userData) + expect(user.tenantId).toEqual(tenantName) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5dff1025..28386872 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,7 +86,7 @@ importers: version: 2.0.0(typescript@5.3.3) vitepress: specifier: 1.0.0-rc.44 - version: 1.0.0-rc.44(@algolia/client-search@4.22.1)(search-insights@2.13.0)(typescript@5.3.3) + version: 1.0.0-rc.44(@algolia/client-search@4.22.1)(jwt-decode@4.0.0)(search-insights@2.13.0)(typescript@5.3.3) vitest: specifier: ^1.3.1 version: 1.3.1(happy-dom@13.4.1) @@ -120,7 +120,7 @@ importers: version: 3.10.3(rollup@3.29.4) '@nuxt/test-utils': specifier: ^3.11.0 - version: 3.11.0(h3@1.10.2)(rollup@3.29.4)(vite@5.1.4)(vue-router@4.3.0)(vue@3.4.19) + version: 3.11.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(playwright-core@1.43.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19) eslint: specifier: ^8.56.0 version: 8.56.0 @@ -136,6 +136,9 @@ importers: nuxt: specifier: ^3.10.3 version: 3.10.3(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.1.4) + playwright-core: + specifier: ^1.43.1 + version: 1.43.1 vuefire: specifier: workspace:* version: link:../.. @@ -157,10 +160,10 @@ importers: devDependencies: '@nuxt/devtools': specifier: ^1.0.8 - version: 1.0.8(nuxt@3.10.3)(vite@5.1.4) + version: 1.0.8(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.4) nuxt: specifier: ^3.10.3 - version: 3.10.3(vite@5.1.4) + version: 3.10.3(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.1.4) playground: dependencies: @@ -203,7 +206,7 @@ importers: version: 5.3.3 unplugin-vue-router: specifier: ^0.7.0 - version: 0.7.0(vue-router@4.3.0)(vue@3.4.19) + version: 0.7.0(rollup@3.29.4)(vue-router@4.3.0)(vue@3.4.19) vite: specifier: ^5.1.4 version: 5.1.4(@types/node@20.11.20) @@ -2142,22 +2145,6 @@ packages: - supports-color dev: true - /@nuxt/devtools-kit@1.0.8(nuxt@3.10.3)(vite@5.1.4): - resolution: {integrity: sha512-j7bNZmoAXQ1a8qv6j6zk4c/aekrxYqYVQM21o/Hy4XHCUq4fajSgpoc8mjyWJSTfpkOmuLyEzMexpDWiIVSr6A==} - peerDependencies: - nuxt: ^3.9.0 - vite: '*' - dependencies: - '@nuxt/kit': 3.10.3 - '@nuxt/schema': 3.10.3 - execa: 7.2.0 - nuxt: 3.10.3(vite@5.1.4) - vite: 5.1.4(@types/node@20.11.20) - transitivePeerDependencies: - - rollup - - supports-color - dev: true - /@nuxt/devtools-wizard@1.0.8: resolution: {integrity: sha512-RxyOlM7Isk5npwXwDJ/rjm9ekX5sTNG0LS0VOBMdSx+D5nlRPMRr/r9yO+9WQDyzPLClLzHaXRHBWLPlRX3IMw==} hasBin: true @@ -2225,57 +2212,6 @@ packages: - utf-8-validate dev: true - /@nuxt/devtools@1.0.8(nuxt@3.10.3)(vite@5.1.4): - resolution: {integrity: sha512-o6aBFEBxc8OgVHV4OPe2g0q9tFIe9HiTxRiJnlTJ+jHvOQsBLS651ArdVtwLChf9UdMouFlpLLJ1HteZqTbtsQ==} - hasBin: true - peerDependencies: - nuxt: ^3.9.0 - vite: '*' - dependencies: - '@antfu/utils': 0.7.7 - '@nuxt/devtools-kit': 1.0.8(nuxt@3.10.3)(vite@5.1.4) - '@nuxt/devtools-wizard': 1.0.8 - '@nuxt/kit': 3.10.3 - birpc: 0.2.17 - consola: 3.2.3 - destr: 2.0.3 - error-stack-parser-es: 0.1.1 - execa: 7.2.0 - fast-glob: 3.3.2 - flatted: 3.3.1 - get-port-please: 3.1.2 - hookable: 5.5.3 - image-meta: 0.2.0 - is-installed-globally: 1.0.0 - launch-editor: 2.6.1 - local-pkg: 0.5.0 - magicast: 0.3.3 - nuxt: 3.10.3(vite@5.1.4) - nypm: 0.3.6 - ohash: 1.1.3 - pacote: 17.0.6 - pathe: 1.1.2 - perfect-debounce: 1.0.0 - pkg-types: 1.0.3 - rc9: 2.1.1 - scule: 1.3.0 - semver: 7.6.0 - simple-git: 3.22.0 - sirv: 2.0.4 - unimport: 3.7.1 - vite: 5.1.4(@types/node@20.11.20) - vite-plugin-inspect: 0.8.3(@nuxt/kit@3.10.3)(vite@5.1.4) - vite-plugin-vue-inspector: 4.0.2(vite@5.1.4) - which: 3.0.1 - ws: 8.16.0 - transitivePeerDependencies: - - bluebird - - bufferutil - - rollup - - supports-color - - utf-8-validate - dev: true - /@nuxt/eslint-config@0.2.0(eslint@8.56.0): resolution: {integrity: sha512-NeJX8TLcnNAjQFiDs3XhP+9CHKK8jaKsP7eUyCSrQdgY7nqWe7VJx64lwzx5FTT4cW3RHMEyH+Y0qzLGYYoa/A==} peerDependencies: @@ -2291,33 +2227,6 @@ packages: - supports-color dev: true - /@nuxt/kit@3.10.3: - resolution: {integrity: sha512-PUjYB9Mvx0qD9H1QZBwwtY4fLlCLET+Mm9BVqUOtXCaGoXd6u6BE4e/dGFPk2UEKkIcDGrUMSbqkHYvsEuK9NQ==} - engines: {node: ^14.18.0 || >=16.10.0} - dependencies: - '@nuxt/schema': 3.10.3 - c12: 1.9.0 - consola: 3.2.3 - defu: 6.1.4 - globby: 14.0.1 - hash-sum: 2.0.0 - ignore: 5.3.1 - jiti: 1.21.0 - knitwork: 1.0.0 - mlly: 1.6.0 - pathe: 1.1.2 - pkg-types: 1.0.3 - scule: 1.3.0 - semver: 7.6.0 - ufo: 1.4.0 - unctx: 2.3.1 - unimport: 3.7.1 - untyped: 1.4.2 - transitivePeerDependencies: - - rollup - - supports-color - dev: true - /@nuxt/kit@3.10.3(rollup@3.29.4): resolution: {integrity: sha512-PUjYB9Mvx0qD9H1QZBwwtY4fLlCLET+Mm9BVqUOtXCaGoXd6u6BE4e/dGFPk2UEKkIcDGrUMSbqkHYvsEuK9NQ==} engines: {node: ^14.18.0 || >=16.10.0} @@ -2364,26 +2273,6 @@ packages: - typescript dev: true - /@nuxt/schema@3.10.3: - resolution: {integrity: sha512-a4cYbeskEVBPazgAhvUGkL/j7ho/iPWMK3vCEm6dRMjSqHVEITRosrj0aMfLbRrDpTrMjlRs0ZitxiaUfE/p5Q==} - engines: {node: ^14.18.0 || >=16.10.0} - dependencies: - '@nuxt/ui-templates': 1.3.1 - consola: 3.2.3 - defu: 6.1.4 - hookable: 5.5.3 - pathe: 1.1.2 - pkg-types: 1.0.3 - scule: 1.3.0 - std-env: 3.7.0 - ufo: 1.4.0 - unimport: 3.7.1 - untyped: 1.4.2 - transitivePeerDependencies: - - rollup - - supports-color - dev: true - /@nuxt/schema@3.10.3(rollup@3.29.4): resolution: {integrity: sha512-a4cYbeskEVBPazgAhvUGkL/j7ho/iPWMK3vCEm6dRMjSqHVEITRosrj0aMfLbRrDpTrMjlRs0ZitxiaUfE/p5Q==} engines: {node: ^14.18.0 || >=16.10.0} @@ -2403,32 +2292,6 @@ packages: - rollup - supports-color - /@nuxt/telemetry@2.5.3: - resolution: {integrity: sha512-Ghv2MgWbJcUM9G5Dy3oQP0cJkUwEgaiuQxEF61FXJdn0a69Q4StZEP/hLF0MWPM9m6EvAwI7orxkJHM7MrmtVg==} - hasBin: true - dependencies: - '@nuxt/kit': 3.10.3 - ci-info: 4.0.0 - consola: 3.2.3 - create-require: 1.1.1 - defu: 6.1.4 - destr: 2.0.3 - dotenv: 16.4.5 - git-url-parse: 13.1.1 - is-docker: 3.0.0 - jiti: 1.21.0 - mri: 1.2.0 - nanoid: 4.0.2 - ofetch: 1.3.3 - parse-git-config: 3.0.0 - pathe: 1.1.2 - rc9: 2.1.1 - std-env: 3.7.0 - transitivePeerDependencies: - - rollup - - supports-color - dev: true - /@nuxt/telemetry@2.5.3(rollup@3.29.4): resolution: {integrity: sha512-Ghv2MgWbJcUM9G5Dy3oQP0cJkUwEgaiuQxEF61FXJdn0a69Q4StZEP/hLF0MWPM9m6EvAwI7orxkJHM7MrmtVg==} hasBin: true @@ -2455,7 +2318,7 @@ packages: - supports-color dev: true - /@nuxt/test-utils@3.11.0(h3@1.10.2)(rollup@3.29.4)(vite@5.1.4)(vue-router@4.3.0)(vue@3.4.19): + /@nuxt/test-utils@3.11.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(playwright-core@1.43.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19): resolution: {integrity: sha512-9ovgpQZkZpVg/MhYVVn2169WjH/IL0XUqwGryTa/lkx0/BCi1LMVEp3HTPkmt4qbRcxitO+kL4vFqqrFGVaSVg==} engines: {node: ^14.18.0 || >=16.10.0} peerDependencies: @@ -2494,6 +2357,7 @@ packages: dependencies: '@nuxt/kit': 3.10.3(rollup@3.29.4) '@nuxt/schema': 3.10.3(rollup@3.29.4) + '@vue/test-utils': 2.4.4(vue@3.4.19) c12: 1.8.0 consola: 3.2.3 defu: 6.1.4 @@ -2503,12 +2367,14 @@ packages: fake-indexeddb: 5.0.2 get-port-please: 3.1.2 h3: 1.10.2 + happy-dom: 13.4.1 local-pkg: 0.5.0 magic-string: 0.30.7 node-fetch-native: 1.6.2 ofetch: 1.3.3 pathe: 1.1.2 perfect-debounce: 1.0.0 + playwright-core: 1.43.1 radix3: 1.1.0 scule: 1.3.0 std-env: 3.7.0 @@ -2516,7 +2382,8 @@ packages: unenv: 1.9.0 unplugin: 1.7.1 vite: 5.1.4(@types/node@20.11.20) - vitest-environment-nuxt: 1.0.0(h3@1.10.2)(rollup@3.29.4)(vite@5.1.4)(vue-router@4.3.0)(vue@3.4.19) + vitest: 1.3.1(happy-dom@13.4.1) + vitest-environment-nuxt: 1.0.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(playwright-core@1.43.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19) vue: 3.4.19(typescript@5.3.3) vue-router: 4.3.0(vue@3.4.19) transitivePeerDependencies: @@ -2588,66 +2455,6 @@ packages: - vue-tsc dev: true - /@nuxt/vite-builder@3.10.3(vue@3.4.19): - resolution: {integrity: sha512-BqkbrYkEk1AVUJleofbqTRV+ltf2p1CDjGDK78zENPCgrSABlj4F4oK8rze8vmRY5qoH7kMZxgMa2dXVXCp6OA==} - engines: {node: ^14.18.0 || >=16.10.0} - peerDependencies: - vue: ^3.3.4 - dependencies: - '@nuxt/kit': 3.10.3 - '@rollup/plugin-replace': 5.0.5 - '@vitejs/plugin-vue': 5.0.4(vite@5.1.4)(vue@3.4.19) - '@vitejs/plugin-vue-jsx': 3.1.0(vite@5.1.4)(vue@3.4.19) - autoprefixer: 10.4.17(postcss@8.4.35) - clear: 0.1.0 - consola: 3.2.3 - cssnano: 6.0.4(postcss@8.4.35) - defu: 6.1.4 - esbuild: 0.20.1 - escape-string-regexp: 5.0.0 - estree-walker: 3.0.3 - externality: 1.0.2 - fs-extra: 11.2.0 - get-port-please: 3.1.2 - h3: 1.10.2 - knitwork: 1.0.0 - magic-string: 0.30.7 - mlly: 1.6.0 - ohash: 1.1.3 - pathe: 1.1.2 - perfect-debounce: 1.0.0 - pkg-types: 1.0.3 - postcss: 8.4.35 - rollup-plugin-visualizer: 5.12.0(rollup@3.29.4) - std-env: 3.7.0 - strip-literal: 2.0.0 - ufo: 1.4.0 - unenv: 1.9.0 - unplugin: 1.7.1 - vite: 5.1.4(@types/node@20.11.20) - vite-node: 1.3.1 - vite-plugin-checker: 0.6.4(eslint@8.56.0)(typescript@5.3.3)(vite@5.1.4) - vue: 3.4.19(typescript@5.3.3) - vue-bundle-renderer: 2.0.0 - transitivePeerDependencies: - - '@types/node' - - eslint - - less - - lightningcss - - meow - - optionator - - rollup - - sass - - stylelint - - stylus - - sugarss - - supports-color - - terser - - vls - - vti - - vue-tsc - dev: true - /@one-ini/wasm@0.1.1: resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} dev: true @@ -2991,19 +2798,6 @@ packages: rollup: 4.12.0 dev: true - /@rollup/plugin-replace@5.0.5: - resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.12.0) - magic-string: 0.30.7 - dev: true - /@rollup/plugin-replace@5.0.5(rollup@3.29.4): resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==} engines: {node: '>=14.0.0'} @@ -3799,26 +3593,6 @@ packages: - rollup dev: true - /@vue-macros/common@1.8.0(vue@3.4.19): - resolution: {integrity: sha512-auDJJzE0z3uRe3867e0DsqcseKImktNf5ojCZgUKqiVxb2yTlwlgOVAYCgoep9oITqxkXQymSvFeKhedi8PhaA==} - engines: {node: '>=16.14.0'} - peerDependencies: - vue: ^2.7.0 || ^3.2.25 - peerDependenciesMeta: - vue: - optional: true - dependencies: - '@babel/types': 7.23.9 - '@rollup/pluginutils': 5.1.0(rollup@4.12.0) - '@vue/compiler-sfc': 3.4.19 - ast-kit: 0.11.2 - local-pkg: 0.4.3 - magic-string-ast: 0.3.0 - vue: 3.4.19(typescript@5.3.3) - transitivePeerDependencies: - - rollup - dev: true - /@vue/babel-helper-vue-transform-on@1.2.1: resolution: {integrity: sha512-jtEXim+pfyHWwvheYwUwSXm43KwQo8nhOBDyjrUITV6X2tB7lJm6n/+4sqR8137UVZZul5hBzWHdZ2uStYpyRQ==} dev: true @@ -4008,7 +3782,7 @@ packages: - vue dev: true - /@vueuse/integrations@10.8.0(focus-trap@7.5.4)(vue@3.4.19): + /@vueuse/integrations@10.8.0(focus-trap@7.5.4)(jwt-decode@4.0.0)(vue@3.4.19): resolution: {integrity: sha512-sw3P/7cXOfNLQfERp7P0IJ2ODjLE2C3BGXpBQJQkS309c1jbJak9yu4EnY70WaZjkj53aeWSFU6BbHrUxXJ7SA==} peerDependencies: async-validator: '*' @@ -4052,6 +3826,7 @@ packages: '@vueuse/core': 10.8.0(vue@3.4.19) '@vueuse/shared': 10.8.0(vue@3.4.19) focus-trap: 7.5.4 + jwt-decode: 4.0.0 vue-demi: 0.14.7(vue@3.4.19) transitivePeerDependencies: - '@vue/composition-api' @@ -4336,17 +4111,6 @@ packages: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true - /ast-kit@0.11.2: - resolution: {integrity: sha512-Q0DjXK4ApbVoIf9GLyCo252tUH44iTnD/hiJ2TQaJeydYWSpKk0sI34+WMel8S9Wt5pbLgG02oJ+gkgX5DV3sQ==} - engines: {node: '>=16.14.0'} - dependencies: - '@babel/parser': 7.23.9 - '@rollup/pluginutils': 5.1.0(rollup@4.12.0) - pathe: 1.1.2 - transitivePeerDependencies: - - rollup - dev: true - /ast-kit@0.11.2(rollup@3.29.4): resolution: {integrity: sha512-Q0DjXK4ApbVoIf9GLyCo252tUH44iTnD/hiJ2TQaJeydYWSpKk0sI34+WMel8S9Wt5pbLgG02oJ+gkgX5DV3sQ==} engines: {node: '>=16.14.0'} @@ -4358,17 +4122,6 @@ packages: - rollup dev: true - /ast-kit@0.9.5: - resolution: {integrity: sha512-kbL7ERlqjXubdDd+szuwdlQ1xUxEz9mCz1+m07ftNVStgwRb2RWw+U6oKo08PAvOishMxiqz1mlJyLl8yQx2Qg==} - engines: {node: '>=16.14.0'} - dependencies: - '@babel/parser': 7.23.9 - '@rollup/pluginutils': 5.1.0(rollup@4.12.0) - pathe: 1.1.2 - transitivePeerDependencies: - - rollup - dev: true - /ast-kit@0.9.5(rollup@3.29.4): resolution: {integrity: sha512-kbL7ERlqjXubdDd+szuwdlQ1xUxEz9mCz1+m07ftNVStgwRb2RWw+U6oKo08PAvOishMxiqz1mlJyLl8yQx2Qg==} engines: {node: '>=16.14.0'} @@ -4380,16 +4133,6 @@ packages: - rollup dev: true - /ast-walker-scope@0.5.0: - resolution: {integrity: sha512-NsyHMxBh4dmdEHjBo1/TBZvCKxffmZxRYhmclfu0PP6Aftre47jOHYaYaNqJcV0bxihxFXhDkzLHUwHc0ocd0Q==} - engines: {node: '>=16.14.0'} - dependencies: - '@babel/parser': 7.23.9 - ast-kit: 0.9.5 - transitivePeerDependencies: - - rollup - dev: true - /ast-walker-scope@0.5.0(rollup@3.29.4): resolution: {integrity: sha512-NsyHMxBh4dmdEHjBo1/TBZvCKxffmZxRYhmclfu0PP6Aftre47jOHYaYaNqJcV0bxihxFXhDkzLHUwHc0ocd0Q==} engines: {node: '>=16.14.0'} @@ -7547,7 +7290,6 @@ packages: /jwt-decode@4.0.0: resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} engines: {node: '>=18'} - dev: false /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -8644,110 +8386,6 @@ packages: - xml2js dev: true - /nuxt@3.10.3(vite@5.1.4): - resolution: {integrity: sha512-NchGNiiz9g/ErJAb462W/lpX2NqcXYb9hugySKWvLXNdrjeAPiJ2/7mhgwUSiZA9MpjuQg3saiEajr1zlRIOCg==} - engines: {node: ^14.18.0 || >=16.10.0} - hasBin: true - peerDependencies: - '@parcel/watcher': ^2.1.0 - '@types/node': ^14.18.0 || >=16.10.0 - peerDependenciesMeta: - '@parcel/watcher': - optional: true - '@types/node': - optional: true - dependencies: - '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 1.0.8(nuxt@3.10.3)(vite@5.1.4) - '@nuxt/kit': 3.10.3 - '@nuxt/schema': 3.10.3 - '@nuxt/telemetry': 2.5.3 - '@nuxt/ui-templates': 1.3.1 - '@nuxt/vite-builder': 3.10.3(vue@3.4.19) - '@unhead/dom': 1.8.10 - '@unhead/ssr': 1.8.10 - '@unhead/vue': 1.8.10(vue@3.4.19) - '@vue/shared': 3.4.19 - acorn: 8.11.3 - c12: 1.9.0 - chokidar: 3.6.0 - cookie-es: 1.0.0 - defu: 6.1.4 - destr: 2.0.3 - devalue: 4.3.2 - esbuild: 0.20.1 - escape-string-regexp: 5.0.0 - estree-walker: 3.0.3 - fs-extra: 11.2.0 - globby: 14.0.1 - h3: 1.10.2 - hookable: 5.5.3 - jiti: 1.21.0 - klona: 2.0.6 - knitwork: 1.0.0 - magic-string: 0.30.7 - mlly: 1.6.0 - nitropack: 2.8.1 - nuxi: 3.10.1 - nypm: 0.3.6 - ofetch: 1.3.3 - ohash: 1.1.3 - pathe: 1.1.2 - perfect-debounce: 1.0.0 - pkg-types: 1.0.3 - radix3: 1.1.0 - scule: 1.3.0 - std-env: 3.7.0 - strip-literal: 2.0.0 - ufo: 1.4.0 - ultrahtml: 1.5.3 - uncrypto: 0.1.3 - unctx: 2.3.1 - unenv: 1.9.0 - unimport: 3.7.1 - unplugin: 1.7.1 - unplugin-vue-router: 0.7.0(vue-router@4.3.0)(vue@3.4.19) - untyped: 1.4.2 - vue: 3.4.19(typescript@5.3.3) - vue-bundle-renderer: 2.0.0 - vue-devtools-stub: 0.1.0 - vue-router: 4.3.0(vue@3.4.19) - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/kv' - - bluebird - - bufferutil - - encoding - - eslint - - idb-keyval - - less - - lightningcss - - meow - - optionator - - rollup - - sass - - stylelint - - stylus - - sugarss - - supports-color - - terser - - utf-8-validate - - vite - - vls - - vti - - vue-tsc - - xml2js - dev: true - /nypm@0.3.6: resolution: {integrity: sha512-2CATJh3pd6CyNfU5VZM7qSwFu0ieyabkEdnogE30Obn1czrmOYiZ8DOZLe1yBdLKWoyD3Mcy2maUs+0MR3yVjQ==} engines: {node: ^14.16.0 || >=16.10.0} @@ -9147,6 +8785,12 @@ packages: mlly: 1.6.0 pathe: 1.1.2 + /playwright-core@1.43.1: + resolution: {integrity: sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==} + engines: {node: '>=16'} + hasBin: true + dev: true + /postcss-calc@9.0.1(postcss@8.4.35): resolution: {integrity: sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==} engines: {node: ^14 || ^16 || >=18.0} @@ -10929,26 +10573,6 @@ packages: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} - /unimport@3.7.1: - resolution: {integrity: sha512-V9HpXYfsZye5bPPYUgs0Otn3ODS1mDUciaBlXljI4C2fTwfFpvFZRywmlOu943puN9sncxROMZhsZCjNXEpzEQ==} - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.12.0) - acorn: 8.11.3 - escape-string-regexp: 5.0.0 - estree-walker: 3.0.3 - fast-glob: 3.3.2 - local-pkg: 0.5.0 - magic-string: 0.30.7 - mlly: 1.6.0 - pathe: 1.1.2 - pkg-types: 1.0.3 - scule: 1.3.0 - strip-literal: 1.3.0 - unplugin: 1.7.1 - transitivePeerDependencies: - - rollup - dev: true - /unimport@3.7.1(rollup@3.29.4): resolution: {integrity: sha512-V9HpXYfsZye5bPPYUgs0Otn3ODS1mDUciaBlXljI4C2fTwfFpvFZRywmlOu943puN9sncxROMZhsZCjNXEpzEQ==} dependencies: @@ -11039,33 +10663,6 @@ packages: - vue dev: true - /unplugin-vue-router@0.7.0(vue-router@4.3.0)(vue@3.4.19): - resolution: {integrity: sha512-ddRreGq0t5vlSB7OMy4e4cfU1w2AwBQCwmvW3oP/0IHQiokzbx4hd3TpwBu3eIAFVuhX2cwNQwp1U32UybTVCw==} - peerDependencies: - vue-router: ^4.1.0 - peerDependenciesMeta: - vue-router: - optional: true - dependencies: - '@babel/types': 7.23.9 - '@rollup/pluginutils': 5.1.0(rollup@4.12.0) - '@vue-macros/common': 1.8.0(vue@3.4.19) - ast-walker-scope: 0.5.0 - chokidar: 3.6.0 - fast-glob: 3.3.2 - json5: 2.2.3 - local-pkg: 0.4.3 - mlly: 1.5.0 - pathe: 1.1.2 - scule: 1.3.0 - unplugin: 1.7.1 - vue-router: 4.3.0(vue@3.4.19) - yaml: 2.3.4 - transitivePeerDependencies: - - rollup - - vue - dev: true - /unplugin@1.7.1: resolution: {integrity: sha512-JqzORDAPxxs8ErLV4x+LL7bk5pk3YlcWqpSNsIkAZj972KzFZLClc/ekppahKkOczGkwIG6ElFgdOgOlK4tXZw==} dependencies: @@ -11331,32 +10928,6 @@ packages: - supports-color dev: true - /vite-plugin-inspect@0.8.3(@nuxt/kit@3.10.3)(vite@5.1.4): - resolution: {integrity: sha512-SBVzOIdP/kwe6hjkt7LSW4D0+REqqe58AumcnCfRNw4Kt3mbS9pEBkch+nupu2PBxv2tQi69EQHQ1ZA1vgB/Og==} - engines: {node: '>=14'} - peerDependencies: - '@nuxt/kit': '*' - vite: ^3.1.0 || ^4.0.0 || ^5.0.0-0 - peerDependenciesMeta: - '@nuxt/kit': - optional: true - dependencies: - '@antfu/utils': 0.7.7 - '@nuxt/kit': 3.10.3 - '@rollup/pluginutils': 5.1.0(rollup@4.12.0) - debug: 4.3.4 - error-stack-parser-es: 0.1.1 - fs-extra: 11.2.0 - open: 10.0.3 - perfect-debounce: 1.0.0 - picocolors: 1.0.0 - sirv: 2.0.4 - vite: 5.1.4(@types/node@20.11.20) - transitivePeerDependencies: - - rollup - - supports-color - dev: true - /vite-plugin-vue-inspector@4.0.2(vite@5.1.4): resolution: {integrity: sha512-KPvLEuafPG13T7JJuQbSm5PwSxKFnVS965+MP1we2xGw9BPkkc/+LPix5MMWenpKWqtjr0ws8THrR+KuoDC8hg==} peerDependencies: @@ -11412,7 +10983,7 @@ packages: fsevents: 2.3.3 dev: true - /vitepress@1.0.0-rc.44(@algolia/client-search@4.22.1)(search-insights@2.13.0)(typescript@5.3.3): + /vitepress@1.0.0-rc.44(@algolia/client-search@4.22.1)(jwt-decode@4.0.0)(search-insights@2.13.0)(typescript@5.3.3): resolution: {integrity: sha512-tO5taxGI7fSpBK1D8zrZTyJJERlyU9nnt0jHSt3fywfq3VKn977Hg0wUuTkEmwXlFYwuW26+6+3xorf4nD3XvA==} hasBin: true peerDependencies: @@ -11432,7 +11003,7 @@ packages: '@vitejs/plugin-vue': 5.0.4(vite@5.1.4)(vue@3.4.19) '@vue/devtools-api': 7.0.15(vue@3.4.19) '@vueuse/core': 10.8.0(vue@3.4.19) - '@vueuse/integrations': 10.8.0(focus-trap@7.5.4)(vue@3.4.19) + '@vueuse/integrations': 10.8.0(focus-trap@7.5.4)(jwt-decode@4.0.0)(vue@3.4.19) focus-trap: 7.5.4 mark.js: 8.11.1 minisearch: 6.3.0 @@ -11467,21 +11038,24 @@ packages: - universal-cookie dev: true - /vitest-environment-nuxt@1.0.0(h3@1.10.2)(rollup@3.29.4)(vite@5.1.4)(vue-router@4.3.0)(vue@3.4.19): + /vitest-environment-nuxt@1.0.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(playwright-core@1.43.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19): resolution: {integrity: sha512-AWMO9h4HdbaFdPWZw34gALFI8gbBiOpvfbyeZwHIPfh4kWg/TwElYHvYMQ61WPUlCGaS5LebfHkaI0WPyb//Iw==} dependencies: - '@nuxt/test-utils': 3.11.0(h3@1.10.2)(rollup@3.29.4)(vite@5.1.4)(vue-router@4.3.0)(vue@3.4.19) + '@nuxt/test-utils': 3.11.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(playwright-core@1.43.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19) transitivePeerDependencies: - '@cucumber/cucumber' - '@jest/globals' - '@testing-library/vue' - '@vitest/ui' + - '@vue/test-utils' - h3 + - happy-dom - jsdom - playwright-core - rollup - supports-color - vite + - vitest - vue - vue-router dev: true diff --git a/tests/auth/parse-tenants-from-token.spec.ts b/tests/auth/parse-tenants-from-token.spec.ts new file mode 100644 index 00000000..a9256ad6 --- /dev/null +++ b/tests/auth/parse-tenants-from-token.spec.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' +import { parseTenantFromFirebaseJwt } from '../../src' + +describe('auth/parseTenantFromFirebaseJwt', () => { + it('should return null if supplied jwt is null', () => { + const sut = null + + const result = parseTenantFromFirebaseJwt(sut) + + expect(result).toBeNull() + }) + + it('should return null if supplied jwt is an empty string', () => { + const sut = '' + + const result = parseTenantFromFirebaseJwt(sut) + + expect(result).toBeNull() + }) + + it('should return null if supplied jwt is an invalid jwt', () => { + const sut = 'a.b.c' + + const result = parseTenantFromFirebaseJwt(sut) + + expect(result).toBeNull() + }) + + it('should return null if supplied jwt does not have the right structure', () => { + // { + // "tenant": "test" + // } + const sut = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0ZW5hbnQiOiJ0ZXN0In0' + + const result = parseTenantFromFirebaseJwt(sut) + + expect(result).toBeNull() + }) + + it('should return payload tenant if supplied jwt is valid but has no signature', () => { + // { + // "firebase": { + // "tenant": "test" + // } + // } + const sut = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJmaXJlYmFzZSI6eyJ0ZW5hbnQiOiJ0ZXN0In19' + + const result = parseTenantFromFirebaseJwt(sut) + + expect(result).toEqual('test') + }) +}) From a4391e797ee1307a53238efa7c5e7bfe205fa7c1 Mon Sep 17 00:00:00 2001 From: JRaams <47593569+JRaams@users.noreply.github.com> Date: Sun, 21 Apr 2024 17:03:23 +0200 Subject: [PATCH 3/3] fix: multi tenant auth should include tenantId in user/accesstoken during SSR --- package.json | 5 +- packages/nuxt/package.json | 4 +- .../playground/components/ServerOnlyPre.vue | 16 ++++++ .../nuxt/playground/pages/authentication.vue | 8 ++- .../auth/plugin-authenticate-user.server.ts | 5 +- packages/nuxt/tests/auth.spec.ts | 52 +++++++++++++++++-- pnpm-lock.yaml | 11 ++++ 7 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 packages/nuxt/playground/components/ServerOnlyPre.vue diff --git a/package.json b/package.json index b21b7a35..35516eaf 100644 --- a/package.json +++ b/package.json @@ -54,9 +54,9 @@ "lint": "prettier -c --parser typescript \"{src,__tests__,e2e}/**/*.[jt]s?(x)\"", "lint:fix": "pnpm run lint --write", "test:types": "tsc --build tsconfig.json", - "test:unit": "vitest --coverage", + "test:unit": "cross-env GOOGLE_CLOUD_PROJECT='vue-fire-store' vitest --coverage", "firebase:emulators": "firebase emulators:start", - "test:dev": "vitest", + "test:dev": "cross-env GOOGLE_CLOUD_PROJECT='vue-fire-store' vitest", "test": "pnpm run lint && pnpm run test:types && pnpm run build && pnpm run -C packages/nuxt build && pnpm run test:unit run", "prepare": "simple-git-hooks" }, @@ -97,6 +97,7 @@ "chalk": "^5.3.0", "consola": "^3.2.3", "conventional-changelog-cli": "^2.0.34", + "cross-env": "^7.0.3", "enquirer": "^2.4.1", "execa": "^8.0.1", "firebase": "^10.8.0", diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 12b3942d..24590538 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -32,8 +32,8 @@ "build": "nuxt-module-build build", "lint": "eslint src", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . -l nuxt-vuefire -r 1", - "test": "vitest", - "dev": "nuxi dev playground", + "test": "cross-env GOOGLE_CLOUD_PROJECT='vue-fire-store' vitest", + "dev": "cross-env GOOGLE_CLOUD_PROJECT='vue-fire-store' nuxi dev playground", "dev:build": "nuxi build playground", "dev:prepare": "nuxt-module-build --stub" }, diff --git a/packages/nuxt/playground/components/ServerOnlyPre.vue b/packages/nuxt/playground/components/ServerOnlyPre.vue new file mode 100644 index 00000000..c2c3a31f --- /dev/null +++ b/packages/nuxt/playground/components/ServerOnlyPre.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/nuxt/playground/pages/authentication.vue b/packages/nuxt/playground/pages/authentication.vue index 4bad16cc..b622e553 100644 --- a/packages/nuxt/playground/pages/authentication.vue +++ b/packages/nuxt/playground/pages/authentication.vue @@ -151,7 +151,13 @@ onMounted(() => {

Current User:

-
{{ user }}
+
{{ user }}
+ + diff --git a/packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts b/packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts index 101b6272..4cf42d59 100644 --- a/packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts +++ b/packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts @@ -35,7 +35,8 @@ export default defineNuxtPlugin(async (nuxtApp) => { // this is also undefined if the user hasn't enabled the session cookie option if (uid) { // reauthenticate if the user is not the same (e.g. invalidated) - if (auth.currentUser?.uid !== uid) { + // OR multi tenancy is used, otherwise tenantId won't be present in SSR accessToken + if (auth.currentUser?.uid !== uid || tenant) { const customToken = await adminAuth .createCustomToken(uid) .catch((err) => { @@ -45,6 +46,8 @@ export default defineNuxtPlugin(async (nuxtApp) => { // console.timeLog('token', `got token for ${user.uid}`) if (customToken) { logger.debug('Signing in with custom token') + // Update firebase/auth tenantId to ensure it is set during SSR + auth.tenantId = tenant ?? null // TODO: allow user to handle error? await signInWithCustomToken(auth, customToken) // console.timeLog('token', `signed in with token for ${user.uid}`) diff --git a/packages/nuxt/tests/auth.spec.ts b/packages/nuxt/tests/auth.spec.ts index 535c3238..70e227f1 100644 --- a/packages/nuxt/tests/auth.spec.ts +++ b/packages/nuxt/tests/auth.spec.ts @@ -6,7 +6,7 @@ const { resolve } = createResolver(import.meta.url) await setup({ rootDir: resolve('../playground'), - build: true, + build: false, server: true, browser: true, dev: true, @@ -14,12 +14,12 @@ await setup({ browserOptions: { type: 'chromium', launch: { - headless: false, + headless: true, }, }, }) -describe.only('auth/multi-tenancy', async () => { +describe('auth/multi-tenancy', async () => { it('should create a default tenant token if no tenant is specified', async () => { const page = await createPage('/authentication') @@ -47,7 +47,7 @@ describe.only('auth/multi-tenancy', async () => { await signinResponse // 4. Assert user does in fact not have a tenant id - const userData = await page.getByTestId('user-data').textContent() + const userData = await page.getByTestId('user-data-client').textContent() expect(userData).toBeTruthy() if (!userData) return @@ -85,7 +85,7 @@ describe.only('auth/multi-tenancy', async () => { await signinResponse // 4. Assert user does in fact not have a tenant id - const userData = await page.getByTestId('user-data').textContent() + const userData = await page.getByTestId('user-data-client').textContent() expect(userData).toBeTruthy() if (!userData) return @@ -93,4 +93,46 @@ describe.only('auth/multi-tenancy', async () => { const user = JSON.parse(userData) expect(user.tenantId).toEqual(tenantName) }) + + it('should return tenantId in server render', async () => { + const page = await createPage('/authentication') + const tenantName = 'tenant A' + + // 1. Sign out, clear tenant to start clean + await page.getByTestId('sign-out').click() + await page.getByTestId('tenant').clear() + await page.getByTestId('tenant').fill(tenantName) + + // 2. Ensure test account exists + const signupResponse = page.waitForResponse((r) => + r.url().includes('accounts:signUp') + ) + await page.getByTestId('email-signup').fill('test@test.com') + await page.getByTestId('password-signup').fill('testtest') + await page.getByTestId('submit-signup').click() + await signupResponse + + // 3. Log in with test account, check tenant + // Call to sign in is 'accounts:signInWithPassword', but we need __session call to get user info + const signinResponse = page.waitForResponse((r) => + r.url().includes('/api/__session') + ) + await page.getByTestId('email-signin').fill('test@test.com') + await page.getByTestId('password-signin').fill('testtest') + await page.getByTestId('submit-signin').click() + await signinResponse + + // 4. Reload the page to trigger server render + await page.reload({ waitUntil: 'domcontentloaded' }) + + const serverUserData = await page + .getByTestId('user-data-server') + .textContent() + + expect(serverUserData).toBeTruthy() + if (!serverUserData) return + + const serverUser = JSON.parse(serverUserData) + expect(serverUser.tenantId).toEqual(tenantName) + }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28386872..f03a8323 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: conventional-changelog-cli: specifier: ^2.0.34 version: 2.2.2 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 enquirer: specifier: ^2.4.1 version: 2.4.1 @@ -4895,6 +4898,14 @@ packages: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true + /cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + dependencies: + cross-spawn: 7.0.3 + dev: true + /cross-spawn@6.0.5: resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} engines: {node: '>=4.8'}