@merkaly/nuxt 0.13.5 → 0.14.1
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/README.md +36 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/app.vue +3 -0
- package/dist/runtime/components/auth/AuthExpired.d.vue.ts +3 -0
- package/dist/runtime/components/auth/AuthExpired.vue +43 -0
- package/dist/runtime/components/auth/AuthExpired.vue.d.ts +3 -0
- package/dist/runtime/composables/useAuth.d.ts +3 -0
- package/dist/runtime/composables/useAuth.js +9 -0
- package/dist/runtime/composables/useDatagrid.d.ts +2 -1
- package/dist/runtime/composables/useDatagrid.js +8 -5
- package/dist/runtime/composables/useNotify.js +9 -9
- package/dist/runtime/plugins/api.global.js +22 -8
- package/dist/runtime/plugins/auth0.client.js +11 -3
- package/dist/runtime/server/middleware/proxy.js +1 -1
- package/dist/runtime/types/nuxt.d.ts +2 -2
- package/package.json +5 -6
package/README.md
CHANGED
|
@@ -169,6 +169,17 @@ To open the Auth0 signup screen instead of the login one, use `useAuth().signup(
|
|
|
169
169
|
|
|
170
170
|
The SPA keeps the session with **refresh tokens**: the client uses `useRefreshTokens` with `localStorage` cache and logs in with `audience` and `scope: openid profile email offline_access`. Before each API call the module asks Auth0 for the current access token, which is cached and renewed with the refresh token once it expires (2 hours for `https://api.merkaly.io`). When the refresh token is gone or expired, protected surfaces (`requiresAuth: true`) go back to login instead of calling the API without a token. Account linking (`linkWithConnection(connection)`) uses a separate Auth0 client, the `auth.merkaly.io` SPA client in Auth0, with an in-memory cache, created only on the first call (no extra `checkSession` on page load). It opens a login popup for that connection and sends the popup's raw ID token to `POST /auth/identities` as `{ idToken }`; the api verifies the token (signature, issuer, audience = the account linking client, expiry) and links the identity from its `sub` into the logged-in user. Requires an api with that contract (older clients that sent `{ provider, userId }` get `400`).
|
|
171
171
|
|
|
172
|
+
### Expired session (`MKAuthExpired`)
|
|
173
|
+
|
|
174
|
+
When the session ends while the app is open (refresh token revoked, rotated or expired), the module does not log the user out and does not call the API with a dead token:
|
|
175
|
+
|
|
176
|
+
- `getTokenSilently` failing with a "no session" error (`login_required`, `invalid_grant`, `missing_refresh_token`, `consent_required`, `interaction_required`) after the user had a token calls `useAuth().expire()`: it clears `token` and sets `isExpired`.
|
|
177
|
+
- An API `401` for a signed-in user renews the token once (`getTokenSilently({ cacheMode: 'off' })`) and repeats the request; the hooks of the first attempt do not run. If the repeated request gets `401` again (after boot), it calls `expire()` too. Boot failures still go to the `error` slot.
|
|
178
|
+
- `MKApp` mounts `MKAuthExpired` once: a modal that cannot be dismissed, with a single button that calls `useAuth().relogin()` (`loginWithRedirect` with `appState.target` = current path and query), so the user lands back on the same page. Texts in `en`, `es` and `pt` (local i18n block).
|
|
179
|
+
- One state (`auth:expired`), so many failing calls show a single modal. Anonymous visitors (store, www) never had a token, so they never see it.
|
|
180
|
+
|
|
181
|
+
Apps need nothing beyond rendering `MKApp`.
|
|
182
|
+
|
|
172
183
|
### Calling app header
|
|
173
184
|
|
|
174
185
|
Every API request carries the app's Auth0 client id (`merkaly.auth0.client`) in the `x-merkaly-client` header: the `$api` plugin adds it to server-side calls and the `/api` proxy sets it on every forwarded request (overwriting whatever the browser sent). The api checks it against its list of Auth0 applications and, on authenticated requests, against the token `azp` (api doc: [`docs/conventions/surface-clients.md`](https://github.com/merkaly-io/api.merkaly.io/blob/master/docs/conventions/surface-clients.md)). An app without `merkaly.auth0.client` sends no header.
|
|
@@ -343,3 +354,28 @@ setMoneyDefaults({
|
|
|
343
354
|
- `MKInputMoney`: `v-model` in cents; props `currency`, `locale`. `prefix`, `decimal`,
|
|
344
355
|
`thousands` and `placeholder` default to the resolved currency/locale and can still
|
|
345
356
|
be passed explicitly.
|
|
357
|
+
|
|
358
|
+
## Datagrid (`useDatagrid`, `MKTableDatagrid`)
|
|
359
|
+
|
|
360
|
+
`useDatagrid(options)` builds the reactive state that `MKTableDatagrid` renders.
|
|
361
|
+
`options` is a `MaybeRefOrGetter`: a plain object, a ref or a getter returning it.
|
|
362
|
+
|
|
363
|
+
With a getter (or ref) the `columns` are recomputed reactively, so titles from i18n
|
|
364
|
+
follow a locale change. The rest of the options (`items`, `limit`, `total`, `search`,
|
|
365
|
+
`row`, ...) are read once as initial values; the state (`items`, `page`, `loading`,
|
|
366
|
+
...) is never reset by a recompute.
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
const { t: $t } = useI18n();
|
|
370
|
+
|
|
371
|
+
const $datagrid = useDatagrid<OrderEntity>(() => ({
|
|
372
|
+
columns: {
|
|
373
|
+
number: { title: $t('order.number') }, // follows the locale
|
|
374
|
+
total: { title: $t('order.total'), class: 'text-end' },
|
|
375
|
+
},
|
|
376
|
+
limit: 20, // initial value only
|
|
377
|
+
}));
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
A plain object still works and keeps the previous behavior (static columns). Without
|
|
381
|
+
`title` the header falls back to the column key in sentence case.
|
package/dist/module.json
CHANGED
|
@@ -5,6 +5,7 @@ import { useAuth, useOrganization, watchOnce, addRouteMiddleware, useNuxtApp, us
|
|
|
5
5
|
import AuthMiddleware from "../middleware/auth";
|
|
6
6
|
import { useNavigation } from "../composables/useNavigation";
|
|
7
7
|
import AuthError from "./auth/AuthError.vue";
|
|
8
|
+
import AuthExpired from "./auth/AuthExpired.vue";
|
|
8
9
|
import { useColorMode } from "@vueuse/core";
|
|
9
10
|
import { Notivue, NotificationProgress, Notification, pastelTheme } from "notivue";
|
|
10
11
|
const $route = useRoute();
|
|
@@ -41,6 +42,8 @@ hook("page:finish", () => regenerate());
|
|
|
41
42
|
<slot v-else>
|
|
42
43
|
<NuxtPage />
|
|
43
44
|
</slot>
|
|
45
|
+
|
|
46
|
+
<AuthExpired />
|
|
44
47
|
</BApp>
|
|
45
48
|
</main>
|
|
46
49
|
</template>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, useAuth } from "#imports";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
4
|
+
const { isExpired, relogin } = useAuth();
|
|
5
|
+
const { t } = useI18n({ useScope: "local" });
|
|
6
|
+
const isRedirecting = ref(false);
|
|
7
|
+
function signIn() {
|
|
8
|
+
isRedirecting.value = true;
|
|
9
|
+
relogin().catch(() => isRedirecting.value = false);
|
|
10
|
+
}
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<BModal :model-value="isExpired" :title="t('title')" centered no-close-on-backdrop no-close-on-esc no-header-close>
|
|
15
|
+
<p class="mb-0" v-text="t('message')" />
|
|
16
|
+
|
|
17
|
+
<template #footer>
|
|
18
|
+
<BButton :loading="isRedirecting" variant="primary" @click="signIn">
|
|
19
|
+
<span v-text="t('signIn')" />
|
|
20
|
+
</BButton>
|
|
21
|
+
</template>
|
|
22
|
+
</BModal>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<i18n lang="json">
|
|
26
|
+
{
|
|
27
|
+
"en": {
|
|
28
|
+
"message": "For your security, sign in again to continue where you left off.",
|
|
29
|
+
"signIn": "Sign in again",
|
|
30
|
+
"title": "Your session expired"
|
|
31
|
+
},
|
|
32
|
+
"es": {
|
|
33
|
+
"message": "Por tu seguridad, vuelve a entrar para continuar donde estabas.",
|
|
34
|
+
"signIn": "Entrar de nuevo",
|
|
35
|
+
"title": "Tu sesión expiró"
|
|
36
|
+
},
|
|
37
|
+
"pt": {
|
|
38
|
+
"message": "Por segurança, entre novamente para continuar de onde parou.",
|
|
39
|
+
"signIn": "Entrar novamente",
|
|
40
|
+
"title": "Sua sessão expirou"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</i18n>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -16,10 +16,13 @@ export declare const useAuth: () => {
|
|
|
16
16
|
claims: import("vue").Ref<User | undefined, User | undefined>;
|
|
17
17
|
clear: () => undefined;
|
|
18
18
|
error: import("vue").Ref<AuthError | undefined, AuthError | undefined>;
|
|
19
|
+
expire: () => void;
|
|
19
20
|
fail: (reason: AuthError) => Promise<void>;
|
|
20
21
|
isAuthenticated: import("vue").ComputedRef<boolean>;
|
|
22
|
+
isExpired: import("vue").Ref<boolean, boolean>;
|
|
21
23
|
isLoading: import("vue").Ref<boolean, boolean>;
|
|
22
24
|
logout: () => any;
|
|
25
|
+
relogin: () => any;
|
|
23
26
|
retry: () => Promise<void>;
|
|
24
27
|
signup: ({ redirectTo }?: {
|
|
25
28
|
redirectTo?: string;
|
|
@@ -7,10 +7,16 @@ export const useAuth = () => {
|
|
|
7
7
|
const token = useState("auth:token", () => null);
|
|
8
8
|
const isLoading = useState("auth:loading", () => true);
|
|
9
9
|
const error = useState("auth:error", () => void 0);
|
|
10
|
+
const isExpired = useState("auth:expired", () => false);
|
|
10
11
|
const isAuthenticated = computed(() => Boolean(claims.value?.sub));
|
|
11
12
|
const user = useBootstrapStage(nuxtApp, "user");
|
|
12
13
|
const clear = () => error.value = void 0;
|
|
13
14
|
const logout = () => nuxtApp.$auth0.logout();
|
|
15
|
+
const expire = () => {
|
|
16
|
+
token.value = null;
|
|
17
|
+
isExpired.value = true;
|
|
18
|
+
};
|
|
19
|
+
const relogin = () => nuxtApp.$auth0.loginWithRedirect({ appState: { target: location.pathname + location.search } });
|
|
14
20
|
const signup = ({ redirectTo = "/" } = {}) => nuxtApp.$auth0.loginWithRedirect({
|
|
15
21
|
appState: { target: redirectTo },
|
|
16
22
|
authorizationParams: { screen_hint: "signup" }
|
|
@@ -33,10 +39,13 @@ export const useAuth = () => {
|
|
|
33
39
|
claims,
|
|
34
40
|
clear,
|
|
35
41
|
error,
|
|
42
|
+
expire,
|
|
36
43
|
fail,
|
|
37
44
|
isAuthenticated,
|
|
45
|
+
isExpired,
|
|
38
46
|
isLoading,
|
|
39
47
|
logout,
|
|
48
|
+
relogin,
|
|
40
49
|
retry,
|
|
41
50
|
signup,
|
|
42
51
|
token,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from 'vue';
|
|
1
2
|
export interface ColumnDefinition<C = unknown> {
|
|
2
3
|
class?: string;
|
|
3
4
|
getter?: (row: C) => unknown;
|
|
@@ -44,5 +45,5 @@ interface OptionArgs<D> {
|
|
|
44
45
|
search?: string;
|
|
45
46
|
total?: number;
|
|
46
47
|
}
|
|
47
|
-
export declare function useDatagrid<D = unknown>(
|
|
48
|
+
export declare function useDatagrid<D = unknown>(options: MaybeRefOrGetter<OptionArgs<D>>): DataGrid<D>;
|
|
48
49
|
export {};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { reactive } from "vue";
|
|
2
|
-
export function useDatagrid(
|
|
1
|
+
import { computed, isRef, reactive, toValue } from "vue";
|
|
2
|
+
export function useDatagrid(options) {
|
|
3
|
+
const params = toValue(options);
|
|
4
|
+
const isDynamic = typeof options === "function" || isRef(options);
|
|
5
|
+
const columns = isDynamic ? computed(() => toValue(options).columns) : params.columns;
|
|
3
6
|
const state = reactive({
|
|
4
|
-
columns
|
|
7
|
+
columns,
|
|
5
8
|
error: params.error ?? null,
|
|
6
9
|
fn: {
|
|
7
|
-
addItem(item,
|
|
8
|
-
const mode =
|
|
10
|
+
addItem(item, options2 = {}) {
|
|
11
|
+
const mode = options2.mode ?? "append";
|
|
9
12
|
if (mode === "prepend") {
|
|
10
13
|
state.items.unshift(item);
|
|
11
14
|
} else if (mode === "append") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { push } from "notivue";
|
|
2
|
-
import {
|
|
2
|
+
import { ref } from "vue";
|
|
3
3
|
import { useModal } from "bootstrap-vue-next";
|
|
4
4
|
export function useNotify() {
|
|
5
5
|
const $modal = useModal();
|
|
@@ -26,14 +26,13 @@ export function useNotify() {
|
|
|
26
26
|
instance.show();
|
|
27
27
|
}
|
|
28
28
|
function confirm(options) {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
29
|
+
const setBusy = (busy) => props.value = { ...props.value, busy, noCloseOnEsc: busy };
|
|
30
|
+
const props = ref({
|
|
31
31
|
body: options.message,
|
|
32
|
-
busy:
|
|
33
|
-
// disables both buttons
|
|
32
|
+
busy: false,
|
|
34
33
|
cancelTitle: options.cancelText || "Cancel",
|
|
35
34
|
cancelVariant: "secondary",
|
|
36
|
-
noCloseOnEsc:
|
|
35
|
+
noCloseOnEsc: false,
|
|
37
36
|
okTitle: options.confirmText || "OK",
|
|
38
37
|
okVariant: options.variant || "primary",
|
|
39
38
|
onHidden: () => void instance.destroy(),
|
|
@@ -42,11 +41,12 @@ export function useNotify() {
|
|
|
42
41
|
const result = options.onConfirm?.();
|
|
43
42
|
if (!(result instanceof Promise)) return;
|
|
44
43
|
event.preventDefault();
|
|
45
|
-
|
|
46
|
-
result.then((value) => value !== false && instance.hide("ok")).finally(() =>
|
|
44
|
+
setBusy(true);
|
|
45
|
+
result.then((value) => value !== false && instance.hide("ok")).finally(() => setBusy(false));
|
|
47
46
|
},
|
|
48
47
|
title: options.title || "Confirm"
|
|
49
|
-
})
|
|
48
|
+
});
|
|
49
|
+
const instance = $modal.create(props);
|
|
50
50
|
instance.show();
|
|
51
51
|
}
|
|
52
52
|
return { toast, alert, confirm };
|
|
@@ -7,13 +7,16 @@ function serverBaseURL(api, global) {
|
|
|
7
7
|
return prefix ? `${api.url.replace(/\/+$/, "")}/${prefix}` : api.url;
|
|
8
8
|
}
|
|
9
9
|
export default defineNuxtPlugin(({ provide }) => {
|
|
10
|
-
const requestHost = import.meta.server ? useRequestURL().host : void 0;
|
|
10
|
+
const requestHost = import.meta.server ? useRequestURL({ xForwardedHost: true }).host : void 0;
|
|
11
11
|
if (import.meta.server && import.meta.dev && new URL(useRuntimeConfig().public.merkaly.api.url).hostname.endsWith(".test")) {
|
|
12
12
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
13
13
|
}
|
|
14
14
|
const nuxtApp = useNuxtApp();
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const renewed = /* @__PURE__ */ new WeakSet();
|
|
16
|
+
const api = async (url, options = {}) => {
|
|
17
|
+
const { claims, expire, isExpired, isLoading, token } = useAuth();
|
|
18
|
+
const canRenew = import.meta.client && Boolean(claims.value) && !isExpired.value && !renewed.has(options);
|
|
19
|
+
let unauthorized = false;
|
|
17
20
|
if (import.meta.client && token.value) await nuxtApp.$auth0?.getTokenSilently();
|
|
18
21
|
const { id: tenant } = useOrganization();
|
|
19
22
|
const { public: { merkaly } } = useRuntimeConfig();
|
|
@@ -27,6 +30,7 @@ This surface has requiresTenant: true. Possible causes:
|
|
|
27
30
|
3. The adapter should use { global: true } to bypass tenant enforcement.`
|
|
28
31
|
);
|
|
29
32
|
}
|
|
33
|
+
let result;
|
|
30
34
|
await $fetch(url, {
|
|
31
35
|
baseURL: import.meta.server ? serverBaseURL(merkaly.api, options.global) : "/api",
|
|
32
36
|
body: options?.body,
|
|
@@ -45,8 +49,8 @@ This surface has requiresTenant: true. Possible causes:
|
|
|
45
49
|
if (options.meta) options.meta.value = {};
|
|
46
50
|
if (options.error) options.error.value = void 0;
|
|
47
51
|
if (options.loading) options.loading.value = true;
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
52
|
+
const result2 = await Promise.resolve(options?.onBeforeSend?.({ body: options.body, headers: options.headers, query: options.query })).then((res) => res).catch(() => false);
|
|
53
|
+
if (result2 === false) {
|
|
50
54
|
options.controller?.abort?.("Request aborted by onBeforeSend");
|
|
51
55
|
}
|
|
52
56
|
},
|
|
@@ -59,8 +63,11 @@ This surface has requiresTenant: true. Possible causes:
|
|
|
59
63
|
onResponse: async ({ response, request }) => {
|
|
60
64
|
const { status, _data, headers } = response;
|
|
61
65
|
const { data, meta } = _data || {};
|
|
66
|
+
unauthorized = status === 401 && canRenew;
|
|
67
|
+
if (unauthorized) return;
|
|
62
68
|
await options?.onResponse?.({ response, request });
|
|
63
69
|
if (status >= 400) return;
|
|
70
|
+
result = data;
|
|
64
71
|
await options?.onSuccess?.({ data, meta, headers, params: options.params });
|
|
65
72
|
if (options.data) options.data.value = data;
|
|
66
73
|
if (options.meta) options.meta.value = meta;
|
|
@@ -68,7 +75,9 @@ This surface has requiresTenant: true. Possible causes:
|
|
|
68
75
|
if (options.loading) options.loading.value = false;
|
|
69
76
|
},
|
|
70
77
|
onResponseError: async ({ response, request }) => {
|
|
71
|
-
const { _data } = response;
|
|
78
|
+
const { _data, status } = response;
|
|
79
|
+
if (unauthorized) return;
|
|
80
|
+
if (status === 401 && renewed.has(options) && !isLoading.value) expire();
|
|
72
81
|
await options.onError?.(_data, { response, request });
|
|
73
82
|
if (options.error) options.error.value = _data;
|
|
74
83
|
await options?.onComplete?.({ response, request });
|
|
@@ -77,6 +86,11 @@ This surface has requiresTenant: true. Possible causes:
|
|
|
77
86
|
query: options?.query,
|
|
78
87
|
retry: false,
|
|
79
88
|
signal: options.controller?.signal
|
|
80
|
-
}).catch((
|
|
81
|
-
|
|
89
|
+
}).catch(() => void 0);
|
|
90
|
+
if (!unauthorized) return result;
|
|
91
|
+
const retry = { ...options };
|
|
92
|
+
renewed.add(retry);
|
|
93
|
+
return nuxtApp.$auth0.getTokenSilently({ cacheMode: "off" }).then(() => api(url, retry));
|
|
94
|
+
};
|
|
95
|
+
provide("api", api);
|
|
82
96
|
});
|
|
@@ -15,7 +15,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
15
15
|
domain: $config.merkaly.auth0.domain,
|
|
16
16
|
useRefreshTokens: true
|
|
17
17
|
});
|
|
18
|
-
const { claims, error, isLoading, token, fail } = useAuth();
|
|
18
|
+
const { claims, error, expire, isLoading, token, fail } = useAuth();
|
|
19
19
|
const isStarting = useState("auth:starting", () => false);
|
|
20
20
|
const self0 = Object.assign(Object.create(Object.getPrototypeOf(auth0)), auth0);
|
|
21
21
|
let tokenError;
|
|
@@ -25,6 +25,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
25
25
|
return token.value = result;
|
|
26
26
|
}).catch((err) => {
|
|
27
27
|
tokenError = err;
|
|
28
|
+
if (token.value && NO_SESSION_ERRORS.includes(err.error)) return expire();
|
|
28
29
|
console.warn("[Auth0] getTokenSilently failed \u2013 fallback, user logged in?", err);
|
|
29
30
|
});
|
|
30
31
|
const resolveIdentity = () => Promise.allSettled([auth0.getUser(), auth0.getTokenSilently()]);
|
|
@@ -94,12 +95,19 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
94
95
|
throw new Error("Failed to get ID token for linking");
|
|
95
96
|
}
|
|
96
97
|
const { $api } = useNuxtApp();
|
|
97
|
-
|
|
98
|
+
let failure;
|
|
99
|
+
const linked = await $api("/auth/identities", {
|
|
98
100
|
// the api verifies it and derives the identity
|
|
99
101
|
body: { idToken: idClaims.__raw },
|
|
100
102
|
global: true,
|
|
101
|
-
method: "POST"
|
|
103
|
+
method: "POST",
|
|
104
|
+
onError: (reason) => void (failure = reason ?? {}),
|
|
105
|
+
// api error body
|
|
106
|
+
onFatal: (reason) => void (failure = reason ?? {})
|
|
107
|
+
// network or aborted
|
|
102
108
|
});
|
|
109
|
+
if (failure) throw new Error(String(failure.message || "Failed to link the account"));
|
|
110
|
+
return linked;
|
|
103
111
|
};
|
|
104
112
|
const isAuthCallback = window.location.pathname === callbackPath;
|
|
105
113
|
if (isAuthCallback) {
|
|
@@ -29,7 +29,7 @@ export default defineEventHandler((event) => {
|
|
|
29
29
|
}
|
|
30
30
|
const { public: $config } = useRuntimeConfig();
|
|
31
31
|
const targetOrigin = new URL($config.merkaly.api.url);
|
|
32
|
-
const url = getRequestURL(event);
|
|
32
|
+
const url = getRequestURL(event, { xForwardedHost: true });
|
|
33
33
|
const prefix = isGlobalRequest(event.node.req.headers[GLOBAL_API_HEADER]) ? "" : $config.merkaly.api.prefix;
|
|
34
34
|
const path = resolveProxyPath(prefix, url.pathname, url.search);
|
|
35
35
|
event.node.req.headers["x-forwarded-host"] = url.host;
|
|
@@ -10,7 +10,7 @@ type MerkalyAuth0Client = Auth0Client & {
|
|
|
10
10
|
declare module '#app' {
|
|
11
11
|
interface NuxtApp {
|
|
12
12
|
$auth0: MerkalyAuth0Client;
|
|
13
|
-
$api: <T = unknown>(url: string, options?: ApiOptions) => Promise<T>;
|
|
13
|
+
$api: <T = unknown>(url: string, options?: ApiOptions) => Promise<T | undefined>; // undefined when the request fails
|
|
14
14
|
$gmap: never;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -25,7 +25,7 @@ declare module '#app' {
|
|
|
25
25
|
declare module 'vue' {
|
|
26
26
|
interface ComponentCustomProperties {
|
|
27
27
|
$auth0: MerkalyAuth0Client;
|
|
28
|
-
$api: <T = unknown>(url: string, options?: ApiOptions) => Promise<T>;
|
|
28
|
+
$api: <T = unknown>(url: string, options?: ApiOptions) => Promise<T | undefined>; // undefined when the request fails
|
|
29
29
|
$gmap: never;
|
|
30
30
|
}
|
|
31
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@merkaly/nuxt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/merkaly-io/nuxt.git"
|
|
@@ -52,9 +52,11 @@
|
|
|
52
52
|
"change-case": "^5.4.4",
|
|
53
53
|
"class-transformer": "^0.5.1",
|
|
54
54
|
"class-validator": "^0.15.1",
|
|
55
|
+
"defu": "^6.1.7",
|
|
55
56
|
"eslint": "^10.14.0",
|
|
56
57
|
"filesize": "^11.0.17",
|
|
57
58
|
"gsap": "^3.15.0",
|
|
59
|
+
"jiti": "^2.7.0",
|
|
58
60
|
"notivue": "^2.4.5",
|
|
59
61
|
"nuxt": "^4.6.3",
|
|
60
62
|
"posthog-js": "^1.447.1",
|
|
@@ -63,19 +65,16 @@
|
|
|
63
65
|
"typescript": "~5.9.3",
|
|
64
66
|
"v-money3": "^3.26.1",
|
|
65
67
|
"vite-svg-loader": "^5.1.1",
|
|
66
|
-
"vue": "^3.5.
|
|
68
|
+
"vue": "^3.5.45",
|
|
67
69
|
"vue-router": "^5.4.0",
|
|
68
70
|
"vue-select": "4.0.0-beta.6"
|
|
69
71
|
},
|
|
70
72
|
"devDependencies": {
|
|
71
73
|
"@nuxt/module-builder": "^1.0.2",
|
|
72
|
-
"@nuxt/schema": "^4.6.
|
|
74
|
+
"@nuxt/schema": "^4.6.4",
|
|
73
75
|
"changelogen": "^0.6.2",
|
|
74
76
|
"vue-tsc": "^3.3.4"
|
|
75
77
|
},
|
|
76
|
-
"optionalDependencies": {
|
|
77
|
-
"defu": "^6.1.7"
|
|
78
|
-
},
|
|
79
78
|
"engines": {
|
|
80
79
|
"node": ">=24"
|
|
81
80
|
},
|