@merkaly/nuxt 0.7.10 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.14.0"
6
6
  },
7
- "version": "0.7.10",
7
+ "version": "0.8.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -1,8 +1,15 @@
1
- declare var __VLS_29: {}, __VLS_31: {};
1
+ declare var __VLS_29: {}, __VLS_31: {
2
+ clear: () => undefined;
3
+ error: import("#imports").AuthError;
4
+ logout: () => any;
5
+ retry: () => Promise<void>;
6
+ }, __VLS_40: {};
2
7
  type __VLS_Slots = {} & {
3
8
  loading?: (props: typeof __VLS_29) => any;
4
9
  } & {
5
- default?: (props: typeof __VLS_31) => any;
10
+ error?: (props: typeof __VLS_31) => any;
11
+ } & {
12
+ default?: (props: typeof __VLS_40) => any;
6
13
  };
7
14
  declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
8
15
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -3,10 +3,11 @@ import { useRoute } from "#app";
3
3
  import { useAuth, watchOnce, addRouteMiddleware, useNuxtApp } from "#imports";
4
4
  import AuthMiddleware from "../middleware/auth";
5
5
  import { useNavigation } from "../composables/useNavigation";
6
+ import AuthError from "./auth/AuthError.vue";
6
7
  import { useColorMode } from "@vueuse/core";
7
8
  import { Notivue, NotificationProgress, Notification, pastelTheme } from "notivue";
8
9
  const $route = useRoute();
9
- const { isLoading } = useAuth();
10
+ const { clear, error, isLoading, logout, retry } = useAuth();
10
11
  const { hook } = useNuxtApp();
11
12
  useColorMode({ attribute: "data-bs-theme" });
12
13
  watchOnce(isLoading, () => AuthMiddleware($route, $route));
@@ -28,6 +29,10 @@ hook("page:finish", () => regenerate());
28
29
  <BApp>
29
30
  <slot v-if="isLoading" name="loading" />
30
31
 
32
+ <slot v-else-if="error" :clear="clear" :error="error" :logout="logout" :retry="retry" name="error">
33
+ <AuthError :error="error" @logout="logout" @retry="retry" />
34
+ </slot>
35
+
31
36
  <slot v-else>
32
37
  <NuxtPage />
33
38
  </slot>
@@ -1,8 +1,15 @@
1
- declare var __VLS_29: {}, __VLS_31: {};
1
+ declare var __VLS_29: {}, __VLS_31: {
2
+ clear: () => undefined;
3
+ error: import("#imports").AuthError;
4
+ logout: () => any;
5
+ retry: () => Promise<void>;
6
+ }, __VLS_40: {};
2
7
  type __VLS_Slots = {} & {
3
8
  loading?: (props: typeof __VLS_29) => any;
4
9
  } & {
5
- default?: (props: typeof __VLS_31) => any;
10
+ error?: (props: typeof __VLS_31) => any;
11
+ } & {
12
+ default?: (props: typeof __VLS_40) => any;
6
13
  };
7
14
  declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
