@merkaly/nuxt 0.12.5 → 0.13.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/README.md +122 -6
- package/dist/module.d.mts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -0
- package/dist/runtime/components/app.vue +2 -2
- package/dist/runtime/components/auth/AuthError.vue +9 -2
- package/dist/runtime/components/format/FormatDate.d.vue.ts +2 -2
- package/dist/runtime/components/format/FormatDate.vue.d.ts +2 -2
- package/dist/runtime/components/format/FormatIcon.d.vue.ts +2 -2
- package/dist/runtime/components/format/FormatIcon.vue.d.ts +2 -2
- package/dist/runtime/composables/useAuth.d.ts +9 -2
- package/dist/runtime/composables/useAuth.js +5 -2
- package/dist/runtime/composables/useOrganization.d.ts +13 -0
- package/dist/runtime/composables/useOrganization.js +13 -0
- package/dist/runtime/internal/bootstrap.d.ts +2 -0
- package/dist/runtime/internal/bootstrap.js +13 -0
- package/dist/runtime/internal/state.d.ts +45 -0
- package/dist/runtime/internal/state.js +51 -0
- package/dist/runtime/plugins/api.global.js +3 -3
- package/dist/runtime/plugins/auth0.client.js +15 -12
- package/dist/runtime/plugins/bootstrap.server.d.ts +3 -0
- package/dist/runtime/plugins/bootstrap.server.js +6 -0
- package/dist/runtime/plugins/posthog.client.js +3 -3
- package/dist/runtime/types/nuxt.d.ts +2 -2
- package/dist/runtime/utils/defineBootstrap.d.ts +2 -0
- package/dist/runtime/utils/defineBootstrap.js +3 -0
- package/dist/types.d.mts +2 -0
- package/package.json +1 -1
- package/dist/runtime/composables/useTenant.d.ts +0 -8
- package/dist/runtime/composables/useTenant.js +0 -17
package/README.md
CHANGED
|
@@ -26,14 +26,130 @@ Install the module to your Nuxt application:
|
|
|
26
26
|
|
|
27
27
|
Every surface can sign in; `auth0.requiresAuth` only decides whether pages require a login (globally, or per page with `definePageMeta({ requiresAuth: true })`). `auth0.requiresTenant` decides whether API calls require a tenant (`global: true` adapters bypass it).
|
|
28
28
|
|
|
29
|
-
On boot the module restores an existing session (
|
|
29
|
+
On boot the module restores an existing session (Auth0 claims and token) and then runs the bootstrap stages in a fixed order, each one only after the previous one finished:
|
|
30
|
+
|
|
31
|
+
1. **session**: the app hooks `merkaly:tenant` then `merkaly:auth`, with those claims (or none).
|
|
32
|
+
2. **organization**: the app's organization loader (see `defineBootstrap`), always, signed in or not.
|
|
33
|
+
3. **user**: the app's `/auth/me` loader, only when the visitor is signed in.
|
|
34
|
+
|
|
35
|
+
The app mounts (and `MKApp` renders its default slot) only after the last stage, so pages, layouts and their requests start with the session, the organization and the user already loaded. A failing stage records the failure with `useAuth().fail()` (stage `tenant`, `organization` or `user`; never a logout) and `MKApp` renders the `error` slot; `retry()` runs again only the stages that did not succeed. The auth callback page renders right away so it can complete the login, and runs the stages after it.
|
|
30
36
|
|
|
31
37
|
| Surface | Login | Tenant | Boot |
|
|
32
38
|
|---|---|---|---|
|
|
33
|
-
| admin | required | required | `merkaly:tenant` creates the session and sets the tenant;
|
|
34
|
-
| dashboard | required | not used | `merkaly:auth` loads the session;
|
|
35
|
-
| store | optional | required | `merkaly:tenant` resolves the store from the host (public); a signed-in buyer
|
|
36
|
-
| www | optional | not used | No hooks; anonymous or signed-in. |
|
|
39
|
+
| admin | required | required | `merkaly:tenant` creates the session and sets the tenant; then organization (`/settings/organization`) and user (`/auth/me`). Without a session the auth middleware redirects to login. |
|
|
40
|
+
| dashboard | required | not used | `merkaly:auth` loads the session; then user if it declares a loader. Without a session it redirects to login. |
|
|
41
|
+
| store | optional | required | `merkaly:tenant` resolves the store from the host (public); then organization (`/company`, public, also during SSR); user only for a signed-in buyer. Protected pages (account, checkout) ask for login. |
|
|
42
|
+
| www | optional | not used | No hooks and no loaders; anonymous or signed-in. |
|
|
43
|
+
|
|
44
|
+
### Ordered bootstrap (`defineBootstrap`, `useOrganization`, `useAuth().user`)
|
|
45
|
+
|
|
46
|
+
Each app declares its loaders once, from a plugin, with its own adapters (the module has no app endpoints):
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// admin: app/plugins/bootstrap.ts
|
|
50
|
+
import { readAccountProfile } from '~/adapters/account/read.account.profile';
|
|
51
|
+
import { readSettingsOrganization } from '~/adapters/settings/organization/read.settings.organization';
|
|
52
|
+
|
|
53
|
+
export default defineNuxtPlugin(() => {
|
|
54
|
+
defineBootstrap({
|
|
55
|
+
organization: () => readSettingsOrganization(() => ({ immediate: false })),
|
|
56
|
+
user: () => readAccountProfile(() => ({ immediate: false })),
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// store: app/plugins/bootstrap.ts (universal, not .client: SSR loads the organization too)
|
|
63
|
+
import { readAppCompany } from '~/adapters/app/read.company';
|
|
64
|
+
|
|
65
|
+
export default defineNuxtPlugin(() => {
|
|
66
|
+
defineBootstrap({ organization: () => readAppCompany(() => ({ immediate: false })) });
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A loader returns an adapter (`withAdapter()` result, preferably with `immediate: false`, the bootstrap runs it), a promise or a plain value. An adapter with an api error, or a rejected promise, fails the stage. Every stage is optional: an app without loaders skips it.
|
|
71
|
+
|
|
72
|
+
Two composables hold the state, shared by the whole app:
|
|
73
|
+
|
|
74
|
+
- `useOrganization()`: everything about the organization (tenant). It exists without a login and also loads on the server.
|
|
75
|
+
- `useAuth()`: everything about the user: `user`, the loaded profile (`/auth/me`), and `claims`, the Auth0 ID token user (`sub`, `email`, `name`, `picture`, custom claims), plus `token`, `isAuthenticated` (from `claims.sub`), `fail`, `retry`, `logout`, `signup`.
|
|
76
|
+
|
|
77
|
+
Use `claims` for what the token already carries (id, email, roles) and `user.data` for the profile the api stores (own avatar, locale, saved name).
|
|
78
|
+
|
|
79
|
+
| `useOrganization()` | Description |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `id` | Organization id (Auth0 org), sent as the `identity` header; `''` until set |
|
|
82
|
+
| `setId(id, { server? })` | Sets the id; `server: true` when resolved during SSR |
|
|
83
|
+
| `hasId` | `Boolean(id)` |
|
|
84
|
+
| `fromServer` | The id was resolved during SSR: the page renders before auth |
|
|
85
|
+
| `data`, `status`, `error`, `isReady`, `refresh()` | The loaded organization, see below |
|
|
86
|
+
|
|
87
|
+
| `useAuth().user` and the stage keys of `useOrganization()` | Description |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `data` | Loaded value; `undefined` until it loads (and always for `user` with visitors) |
|
|
90
|
+
| `status` | `idle`, `pending`, `success` or `error` |
|
|
91
|
+
| `error` | `{ message, status? }` of the last failure |
|
|
92
|
+
| `isReady` | `status === 'success'` |
|
|
93
|
+
| `refresh()` | Loads it again (e.g. after saving the settings); a failure here only sets `error`, it does not block the app |
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const { data: organization, id } = useOrganization();
|
|
97
|
+
const { claims, user } = useAuth();
|
|
98
|
+
|
|
99
|
+
const name = computed(() => user.data.value?.name || claims.value?.name); // typed by MerkalyUser
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Typing (`MerkalyOrganization`, `MerkalyUser`)
|
|
103
|
+
|
|
104
|
+
`data` is typed with two interfaces exported by the package, empty by default. Each app extends them once with its own contracts (module augmentation, no generics at call sites), e.g. in `app/types/merkaly.d.ts`:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import type { AccountAuthProfile } from '@merkaly/api/account/auth';
|
|
108
|
+
import type { SettingsOrganizationProfileResponse } from '@merkaly/api/settings/organization';
|
|
109
|
+
|
|
110
|
+
declare module '@merkaly/nuxt' {
|
|
111
|
+
interface MerkalyOrganization extends SettingsOrganizationProfileResponse {}
|
|
112
|
+
interface MerkalyUser extends AccountAuthProfile {}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The file must be a module (keep at least one `import` or an `export {}`), otherwise it replaces the package types instead of extending them. The loaders passed to `defineBootstrap` are checked against the same interfaces.
|
|
117
|
+
|
|
118
|
+
In SSR apps (store) the organization stage also runs on the server, after the server plugins (`tenant.server.ts`), so it reaches the HTML; the client reuses it from the payload and does not request it again. `user` never runs on the server (there is no session there), so avoid rendering `user` data in SSR markup to keep hydration stable.
|
|
119
|
+
|
|
120
|
+
#### Migration per app (breaking: `useTenant` is gone and `useAuth().user` changed)
|
|
121
|
+
|
|
122
|
+
- `useTenant()` is removed; its keys live in `useOrganization()`: `tenant` → `id`, `setTenant(id, options)` → `setId(id, options)`, `fromServer` → `fromServer`, `isReady` (id set) → `hasId` (`isReady` now means the organization data loaded). The `merkaly:tenant` hook is unchanged: it still runs first and is where the id gets set.
|
|
123
|
+
- `useAuth().user` (the Auth0 user) is now `useAuth().claims`, same object. `useAuth().user` is the loaded profile (`data`, `status`, `error`, `isReady`, `refresh`). Every current app read of `user` is a token field (name, email, picture, sub, roles claim), so the rename is `user` → `claims`, in templates too (`user.name` → `claims.name`, `v-if="user"` → `v-if="claims"`). `isAuthenticated` is unchanged.
|
|
124
|
+
- The `merkaly:tenant` and `merkaly:auth` hooks still receive the Auth0 claims.
|
|
125
|
+
|
|
126
|
+
- **admin**
|
|
127
|
+
- `app/plugins/auth.ts:4`: `const { setTenant } = useTenant()` → `const { setId } = useOrganization()`; line 25: `setTenant(data.orgId)` → `setId(data.orgId)`.
|
|
128
|
+
- `app/plugins/money.ts:8-18`: drop `useTenant()`, the `readSettingsOrganization()` call and the `watch(tenant, ...)`; use `watch(useOrganization().data, (data) => data && setMoneyDefaults({ currency: data.localization?.currency, locale: data.localization?.language }), { immediate: true })`.
|
|
129
|
+
- New `app/plugins/bootstrap.ts` and `app/types/merkaly.d.ts` (above).
|
|
130
|
+
- `app/composables/useAccountProfile.ts:5`: `const { user } = useAuth()` → `const { claims, user } = useAuth()`; line 7: `readAccountProfile()` → `const { data: profile, refresh } = user`; line 9: `user.value?.picture` → `claims.value?.picture`.
|
|
131
|
+
- `app/components/ui/select/SelectLocale.vue:9`: `{ user }` → `{ claims }`; line 32: `user.value?.name` → `claims.value?.name`.
|
|
132
|
+
- `app/components/ui/nav/NavUser.vue:2`: `{ user }` → `{ claims }`; template lines 22, 24, 25, 29, 30, 33, 50, 54, 61: `user` / `user.name` / `user.email` → `claims` / `claims.name` / `claims.email`.
|
|
133
|
+
- `app/pages/settings/(overview)/.components/SettingsMembersSection.vue:16`: `{ user }` → `{ claims }`; line 28: `user.value?.[AUTH_CLAIM]` → `claims.value?.[AUTH_CLAIM]`; line 36: `user.value?.sub` → `claims.value?.sub`.
|
|
134
|
+
- `app/pages/profile/(overview)/.components/ProfileOverviewSection.vue:7`: `{ user }` → `{ claims }`; lines 16, 22: `user.value?.name` → `claims.value?.name`; template lines 83 and 109: `user?.name`, `user?.email` → `claims?.name`, `claims?.email`.
|
|
135
|
+
- `app/pages/profile/(overview)/.components/ProfileSignInMethodSection.vue:9`: `{ user }` → `{ claims }`; template line 74: `user?.email` → `claims?.email`.
|
|
136
|
+
- `app/pages/settings/(overview)/.composables/useSettingsOrganization.ts:5` (optional): read `useOrganization()` (`data`, `status === 'pending'`, `refresh`) instead of a second `readSettingsOrganization()`.
|
|
137
|
+
- **store**
|
|
138
|
+
- `app/plugins/tenant.server.ts:4`: `const { setTenant } = useTenant()` → `const { setId } = useOrganization()`; lines 7 and 15: `setTenant(..., { server: true })` → `setId(..., { server: true })`.
|
|
139
|
+
- `app/plugins/auth.ts:4`: `const { fromServer, setTenant } = useTenant()` → `const { fromServer, setId } = useOrganization()`; lines 9 and 13: `setTenant(...)` → `setId(...)`.
|
|
140
|
+
- `app/pages/checkout/.composables/usePaymentStream.ts:11`: `const { tenant } = useTenant()` → `const { id: tenant } = useOrganization()` (line 43 keeps `tenant.value`).
|
|
141
|
+
- `app/components/nav/NavUser.vue:8`: `user` → `claims` in the destructuring; template line 55: `user?.name` → `claims?.name`.
|
|
142
|
+
- `app/pages/checkout/index.vue:22`: `{ user }` → `{ claims }`; line 76: `watch(user, ...)` → `watch(claims, ...)`.
|
|
143
|
+
- `app/pages/account/.components/AccountUser.vue:2`: `{ user }` → `{ claims }`; line 4 and template lines 9, 10, 21, 23, 24: `user` → `claims`.
|
|
144
|
+
- New universal `app/plugins/bootstrap.ts` (above) and `app/types/merkaly.d.ts` with `MerkalyOrganization extends SettingsOrganizationStoreProfile`.
|
|
145
|
+
- `app/layouts/default.vue:5` and `app/pages/account/orders/[order]/index.vue:85`: `readAppCompany()` → `const { data: company } = useOrganization()`.
|
|
146
|
+
- `app/plugins/README.md:11` mentions `fromServer`: now from `useOrganization()`.
|
|
147
|
+
- **dashboard**
|
|
148
|
+
- `app/components/ui/nav/NavUser.vue:2`: `{ user }` → `{ claims }`; line 7 and template lines 12, 21, 28, 33, 35: `user` → `claims`.
|
|
149
|
+
- No `useTenant` usages. Add `defineBootstrap({ user })` and `MerkalyUser` only if it needs `/auth/me` before the pages.
|
|
150
|
+
- **www**
|
|
151
|
+
- `app/components/modals/ModalBookDemo.vue:11`: `{ isAuthenticated, user }` → `{ isAuthenticated, claims }`; lines 14 and 15: `user.value?.name`, `user.value?.email` → `claims.value?.name`, `claims.value?.email`.
|
|
152
|
+
- No `useTenant` usages, no loaders needed.
|
|
37
153
|
|
|
38
154
|
### Login entry points (`MKAuth`)
|
|
39
155
|
|
|
@@ -103,7 +219,7 @@ overrides it at runtime.
|
|
|
103
219
|
**Client plugin.** Initializes `posthog-js` **only in production builds**
|
|
104
220
|
(`nuxt dev` never sends events) with `api_host: '/phog'` and
|
|
105
221
|
`ui_host: 'https://us.posthog.com'`. It identifies the Auth0 user from
|
|
106
|
-
`useAuth()` (`identify(sub, { email, name })`) and calls `posthog.reset()` on
|
|
222
|
+
`useAuth().claims` (`identify(sub, { email, name })`) and calls `posthog.reset()` on
|
|
107
223
|
logout. The app can keep using `import posthog from 'posthog-js'` for
|
|
108
224
|
`capture()`: the module dedupes `posthog-js` so the app and the plugin share
|
|
109
225
|
the initialized instance (keep `posthog-js` as an app dependency).
|
package/dist/module.d.mts
CHANGED
|
@@ -3,6 +3,7 @@ import { ClientAuthorizationParams } from '@auth0/auth0-spa-js';
|
|
|
3
3
|
import { Strategies } from '@nuxtjs/i18n';
|
|
4
4
|
export { AdapterArgs, AdapterOptions } from '../dist/runtime/utils/withAdapter.js';
|
|
5
5
|
export { ApiOptions, HooksOptions, ParamsOptions, RefOptions } from '../dist/runtime/plugins/api.global.js';
|
|
6
|
+
export { BootstrapError, BootstrapLoader, BootstrapLoaders, BootstrapStatus, MerkalyOrganization, MerkalyUser } from '../dist/runtime/internal/state.js';
|
|
6
7
|
|
|
7
8
|
interface MerkalyI18nLocale {
|
|
8
9
|
code: string;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -278,6 +278,7 @@ function configureNuxtOptions(nuxt, options) {
|
|
|
278
278
|
function registerRuntimeFeatures(options, resolver) {
|
|
279
279
|
addPlugin({ src: resolver.resolve("./runtime/plugins/api.global") });
|
|
280
280
|
addPlugin({ src: resolver.resolve("./runtime/plugins/auth0.client") });
|
|
281
|
+
addPlugin({ src: resolver.resolve("./runtime/plugins/bootstrap.server") });
|
|
281
282
|
addPlugin({ src: resolver.resolve("./runtime/plugins/sentry.global") });
|
|
282
283
|
if (hasPosthogConfig(options)) {
|
|
283
284
|
addPlugin({ src: resolver.resolve("./runtime/plugins/posthog.client") });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { useRoute } from "#app";
|
|
3
3
|
import { computed } from "vue";
|
|
4
|
-
import { useAuth,
|
|
4
|
+
import { useAuth, useOrganization, watchOnce, addRouteMiddleware, useNuxtApp, useRuntimeConfig } from "#imports";
|
|
5
5
|
import AuthMiddleware from "../middleware/auth";
|
|
6
6
|
import { useNavigation } from "../composables/useNavigation";
|
|
7
7
|
import AuthError from "./auth/AuthError.vue";
|
|
@@ -9,7 +9,7 @@ import { useColorMode } from "@vueuse/core";
|
|
|
9
9
|
import { Notivue, NotificationProgress, Notification, pastelTheme } from "notivue";
|
|
10
10
|
const $route = useRoute();
|
|
11
11
|
const { clear, error, isLoading, logout, retry } = useAuth();
|
|
12
|
-
const { fromServer } =
|
|
12
|
+
const { fromServer } = useOrganization();
|
|
13
13
|
const { hook } = useNuxtApp();
|
|
14
14
|
const { public: { merkaly } } = useRuntimeConfig();
|
|
15
15
|
useColorMode({ attribute: "data-bs-theme" });
|
|
@@ -5,8 +5,15 @@ const props = defineProps({
|
|
|
5
5
|
});
|
|
6
6
|
const emit = defineEmits(["logout", "retry"]);
|
|
7
7
|
const isDenied = computed(() => [401, 403].includes(props.error.status));
|
|
8
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const isOrganization = computed(() => props.error.stage === "organization");
|
|
9
|
+
const title = computed(() => {
|
|
10
|
+
if (isDenied.value) return "Access denied";
|
|
11
|
+
return isOrganization.value ? "We could not load this organization" : "We could not start your session";
|
|
12
|
+
});
|
|
13
|
+
const description = computed(() => {
|
|
14
|
+
if (isDenied.value) return "You signed in successfully, but this account is not allowed to access this application.";
|
|
15
|
+
return isOrganization.value ? "Something failed while loading the organization of this application." : "You signed in successfully, but something failed while preparing your session.";
|
|
16
|
+
});
|
|
10
17
|
const details = computed(() => [props.error.stage, props.error.status, props.error.url].filter(Boolean).join(" \xB7 "));
|
|
11
18
|
</script>
|
|
12
19
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PropType } from 'vue';
|
|
2
2
|
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
3
3
|
date: {
|
|
4
|
-
type: (
|
|
4
|
+
type: (StringConstructor | DateConstructor)[];
|
|
5
5
|
default: () => Date;
|
|
6
6
|
};
|
|
7
7
|
format: {
|
|
@@ -10,7 +10,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|
|
10
10
|
};
|
|
11
11
|
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
12
12
|
date: {
|
|
13
|
-
type: (
|
|
13
|
+
type: (StringConstructor | DateConstructor)[];
|
|
14
14
|
default: () => Date;
|
|
15
15
|
};
|
|
16
16
|
format: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PropType } from 'vue';
|
|
2
2
|
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
3
3
|
date: {
|
|
4
|
-
type: (
|
|
4
|
+
type: (StringConstructor | DateConstructor)[];
|
|
5
5
|
default: () => Date;
|
|
6
6
|
};
|
|
7
7
|
format: {
|
|
@@ -10,7 +10,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|
|
10
10
|
};
|
|
11
11
|
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
12
12
|
date: {
|
|
13
|
-
type: (
|
|
13
|
+
type: (StringConstructor | DateConstructor)[];
|
|
14
14
|
default: () => Date;
|
|
15
15
|
};
|
|
16
16
|
format: {
|
|
@@ -18,7 +18,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
18
18
|
required: true;
|
|
19
19
|
};
|
|
20
20
|
opacity: {
|
|
21
|
-
type: (
|
|
21
|
+
type: (StringConstructor | NumberConstructor)[];
|
|
22
22
|
default: () => string;
|
|
23
23
|
};
|
|
24
24
|
provider: {
|
|
@@ -71,7 +71,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
71
71
|
required: true;
|
|
72
72
|
};
|
|
73
73
|
opacity: {
|
|
74
|
-
type: (
|
|
74
|
+
type: (StringConstructor | NumberConstructor)[];
|
|
75
75
|
default: () => string;
|
|
76
76
|
};
|
|
77
77
|
provider: {
|
|
@@ -18,7 +18,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
18
18
|
required: true;
|
|
19
19
|
};
|
|
20
20
|
opacity: {
|
|
21
|
-
type: (
|
|
21
|
+
type: (StringConstructor | NumberConstructor)[];
|
|
22
22
|
default: () => string;
|
|
23
23
|
};
|
|
24
24
|
provider: {
|
|
@@ -71,7 +71,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
71
71
|
required: true;
|
|
72
72
|
};
|
|
73
73
|
opacity: {
|
|
74
|
-
type: (
|
|
74
|
+
type: (StringConstructor | NumberConstructor)[];
|
|
75
75
|
default: () => string;
|
|
76
76
|
};
|
|
77
77
|
provider: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { User } from '@auth0/auth0-spa-js';
|
|
2
|
-
export type AuthErrorStage = 'callback' | 'token' | 'session' | 'tenant';
|
|
2
|
+
export type AuthErrorStage = 'callback' | 'token' | 'session' | 'tenant' | 'organization' | 'user';
|
|
3
3
|
export interface AuthError {
|
|
4
4
|
message: string;
|
|
5
5
|
stage: AuthErrorStage;
|
|
@@ -13,6 +13,7 @@ export interface AuthErrorContext {
|
|
|
13
13
|
retry: () => Promise<void>;
|
|
14
14
|
}
|
|
15
15
|
export declare const useAuth: () => {
|
|
16
|
+
claims: import("vue").Ref<User | undefined, User | undefined>;
|
|
16
17
|
clear: () => undefined;
|
|
17
18
|
error: import("vue").Ref<AuthError | undefined, AuthError | undefined>;
|
|
18
19
|
fail: (reason: AuthError) => Promise<void>;
|
|
@@ -24,5 +25,11 @@ export declare const useAuth: () => {
|
|
|
24
25
|
redirectTo?: string;
|
|
25
26
|
}) => any;
|
|
26
27
|
token: import("vue").Ref<string | null, string | null>;
|
|
27
|
-
user:
|
|
28
|
+
user: {
|
|
29
|
+
data: import("vue").Ref<import("../internal/state.js").MerkalyUser | undefined, import("../internal/state.js").MerkalyUser | undefined>;
|
|
30
|
+
error: import("vue").Ref<import("../internal/state.js").BootstrapError | undefined, import("../internal/state.js").BootstrapError | undefined>;
|
|
31
|
+
isReady: import("vue").ComputedRef<boolean>;
|
|
32
|
+
refresh: () => Promise<boolean>;
|
|
33
|
+
status: import("vue").Ref<import("../internal/state.js").BootstrapStatus, import("../internal/state.js").BootstrapStatus>;
|
|
34
|
+
};
|
|
28
35
|
};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useNuxtApp, useState } from "#imports";
|
|
3
|
+
import { useBootstrapStage } from "../internal/state.js";
|
|
3
4
|
export const useAuth = () => {
|
|
4
5
|
const nuxtApp = useNuxtApp();
|
|
5
|
-
const
|
|
6
|
+
const claims = useState("auth:claims", () => void 0);
|
|
6
7
|
const token = useState("auth:token", () => null);
|
|
7
8
|
const isLoading = useState("auth:loading", () => true);
|
|
8
9
|
const error = useState("auth:error", () => void 0);
|
|
9
|
-
const isAuthenticated = computed(() => Boolean(
|
|
10
|
+
const isAuthenticated = computed(() => Boolean(claims.value?.sub));
|
|
11
|
+
const user = useBootstrapStage(nuxtApp, "user");
|
|
10
12
|
const clear = () => error.value = void 0;
|
|
11
13
|
const logout = () => nuxtApp.$auth0.logout();
|
|
12
14
|
const signup = ({ redirectTo = "/" } = {}) => nuxtApp.$auth0.loginWithRedirect({
|
|
@@ -28,6 +30,7 @@ export const useAuth = () => {
|
|
|
28
30
|
await nuxtApp.callHook("merkaly:auth:error", { clear, error: reason, logout, retry });
|
|
29
31
|
};
|
|
30
32
|
return {
|
|
33
|
+
claims,
|
|
31
34
|
clear,
|
|
32
35
|
error,
|
|
33
36
|
fail,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const useOrganization: () => {
|
|
2
|
+
fromServer: import("vue").Ref<boolean, boolean>;
|
|
3
|
+
hasId: import("vue").ComputedRef<boolean>;
|
|
4
|
+
id: import("vue").Ref<string, string>;
|
|
5
|
+
setId: (value: string, options?: {
|
|
6
|
+
server?: boolean;
|
|
7
|
+
}) => void;
|
|
8
|
+
data: import("vue").Ref<import("../internal/state.js").MerkalyOrganization | undefined, import("../internal/state.js").MerkalyOrganization | undefined>;
|
|
9
|
+
error: import("vue").Ref<import("../internal/state.js").BootstrapError | undefined, import("../internal/state.js").BootstrapError | undefined>;
|
|
10
|
+
isReady: import("vue").ComputedRef<boolean>;
|
|
11
|
+
refresh: () => Promise<boolean>;
|
|
12
|
+
status: import("vue").Ref<import("../internal/state.js").BootstrapStatus, import("../internal/state.js").BootstrapStatus>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { computed, useNuxtApp, useState } from "#imports";
|
|
2
|
+
import { useBootstrapStage } from "../internal/state.js";
|
|
3
|
+
export const useOrganization = () => {
|
|
4
|
+
const id = useState("merkaly:organization:id", () => "");
|
|
5
|
+
const fromServer = useState("merkaly:organization:server", () => false);
|
|
6
|
+
const stage = useBootstrapStage(useNuxtApp(), "organization");
|
|
7
|
+
const hasId = computed(() => Boolean(id.value));
|
|
8
|
+
const setId = (value, options = {}) => {
|
|
9
|
+
id.value = value;
|
|
10
|
+
if (options.server) fromServer.value = true;
|
|
11
|
+
};
|
|
12
|
+
return { ...stage, fromServer, hasId, id, setId };
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useAuth } from "../composables/useAuth.js";
|
|
2
|
+
import { inApp, runBootstrapStage, useBootstrapState } from "./state.js";
|
|
3
|
+
export const runBootstrap = async (nuxtApp) => {
|
|
4
|
+
const { error, fail, isAuthenticated, token } = inApp(nuxtApp, useAuth);
|
|
5
|
+
if (error.value || nuxtApp.payload.error) return;
|
|
6
|
+
const failStage = (stage) => {
|
|
7
|
+
const reason = inApp(nuxtApp, () => useBootstrapState(stage)).error.value;
|
|
8
|
+
return fail({ message: reason?.message || `${stage} failed`, stage, status: reason?.status });
|
|
9
|
+
};
|
|
10
|
+
if (!await runBootstrapStage(nuxtApp, "organization")) return failStage("organization");
|
|
11
|
+
if (!isAuthenticated.value || !token.value) return;
|
|
12
|
+
if (!await runBootstrapStage(nuxtApp, "user")) return failStage("user");
|
|
13
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { NuxtApp } from '#app';
|
|
2
|
+
import type { Ref } from '#imports';
|
|
3
|
+
export interface MerkalyOrganization {
|
|
4
|
+
}
|
|
5
|
+
export interface MerkalyUser {
|
|
6
|
+
}
|
|
7
|
+
export type BootstrapStage = 'organization' | 'user';
|
|
8
|
+
export type BootstrapStatus = 'idle' | 'pending' | 'success' | 'error';
|
|
9
|
+
export interface BootstrapError {
|
|
10
|
+
message: string;
|
|
11
|
+
status?: number;
|
|
12
|
+
}
|
|
13
|
+
interface AdapterLike<T> {
|
|
14
|
+
data: Ref<T>;
|
|
15
|
+
error: Ref<unknown>;
|
|
16
|
+
execute: () => Promise<void>;
|
|
17
|
+
loading: Ref<boolean>;
|
|
18
|
+
}
|
|
19
|
+
export type BootstrapLoader<T = unknown> = () => T | Promise<T> | AdapterLike<T>;
|
|
20
|
+
export interface BootstrapLoaders {
|
|
21
|
+
organization?: BootstrapLoader<MerkalyOrganization>;
|
|
22
|
+
user?: BootstrapLoader<MerkalyUser>;
|
|
23
|
+
}
|
|
24
|
+
interface StageData {
|
|
25
|
+
organization: MerkalyOrganization;
|
|
26
|
+
user: MerkalyUser;
|
|
27
|
+
}
|
|
28
|
+
export declare const inApp: <T>(nuxtApp: NuxtApp, fn: () => T) => T;
|
|
29
|
+
export declare const registerBootstrapLoaders: (nuxtApp: NuxtApp, loaders: BootstrapLoaders) => void;
|
|
30
|
+
export declare const useBootstrapState: <S extends BootstrapStage>(stage: S) => {
|
|
31
|
+
data: Ref<StageData[S] | undefined, StageData[S] | undefined>;
|
|
32
|
+
error: Ref<BootstrapError | undefined, BootstrapError | undefined>;
|
|
33
|
+
status: Ref<BootstrapStatus, BootstrapStatus>;
|
|
34
|
+
};
|
|
35
|
+
export declare const runBootstrapStage: (nuxtApp: NuxtApp, stage: BootstrapStage, { force }?: {
|
|
36
|
+
force?: boolean | undefined;
|
|
37
|
+
}) => Promise<boolean>;
|
|
38
|
+
export declare const useBootstrapStage: <S extends BootstrapStage>(nuxtApp: NuxtApp, stage: S) => {
|
|
39
|
+
data: Ref<StageData[S] | undefined, StageData[S] | undefined>;
|
|
40
|
+
error: Ref<BootstrapError | undefined, BootstrapError | undefined>;
|
|
41
|
+
isReady: import("vue").ComputedRef<boolean>;
|
|
42
|
+
refresh: () => Promise<boolean>;
|
|
43
|
+
status: Ref<BootstrapStatus, BootstrapStatus>;
|
|
44
|
+
};
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { computed, useState } from "#imports";
|
|
2
|
+
import { until } from "@vueuse/core";
|
|
3
|
+
export const inApp = (nuxtApp, fn) => nuxtApp.runWithContext(fn);
|
|
4
|
+
const registry = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
export const registerBootstrapLoaders = (nuxtApp, loaders) => {
|
|
6
|
+
registry.set(nuxtApp, { ...registry.get(nuxtApp), ...loaders });
|
|
7
|
+
};
|
|
8
|
+
export const useBootstrapState = (stage) => ({
|
|
9
|
+
data: useState(`merkaly:${stage}:data`, () => void 0),
|
|
10
|
+
error: useState(`merkaly:${stage}:error`, () => void 0),
|
|
11
|
+
status: useState(`merkaly:${stage}:status`, () => "idle")
|
|
12
|
+
});
|
|
13
|
+
const isAdapter = (value) => Boolean(value && typeof value === "object" && "execute" in value && "loading" in value);
|
|
14
|
+
const toError = (reason) => {
|
|
15
|
+
const value = reason;
|
|
16
|
+
return {
|
|
17
|
+
message: String(value?.message || reason || "Request failed"),
|
|
18
|
+
status: Number(value?.statusCode ?? value?.status) || void 0
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const resolveLoader = async (nuxtApp, loader) => {
|
|
22
|
+
const result = await nuxtApp.runWithContext(loader);
|
|
23
|
+
if (!isAdapter(result)) return result;
|
|
24
|
+
if (result.loading.value) await until(result.loading).toBe(false);
|
|
25
|
+
else await result.execute();
|
|
26
|
+
if (result.error.value) throw result.error.value;
|
|
27
|
+
return result.data.value;
|
|
28
|
+
};
|
|
29
|
+
export const runBootstrapStage = (nuxtApp, stage, { force = false } = {}) => {
|
|
30
|
+
const loader = registry.get(nuxtApp)?.[stage];
|
|
31
|
+
const { data, error, status } = inApp(nuxtApp, () => useBootstrapState(stage));
|
|
32
|
+
if (!loader) return Promise.resolve(true);
|
|
33
|
+
if (status.value === "success" && !force) return Promise.resolve(true);
|
|
34
|
+
status.value = "pending";
|
|
35
|
+
error.value = void 0;
|
|
36
|
+
return resolveLoader(nuxtApp, loader).then((value) => {
|
|
37
|
+
data.value = value;
|
|
38
|
+
status.value = "success";
|
|
39
|
+
return true;
|
|
40
|
+
}).catch((reason) => {
|
|
41
|
+
error.value = toError(reason);
|
|
42
|
+
status.value = "error";
|
|
43
|
+
return false;
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
export const useBootstrapStage = (nuxtApp, stage) => {
|
|
47
|
+
const { data, error, status } = useBootstrapState(stage);
|
|
48
|
+
const isReady = computed(() => status.value === "success");
|
|
49
|
+
const refresh = () => runBootstrapStage(nuxtApp, stage, { force: true });
|
|
50
|
+
return { data, error, isReady, refresh, status };
|
|
51
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineNuxtPlugin, useNuxtApp, useRequestURL, useRuntimeConfig } from "#app";
|
|
2
|
-
import { useAuth,
|
|
2
|
+
import { useAuth, useOrganization } from "#imports";
|
|
3
3
|
const GLOBAL_API_HEADER = "x-merkaly-global";
|
|
4
4
|
function serverBaseURL(api, global) {
|
|
5
5
|
const prefix = global ? "" : (api.prefix ?? "").replace(/^\/+|\/+$/g, "");
|
|
@@ -14,7 +14,7 @@ export default defineNuxtPlugin(({ provide }) => {
|
|
|
14
14
|
provide("api", async (url, options = {}) => {
|
|
15
15
|
const { token } = useAuth();
|
|
16
16
|
if (import.meta.client && token.value) await nuxtApp.$auth0?.getTokenSilently();
|
|
17
|
-
const { tenant } =
|
|
17
|
+
const { id: tenant } = useOrganization();
|
|
18
18
|
const { public: { merkaly } } = useRuntimeConfig();
|
|
19
19
|
const requiresTenant = options.requiresTenant ?? merkaly.auth0.requiresTenant;
|
|
20
20
|
if (!tenant.value && !options.global && requiresTenant) {
|
|
@@ -22,7 +22,7 @@ export default defineNuxtPlugin(({ provide }) => {
|
|
|
22
22
|
`[api] Tenant context is required but not set for: ${url}
|
|
23
23
|
This surface has requiresTenant: true. Possible causes:
|
|
24
24
|
1. The request fired before the auth boot sequence completed (race condition).
|
|
25
|
-
2.
|
|
25
|
+
2. useOrganization().setId() was never called in this surface's auth plugin.
|
|
26
26
|
3. The adapter should use { global: true } to bypass tenant enforcement.`
|
|
27
27
|
);
|
|
28
28
|
}
|
|
@@ -3,8 +3,10 @@ import { defineNuxtPlugin, useRuntimeConfig, useNuxtApp, useState } from "#impor
|
|
|
3
3
|
import { navigateTo } from "#app";
|
|
4
4
|
import { defu } from "defu";
|
|
5
5
|
import { useAuth } from "../composables/useAuth.js";
|
|
6
|
+
import { runBootstrap } from "../internal/bootstrap.js";
|
|
6
7
|
const NO_SESSION_ERRORS = ["consent_required", "interaction_required", "invalid_grant", "login_required", "missing_refresh_token"];
|
|
7
|
-
export default defineNuxtPlugin(async (
|
|
8
|
+
export default defineNuxtPlugin(async (nuxtApp) => {
|
|
9
|
+
const { callHook, hook } = nuxtApp;
|
|
8
10
|
const { public: $config } = useRuntimeConfig();
|
|
9
11
|
const callbackPath = URL.canParse($config.merkaly.auth0.callbackUrl) ? new URL($config.merkaly.auth0.callbackUrl).pathname : $config.merkaly.auth0.callbackUrl;
|
|
10
12
|
const auth0 = await createAuth0Client({
|
|
@@ -13,11 +15,11 @@ export default defineNuxtPlugin(async ({ callHook, hook }) => {
|
|
|
13
15
|
domain: $config.merkaly.auth0.domain,
|
|
14
16
|
useRefreshTokens: true
|
|
15
17
|
});
|
|
16
|
-
const { error, isLoading,
|
|
18
|
+
const { claims, error, isLoading, token, fail } = useAuth();
|
|
17
19
|
const isStarting = useState("auth:starting", () => false);
|
|
18
20
|
const self0 = Object.assign(Object.create(Object.getPrototypeOf(auth0)), auth0);
|
|
19
21
|
let tokenError;
|
|
20
|
-
auth0.getUser = () => self0.getUser().then((result) =>
|
|
22
|
+
auth0.getUser = () => self0.getUser().then((result) => claims.value = result).catch((reason) => console.error("[Auth0] getUser failed", reason));
|
|
21
23
|
auth0.getTokenSilently = (options = {}) => self0.getTokenSilently(defu({ authorizationParams: { audience: $config.merkaly.auth0.audience } }, options)).then((result) => {
|
|
22
24
|
tokenError = void 0;
|
|
23
25
|
return token.value = result;
|
|
@@ -28,26 +30,27 @@ export default defineNuxtPlugin(async ({ callHook, hook }) => {
|
|
|
28
30
|
const resolveIdentity = () => Promise.allSettled([auth0.getUser(), auth0.getTokenSilently()]);
|
|
29
31
|
const initTenantContext = async () => {
|
|
30
32
|
try {
|
|
31
|
-
await callHook("merkaly:tenant",
|
|
32
|
-
await callHook("merkaly:auth",
|
|
33
|
+
await callHook("merkaly:tenant", claims.value);
|
|
34
|
+
await callHook("merkaly:auth", claims.value);
|
|
33
35
|
} catch (reason) {
|
|
34
36
|
if (!error.value) {
|
|
35
37
|
await fail({ message: reason?.message || String(reason), stage: "tenant" });
|
|
36
38
|
}
|
|
37
39
|
}
|
|
40
|
+
await runBootstrap(nuxtApp);
|
|
38
41
|
};
|
|
39
42
|
const startSession = async () => {
|
|
40
|
-
if (
|
|
43
|
+
if (claims.value && !token.value && tokenError && !NO_SESSION_ERRORS.includes(tokenError.error)) {
|
|
41
44
|
return fail({ message: tokenError.error_description || tokenError.message, stage: "token" });
|
|
42
45
|
}
|
|
43
|
-
if (
|
|
44
|
-
|
|
46
|
+
if (claims.value && !token.value && tokenError) {
|
|
47
|
+
claims.value = void 0;
|
|
45
48
|
await self0.logout({ openUrl: false });
|
|
46
49
|
if ($config.merkaly.auth0.requiresAuth) {
|
|
47
50
|
return auth0.loginWithRedirect({ appState: { target: location.pathname + location.search } });
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
|
-
if (!
|
|
53
|
+
if (!claims.value && $config.merkaly.auth0.requiresAuth) {
|
|
51
54
|
return auth0.loginWithRedirect({ appState: { target: location.pathname + location.search } });
|
|
52
55
|
}
|
|
53
56
|
return initTenantContext();
|
|
@@ -86,14 +89,14 @@ export default defineNuxtPlugin(async ({ callHook, hook }) => {
|
|
|
86
89
|
auth0.linkWithConnection = async (connection) => {
|
|
87
90
|
const client = getLinkingClient();
|
|
88
91
|
await client.loginWithPopup({ authorizationParams: { connection } });
|
|
89
|
-
const
|
|
90
|
-
if (!
|
|
92
|
+
const idClaims = await client.getIdTokenClaims();
|
|
93
|
+
if (!idClaims?.__raw) {
|
|
91
94
|
throw new Error("Failed to get ID token for linking");
|
|
92
95
|
}
|
|
93
96
|
const { $api } = useNuxtApp();
|
|
94
97
|
return await $api("/auth/identities", {
|
|
95
98
|
// the api verifies it and derives the identity
|
|
96
|
-
body: { idToken:
|
|
99
|
+
body: { idToken: idClaims.__raw },
|
|
97
100
|
global: true,
|
|
98
101
|
method: "POST"
|
|
99
102
|
});
|
|
@@ -14,9 +14,9 @@ export default defineNuxtPlugin(() => {
|
|
|
14
14
|
// first-party proxy
|
|
15
15
|
ui_host: "https://us.posthog.com"
|
|
16
16
|
});
|
|
17
|
-
const {
|
|
18
|
-
watchImmediate(() =>
|
|
19
|
-
if (sub) return posthog.identify(sub, { email:
|
|
17
|
+
const { claims } = useAuth();
|
|
18
|
+
watchImmediate(() => claims.value?.sub, (sub, previous) => {
|
|
19
|
+
if (sub) return posthog.identify(sub, { email: claims.value?.email, name: claims.value?.name });
|
|
20
20
|
if (previous) posthog.reset();
|
|
21
21
|
});
|
|
22
22
|
});
|
|
@@ -15,10 +15,10 @@ declare module '#app' {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
interface RuntimeNuxtHooks {
|
|
18
|
-
'merkaly:auth': (
|
|
18
|
+
'merkaly:auth': (claims?: User) => HookResult;
|
|
19
19
|
'merkaly:auth:error': (context: AuthErrorContext) => HookResult;
|
|
20
20
|
'merkaly:auth:retry': () => HookResult;
|
|
21
|
-
'merkaly:tenant': (
|
|
21
|
+
'merkaly:tenant': (claims?: User) => HookResult;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
package/dist/types.d.mts
CHANGED
|
@@ -8,6 +8,8 @@ export { type AdapterArgs, type AdapterOptions } from '../dist/runtime/utils/wit
|
|
|
8
8
|
|
|
9
9
|
export { type ApiOptions, type HooksOptions, type ParamsOptions, type RefOptions } from '../dist/runtime/plugins/api.global.js'
|
|
10
10
|
|
|
11
|
+
export { type BootstrapError, type BootstrapLoader, type BootstrapLoaders, type BootstrapStatus, type MerkalyOrganization, type MerkalyUser } from '../dist/runtime/internal/state.js'
|
|
12
|
+
|
|
11
13
|
export { default } from './module.mjs'
|
|
12
14
|
|
|
13
15
|
export { type MerkalyI18nLocale, type MerkalyModuleOptions } from './module.mjs'
|
package/package.json
CHANGED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { computed } from "vue";
|
|
2
|
-
import { useState } from "#imports";
|
|
3
|
-
export const useTenant = () => {
|
|
4
|
-
const tenant = useState("tenant:id", () => "");
|
|
5
|
-
const fromServer = useState("tenant:server", () => false);
|
|
6
|
-
const isReady = computed(() => Boolean(tenant.value));
|
|
7
|
-
const setTenant = (value, options = {}) => {
|
|
8
|
-
tenant.value = value;
|
|
9
|
-
if (options.server) fromServer.value = true;
|
|
10
|
-
};
|
|
11
|
-
return {
|
|
12
|
-
fromServer,
|
|
13
|
-
isReady,
|
|
14
|
-
setTenant,
|
|
15
|
-
tenant
|
|
16
|
-
};
|
|
17
|
-
};
|