8
15
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -0,0 +1,21 @@
1
+ import type { PropType } from 'vue';
2
+ import type { AuthError } from '../../composables/useAuth.js';
3
+ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
4
+ error: {
5
+ type: PropType<AuthError>;
6
+ required: true;
7
+ };
8
+ }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
+ retry: () => any;
10
+ logout: () => any;
11
+ }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
12
+ error: {
13
+ type: PropType<AuthError>;
14
+ required: true;
15
+ };
16
+ }>> & Readonly<{
17
+ onRetry?: (() => any) | undefined;
18
+ onLogout?: (() => any) | undefined;
19
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
20
+ declare const _default: typeof __VLS_export;
21
+ export default _default;
@@ -0,0 +1,33 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ const props = defineProps({
4
+ error: { type: Object, required: true }
5
+ });
6
+ const emit = defineEmits(["logout", "retry"]);
7
+ const isDenied = computed(() => [401, 403].includes(props.error.status));
8
+ const title = computed(() => isDenied.value ? "Access denied" : "We could not start your session");
9
+ const description = computed(() => isDenied.value ? "You signed in successfully, but this account is not allowed to access this application." : "You signed in successfully, but something failed while preparing your session.");
10
+ const details = computed(() => [props.error.stage, props.error.status, props.error.url].filter(Boolean).join(" \xB7 "));
11
+ </script>
12
+
13
+ <template>
14
+ <div class="d-flex flex-column justify-content-center align-items-center text-center gap-3 min-vh-100 p-5">
15
+ <h1 class="h3 mb-0" v-text="title" />
16
+
17
+ <p class="text-muted mb-0" v-text="description" />
18
+
19
+ <code class="small" v-text="details" />
20
+
21
+ <p class="small text-danger mb-0" v-text="props.error.message" />
22
+
23
+ <div class="d-flex gap-3">
24
+ <BButton v-if="props.error.stage !== 'callback'" variant="primary" @click="emit('retry')">
25
+ <span v-text="'Retry'" />
26
+ </BButton>
27
+
28
+ <BButton variant="light" @click="emit('logout')">
29
+ <span v-text="'Sign in with another account'" />
30
+ </BButton>
31
+ </div>
32
+ </div>
33
+ </template>
@@ -0,0 +1,21 @@
1
+ import type { PropType } from 'vue';
2
+ import type { AuthError } from '../../composables/useAuth.js';
3
+ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
4
+ error: {
5
+ type: PropType<AuthError>;
6
+ required: true;
7
+ };
8
+ }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
+ retry: () => any;
10
+ logout: () => any;
11
+ }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
12
+ error: {
13
+ type: PropType<AuthError>;
14
+ required: true;
15
+ };
16
+ }>> & Readonly<{
17
+ onRetry?: (() => any) | undefined;
18
+ onLogout?: (() => any) | undefined;
19
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
20
+ declare const _default: typeof __VLS_export;
21
+ export default _default;
@@ -1,7 +1,25 @@
1
1
  import type { User } from '@auth0/auth0-spa-js';
2
+ export type AuthErrorStage = 'callback' | 'token' | 'session' | 'tenant';
3
+ export interface AuthError {
4
+ message: string;
5
+ stage: AuthErrorStage;
6
+ status?: number;
7
+ url?: string;
8
+ }
9
+ export interface AuthErrorContext {
10
+ clear: () => void;
11
+ error: AuthError;
12
+ logout: () => Promise<void>;
13
+ retry: () => Promise<void>;
14
+ }
2
15
  export declare const useAuth: () => {
16
+ clear: () => undefined;
17
+ error: import("vue").Ref<AuthError | undefined, AuthError | undefined>;
18
+ fail: (reason: AuthError) => Promise<void>;
3
19
  isAuthenticated: import("vue").ComputedRef<boolean>;
4
20
  isLoading: import("vue").Ref<boolean, boolean>;
21
+ logout: () => any;
22
+ retry: () => Promise<void>;
5
23
  token: import("vue").Ref<string | null, string | null>;
6
24
  user: import("vue").Ref<User | undefined, User | undefined>;
7
25
  };
@@ -1,13 +1,31 @@
1
1
  import { computed } from "vue";
2
- import { useState } from "#imports";
2
+ import { useNuxtApp, useState } from "#imports";
3
3
  export const useAuth = () => {
4
+ const nuxtApp = useNuxtApp();
4
5
  const user = useState("auth:user", () => void 0);
5
6
  const token = useState("auth:token", () => null);
6
7
  const isLoading = useState("auth:loading", () => true);
8
+ const error = useState("auth:error", () => void 0);
7
9
  const isAuthenticated = computed(() => Boolean(user.value?.sub));
10
+ const clear = () => error.value = void 0;
11
+ const logout = () => nuxtApp.$auth0.logout();
12
+ const retry = async () => {
13
+ clear();
14
+ await nuxtApp.callHook("merkaly:auth:retry");
15
+ };
16
+ const fail = async (reason) => {
17
+ error.value = reason;
18
+ console.error(`[Auth] ${reason.stage} failed`, reason);
19
+ await nuxtApp.callHook("merkaly:auth:error", { clear, error: reason, logout, retry });
20
+ };
8
21
  return {
22
+ clear,
23
+ error,
24
+ fail,
9
25
  isAuthenticated,
10
26
  isLoading,
27
+ logout,
28
+ retry,
11
29
  token,
12
30
  user
13
31
  };
@@ -1,2 +1,2 @@
1
- declare const _default: import("nuxt/app").RouteMiddleware;
1
+ declare const _default: import("#app").RouteMiddleware;
2
2
  export default _default;
@@ -28,7 +28,7 @@ export interface RefOptions {
28
28
  export interface HooksOptions<TData = unknown, TMeta = Record<string, unknown>, TParams = object> {
29
29
  onBeforeSend?(args: OnBeforeSendArgs): Promise<unknown> | unknown;
30
30
  onComplete?(args: OnCompleteArgs): Promise<unknown> | unknown;
31
- onError?(reason: Error): Promise<unknown> | unknown;
31
+ onError?(reason: Error, args: OnResponseArgs): Promise<unknown> | unknown;
32
32
  onFatal?(reason: Error): Promise<unknown> | unknown;
33
33
  onResponse?(args: OnResponseArgs): Promise<unknown> | unknown;
34
34
  onSuccess?(args: OnSuccessArgs<TData, TMeta, TParams>): Promise<unknown> | unknown;
@@ -47,5 +47,5 @@ export interface ParamsOptions {
47
47
  export type ApiOptions<TData = unknown, TMeta = Record<string, unknown>, TParams = object> = ParamsOptions & HooksOptions<TData, TMeta, TParams> & RefOptions & {
48
48
  params?: TParams;
49
49
  };
50
- declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
50
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
51
51
  export default _default;
@@ -54,7 +54,7 @@ This surface has requiresTenant: true. Possible causes:
54
54
  },
55
55
  onResponseError: async ({ response, request }) => {
56
56
  const { _data } = response;
57
- await options.onError?.(_data);
57
+ await options.onError?.(_data, { response, request });
58
58
  if (options.error) options.error.value = _data;
59
59
  await options?.onComplete?.({ response, request });
60
60
  if (options.loading) options.loading.value = false;
@@ -1,6 +1,6 @@
1
- declare const _default: import("nuxt/app").Plugin<{
1
+ declare const _default: import("#app").Plugin<{
2
2
  auth0: import("@auth0/auth0-spa-js").Auth0Client;
3
- }> & import("nuxt/app").ObjectPlugin<{
3
+ }> & import("#app").ObjectPlugin<{
4
4
  auth0: import("@auth0/auth0-spa-js").Auth0Client;
5
5
  }>;
6
6
  export default _default;
@@ -3,6 +3,7 @@ import { defineNuxtPlugin, useRuntimeConfig, useNuxtApp } from "#imports";
3
3
  import { navigateTo } from "#app";
4
4
  import { defu } from "defu";
5
5
  import { useAuth } from "../composables/useAuth.js";
6
+ const NO_SESSION_ERRORS = ["consent_required", "interaction_required", "invalid_grant", "login_required", "missing_refresh_token"];
6
7
  export default defineNuxtPlugin(async ({ callHook, hook }) => {
7
8
  const { public: $config } = useRuntimeConfig();
8
9
  const callbackPath = URL.canParse($config.merkaly.auth0.callbackUrl) ? new URL($config.merkaly.auth0.callbackUrl).pathname : $config.merkaly.auth0.callbackUrl;
@@ -12,18 +13,34 @@ export default defineNuxtPlugin(async ({ callHook, hook }) => {
12
13
  domain: $config.merkaly.auth0.domain,
13
14
  useRefreshTokens: true
14
15
  });
15
- const { isLoading, user, token } = useAuth();
16
+ const { isLoading, user, token, fail } = useAuth();
16
17
  const self0 = Object.assign(Object.create(Object.getPrototypeOf(auth0)), auth0);
18
+ let tokenError;
17
19
  auth0.getUser = () => self0.getUser().then((result) => user.value = result).catch((reason) => console.error("[Auth0] getUser failed", reason));
18
- auth0.getTokenSilently = (options = {}) => self0.getTokenSilently(defu({ authorizationParams: { audience: $config.merkaly.auth0.audience } }, options)).then((result) => token.value = result).catch((err) => console.warn("[Auth0] getTokenSilently failed \u2013 fallback, user logged in?", err));
20
+ auth0.getTokenSilently = (options = {}) => self0.getTokenSilently(defu({ authorizationParams: { audience: $config.merkaly.auth0.audience } }, options)).then((result) => {
21
+ tokenError = void 0;
22
+ return token.value = result;
23
+ }).catch((err) => {
24
+ tokenError = err;
25
+ console.warn("[Auth0] getTokenSilently failed \u2013 fallback, user logged in?", err);
26
+ });
27
+ const resolveIdentity = () => Promise.allSettled([auth0.getUser(), auth0.getTokenSilently()]);
19
28
  const initTenantContext = async () => {
20
29
  await callHook("merkaly:tenant", user.value);
21
30
  await callHook("merkaly:auth", user.value);
22
31
  };
23
- auth0.handleRedirectCallback = () => self0.handleRedirectCallback().then(({ appState }) => Promise.allSettled([auth0.getUser(), auth0.getTokenSilently()]).then(async () => {
24
- await initTenantContext();
32
+ const startSession = () => {
33
+ if (user.value && !token.value && tokenError && !NO_SESSION_ERRORS.includes(tokenError.error)) {
34
+ return fail({ message: tokenError.error_description || tokenError.message, stage: "token" });
35
+ }
36
+ return initTenantContext();
37
+ };
38
+ hook("merkaly:auth:retry", () => resolveIdentity().then(startSession));
39
+ auth0.handleRedirectCallback = () => self0.handleRedirectCallback().then(async ({ appState }) => {
40
+ await resolveIdentity();
41
+ await startSession();
25
42
  void navigateTo(appState?.target || "/");
26
- })).catch(() => navigateTo("/"));
43
+ }, (reason) => fail({ message: reason.error_description || reason.message, stage: "callback" }));
27
44
  auth0.loginWithRedirect = (options = {}) => self0.loginWithRedirect(defu(options, {
28
45
  authorizationParams: {
29
46
  audience: $config.merkaly.auth0.audience,
@@ -61,7 +78,8 @@ export default defineNuxtPlugin(async ({ callHook, hook }) => {
61
78
  };
62
79
  const isAuthCallback = window.location.pathname === callbackPath;
63
80
  if (!isAuthCallback) {
64
- await Promise.allSettled([auth0.getUser(), auth0.getTokenSilently()]).then(() => hook("app:created", initTenantContext)).catch(() => void 0);
81
+ await resolveIdentity();
82
+ hook("app:created", startSession);
65
83
  }
66
84
  isLoading.value = false;
67
85
  return { provide: { auth0 } };
@@ -1,2 +1,2 @@
1
- declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
1
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
2
2
  export default _default;
@@ -5,6 +5,7 @@ const TENANT_HOST_HEADER = "x-merkaly-host";
5
5
  const API_PREFIX_PATTERN = /^\/api\/?/;
6
6
  const API_ROUTE_PATTERN = /^\/api(?:\/|$)/;
7
7
  const EDGE_SLASH_PATTERN = /^\/|\/$/g;
8
+ const EDGE_HEADER_PATTERN = /^(cf-|cdn-loop$|true-client-ip$)/;
8
9
  function normalizeSegment(value) {
9
10
  return value.replace(EDGE_SLASH_PATTERN, "");
10
11
  }
@@ -12,6 +13,9 @@ function resolveProxyPath(prefix, pathname, search) {
12
13
  const normalizedPathname = pathname.replace(API_PREFIX_PATTERN, "");
13
14
  return `/${[prefix, normalizedPathname].map(normalizeSegment).filter(Boolean).join("/")}${search}`;
14
15
  }
16
+ function stripEdgeHeaders(headers) {
17
+ Object.keys(headers).filter((name) => EDGE_HEADER_PATTERN.test(name)).forEach((name) => Reflect.deleteProperty(headers, name));
18
+ }
15
19
  function isGlobalRequest(value) {
16
20
  if (Array.isArray(value)) {
17
21
  return value.includes("true");
@@ -29,6 +33,7 @@ export default defineEventHandler((event) => {
29
33
  const path = resolveProxyPath(prefix, url.pathname, url.search);
30
34
  event.node.req.headers["x-forwarded-host"] = url.host;
31
35
  event.node.req.headers[TENANT_HOST_HEADER] = url.host;
36
+ stripEdgeHeaders(event.node.req.headers);
32
37
  if (import.meta.dev && targetOrigin.hostname.endsWith(".test")) {
33
38
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
34
39
  }
@@ -1,6 +1,7 @@
1
1
  import type { HookResult } from '@nuxt/schema';
2
2
  import type { Auth0Client, User } from '@auth0/auth0-spa-js';
3
3
  import type { ApiOptions } from '../plugins/api.global';
4
+ import type { AuthErrorContext } from '../composables/useAuth';
4
5
 
5
6
  declare module '#app' {
6
7
  interface NuxtApp {
@@ -11,6 +12,8 @@ declare module '#app' {
11
12
 
12
13
  interface RuntimeNuxtHooks {
13
14
  'merkaly:auth': (user?: User) => HookResult;
15
+ 'merkaly:auth:error': (context: AuthErrorContext) => HookResult;
16
+ 'merkaly:auth:retry': () => HookResult;
14
17
  'merkaly:tenant': (user?: User) => HookResult;
15
18
  }
16
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merkaly/nuxt",
3
- "version": "0.7.10",
3
+ "version": "0.8.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/merkaly-io/nuxt.git"
@@ -28,49 +28,47 @@
28
28
  "dist"
29
29
  ],
30
30
  "dependencies": {
31
- "@auth0/auth0-spa-js": "^2.21.1",
32
- "@bootstrap-vue-next/nuxt": "^0.45.5",
33
- "@nuxt/devtools": "^3.2.4",
34
- "@nuxt/eslint": "1.16.0",
35
- "@nuxt/eslint-config": "^1.16.0",
36
- "@nuxt/fonts": "0.14.0",
37
- "@nuxt/image": "^2.0.0",
38
- "@nuxt/kit": "^4.4.8",
39
- "@nuxtjs/i18n": "^10.4.0",
40
- "@nuxtjs/plausible": "^3.0.2",
41
- "@sentry/nuxt": "^10.59.0",
31
+ "@auth0/auth0-spa-js": "^2.29.0",
32
+ "@bootstrap-vue-next/nuxt": "^1.0.0",
33
+ "@nuxt/devtools": "^3.4.0",
34
+ "@nuxt/eslint": "1.17.0",
35
+ "@nuxt/eslint-config": "^1.17.0",
36
+ "@nuxt/fonts": "1.1.0",
37
+ "@nuxt/image": "^2.2.0",
38
+ "@nuxt/kit": "^4.6.0",
39
+ "@nuxtjs/i18n": "^10.6.0",
40
+ "@nuxtjs/plausible": "^4.0.0",
41
+ "@sentry/nuxt": "^11.9.0",
42
42
  "@types/node": "latest",
43
43
  "@types/vue-select": "^3.16.8",
44
- "@vueuse/components": "^14.3.0",
45
- "@vueuse/core": "^14.3.0",
46
- "@vueuse/integrations": "^14.3.0",
47
- "@vueuse/nuxt": "^14.3.0",
44
+ "@vueuse/components": "^15.0.0",
45
+ "@vueuse/core": "^15.0.0",
46
+ "@vueuse/integrations": "^15.0.0",
47
+ "@vueuse/nuxt": "^15.0.0",
48
48
  "bootstrap": "^5.3.8",
49
- "bootstrap-vue-next": "^0.45.5",
49
+ "bootstrap-vue-next": "^1.2.0",
50
50
  "change-case": "^5.4.4",
51
51
  "class-transformer": "^0.5.1",
52
52
  "class-validator": "^0.15.1",
53
- "eslint": "^10.5.0",
53
+ "eslint": "^10.13.0",
54
54
  "filesize": "^11.0.17",
55
55
  "gsap": "^3.15.0",
56
56
  "notivue": "^2.4.5",
57
- "nuxt": "^4.4.8",
57
+ "nuxt": "^4.6.2",
58
58
  "reflect-metadata": "^0.2.2",
59
- "sass": "^1.101.0",
59
+ "sass": "^1.106.1",
60
60
  "typescript": "~5.9.3",
61
61
  "v-money3": "^3.26.1",
62
62
  "vite-svg-loader": "^5.1.1",
63
- "vue": "^3.5.38",
64
- "vue-router": "^5.1.0",
63
+ "vue": "^3.5.43",
64
+ "vue-router": "^5.4.0",
65
65
  "vue-select": "4.0.0-beta.6"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@nuxt/module-builder": "^1.0.2",
69
- "@nuxt/schema": "^4.4.8",
70
- "@nuxt/test-utils": "^4.0.3",
69
+ "@nuxt/schema": "^4.6.0",
71
70
  "changelogen": "^0.6.2",
72
- "eslint-plugin-storybook": "^10.4.3",
73
- "vitest": "^4.1.8",
71
+ "eslint-plugin-storybook": "^10.6.0",
74
72
  "vue-tsc": "^3.3.4"
75
73
  },
76
74
  "optionalDependencies": {
@@ -83,8 +81,6 @@
83
81
  "build": "nuxt-module-build build",
84
82
  "build:stub": "nuxt-module-build build --stub",
85
83
  "lint": "eslint .",
86
- "test": "vitest run",
87
- "test:types": "vue-tsc --noEmit",
88
- "test:watch": "vitest watch"
84
+ "test:types": "vue-tsc --noEmit"
89
85
  }
90
86
  }