@dev.smartpricing/platform-layer 0.0.4 → 0.0.5

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 CHANGED
@@ -38,24 +38,26 @@ pnpm add nuxt-ui-layer@"github:smartpricing/smartness-nuxt-ui#v1.6.3"
38
38
  ```ts
39
39
  // nuxt.config.ts
40
40
  export default defineNuxtConfig({
41
- extends: ['@dev.smartpricing/platform-layer'],
42
- })
41
+ extends: ["@dev.smartpricing/platform-layer"],
42
+ });
43
43
  ```
44
44
 
45
45
  ### 2. Configure Runtime Environment
46
46
 
47
47
  Set these environment variables (or define them in `nuxt.config.ts` under `runtimeConfig.public`):
48
48
 
49
- | Variable | Description | Default |
50
- |----------|-------------|---------|
51
- | `NUXT_PUBLIC_BACKEND_BASE_URL` | Backend API base URL | `''` |
52
- | `NUXT_PUBLIC_ENV_CSRF_COOKIE_NAME` | CSRF cookie name | `smt_csrf` |
53
- | `NUXT_PUBLIC_PLATFORM_URL` | Platform URL for auth redirects | `''` |
49
+ | Variable | Description | Default |
50
+ | ------------------------------ | ------------------------------- | ---------- |
51
+ | `NUXT_PUBLIC_BACKEND_BASE_URL` | Backend API base URL | `''` |
52
+ | `NUXT_PUBLIC_CSRF_COOKIE_NAME` | CSRF cookie name | `smt_csrf` |
53
+ | `NUXT_PUBLIC_ENVIRONMENT` | Environment | `local` |
54
+ | `NUXT_PUBLIC_PLATFORM_URL` | Platform URL for auth redirects | `''` |
54
55
 
55
56
  ```bash
56
57
  # .env
57
58
  NUXT_PUBLIC_BACKEND_BASE_URL=https://api.smartpricing.com
58
- NUXT_PUBLIC_ENV_CSRF_COOKIE_NAME=smt_csrf
59
+ NUXT_PUBLIC_CSRF_COOKIE_NAME=smt_csrf
60
+ ENVIRONMENT=local
59
61
  NUXT_PUBLIC_PLATFORM_URL=https://app.smartpricing.com
60
62
  ```
61
63
 
@@ -63,13 +65,13 @@ NUXT_PUBLIC_PLATFORM_URL=https://app.smartpricing.com
63
65
 
64
66
  The layer auto-registers these [Nuxt modules](https://nuxt.com/docs/guide/concepts/modules):
65
67
 
66
- | Module | Purpose | Docs |
67
- |--------|---------|------|
68
- | [`@pinia/nuxt`](https://pinia.vuejs.org/ssr/nuxt.html) | State management | [Pinia docs](https://pinia.vuejs.org) |
69
- | [`@pinia/colada-nuxt`](https://pinia-colada.esm.dev) | Async data caching, queries & mutations | [Pinia Colada docs](https://pinia-colada.esm.dev) |
70
- | [`@nuxtjs/i18n`](https://i18n.nuxtjs.org) | Internationalization (en, it, de, es) | [Nuxt i18n docs](https://i18n.nuxtjs.org) |
71
- | [`@vueuse/nuxt`](https://vueuse.org/nuxt/README.html) | Utility composables | [VueUse docs](https://vueuse.org) |
72
- | [`nuxt-ui-layer`](https://github.com/smartpricing/smartness-nuxt-ui) | Smartness design system | — |
68
+ | Module | Purpose | Docs |
69
+ | -------------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------- |
70
+ | [`@pinia/nuxt`](https://pinia.vuejs.org/ssr/nuxt.html) | State management | [Pinia docs](https://pinia.vuejs.org) |
71
+ | [`@pinia/colada-nuxt`](https://pinia-colada.esm.dev) | Async data caching, queries & mutations | [Pinia Colada docs](https://pinia-colada.esm.dev) |
72
+ | [`@nuxtjs/i18n`](https://i18n.nuxtjs.org) | Internationalization (en, it, de, es) | [Nuxt i18n docs](https://i18n.nuxtjs.org) |
73
+ | [`@vueuse/nuxt`](https://vueuse.org/nuxt/README.html) | Utility composables | [VueUse docs](https://vueuse.org) |
74
+ | [`nuxt-ui-layer`](https://github.com/smartpricing/smartness-nuxt-ui) | Smartness design system | — |
73
75
 
74
76
  All composables, queries, and mutations are **auto-imported** — no manual imports needed.
75
77
 
@@ -85,30 +87,34 @@ All composables, queries, and mutations are **auto-imported** — no manual impo
85
87
 
86
88
  ```ts
87
89
  interface ApiClientOptions {
88
- baseURL: string
89
- csrf?: ApiClientCsrfConfig
90
- auth?: ApiClientAuthConfig
91
- errorSerializer?: (context: { status: number; statusText: string; data: unknown }) => unknown
92
- onRequest?: FetchHook<FetchContext>
93
- onResponseError?: FetchHook<FetchContext & { response: Response }>
90
+ baseURL: string;
91
+ csrf?: ApiClientCsrfConfig;
92
+ auth?: ApiClientAuthConfig;
93
+ errorSerializer?: (context: {
94
+ status: number;
95
+ statusText: string;
96
+ data: unknown;
97
+ }) => unknown;
98
+ onRequest?: FetchHook<FetchContext>;
99
+ onResponseError?: FetchHook<FetchContext & { response: Response }>;
94
100
  }
95
101
  ```
96
102
 
97
- | Option | Type | Description |
98
- |--------|------|-------------|
99
- | `baseURL` | `string` | Base URL prepended to all requests. Slash handling is automatic ([ofetch docs](https://github.com/unjs/ofetch#baseurl)). |
100
- | `csrf` | `ApiClientCsrfConfig` | CSRF token injection config. |
101
- | `auth` | `ApiClientAuthConfig` | Auth config for token refresh and MFA. When omitted, returns a raw `$fetch` instance with no auth handling. |
102
- | `errorSerializer` | `(context) => unknown` | Custom error serializer. Receives `{ status, statusText, data }` from [`FetchError`](https://github.com/unjs/ofetch#%EF%B8%8F-access-to-raw-response). |
103
- | `onRequest` | `FetchHook<FetchContext>` | Custom [`onRequest` interceptor](https://github.com/unjs/ofetch#%EF%B8%8F-interceptors) appended after built-in hooks (cookie forwarding, CSRF). |
104
- | `onResponseError` | `FetchHook<FetchContext & { response: Response }>` | Custom [`onResponseError` interceptor](https://github.com/unjs/ofetch#%EF%B8%8F-interceptors) called after auth/MFA handling. |
103
+ | Option | Type | Description |
104
+ | ----------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
105
+ | `baseURL` | `string` | Base URL prepended to all requests. Slash handling is automatic ([ofetch docs](https://github.com/unjs/ofetch#baseurl)). |
106
+ | `csrf` | `ApiClientCsrfConfig` | CSRF token injection config. |
107
+ | `auth` | `ApiClientAuthConfig` | Auth config for token refresh and MFA. When omitted, returns a raw `$fetch` instance with no auth handling. |
108
+ | `errorSerializer` | `(context) => unknown` | Custom error serializer. Receives `{ status, statusText, data }` from [`FetchError`](https://github.com/unjs/ofetch#%EF%B8%8F-access-to-raw-response). |
109
+ | `onRequest` | `FetchHook<FetchContext>` | Custom [`onRequest` interceptor](https://github.com/unjs/ofetch#%EF%B8%8F-interceptors) appended after built-in hooks (cookie forwarding, CSRF). |
110
+ | `onResponseError` | `FetchHook<FetchContext & { response: Response }>` | Custom [`onResponseError` interceptor](https://github.com/unjs/ofetch#%EF%B8%8F-interceptors) called after auth/MFA handling. |
105
111
 
106
112
  #### CSRF Config
107
113
 
108
114
  ```ts
109
115
  interface ApiClientCsrfConfig {
110
- cookieName: string
111
- headerName?: string // default: 'X-CSRF-Token'
116
+ cookieName: string;
117
+ headerName?: string; // default: 'X-CSRF-Token'
112
118
  }
113
119
  ```
114
120
 
@@ -118,17 +124,17 @@ The client reads the CSRF token from the specified cookie via Nuxt's [`useCookie
118
124
 
119
125
  ```ts
120
126
  interface ApiClientAuthConfig {
121
- refreshBaseURL: string
122
- refreshEndpoint?: string // default: '/api/auth/refresh'
123
- platformURL: string
124
- shouldRefresh?: (status: number, data: unknown) => boolean
125
- onRefreshFailed?: () => void
126
- mfa?: ApiClientMfaConfig
127
+ refreshBaseURL: string;
128
+ refreshEndpoint?: string; // default: '/api/auth/refresh'
129
+ platformURL: string;
130
+ shouldRefresh?: (status: number, data: unknown) => boolean;
131
+ onRefreshFailed?: () => void;
132
+ mfa?: ApiClientMfaConfig;
127
133
  }
128
134
 
129
135
  interface ApiClientMfaConfig {
130
- shouldChallenge: (status: number, data: unknown) => boolean
131
- challengePath?: string // default: '/auth/mfa-challenge'
136
+ shouldChallenge: (status: number, data: unknown) => boolean;
137
+ challengePath?: string; // default: '/auth/mfa-challenge'
132
138
  }
133
139
  ```
134
140
 
@@ -148,20 +154,20 @@ On the server, the client reads the incoming request cookies via Nuxt's [`useReq
148
154
 
149
155
  ```ts
150
156
  const client = createApiClient({
151
- baseURL: 'https://api.example.com',
152
- csrf: { cookieName: 'my_csrf' },
157
+ baseURL: "https://api.example.com",
158
+ csrf: { cookieName: "my_csrf" },
153
159
  auth: {
154
- refreshBaseURL: 'https://api.example.com',
155
- platformURL: 'https://app.example.com',
156
- onRefreshFailed: () => navigateTo('/login'),
160
+ refreshBaseURL: "https://api.example.com",
161
+ platformURL: "https://app.example.com",
162
+ onRefreshFailed: () => navigateTo("/login"),
157
163
  },
158
164
  errorSerializer: ({ status, data }) => ({
159
165
  code: status,
160
- message: (data as any)?.error?.message ?? 'Unknown error',
166
+ message: (data as any)?.error?.message ?? "Unknown error",
161
167
  }),
162
- })
168
+ });
163
169
 
164
- const users = await client<User[]>('/users', { query: { active: true } })
170
+ const users = await client<User[]>("/users", { query: { active: true } });
165
171
  ```
166
172
 
167
173
  ---
@@ -206,12 +212,12 @@ const filtered = await client<User[]>('/api/users', {
206
212
 
207
213
  ```vue
208
214
  <script setup lang="ts">
209
- const client = useApiClient()
215
+ const client = useApiClient();
210
216
 
211
217
  const { data, status, error } = useQuery({
212
- key: ['custom-data'],
213
- query: () => client<MyData>('/api/custom-endpoint'),
214
- })
218
+ key: ["custom-data"],
219
+ query: () => client<MyData>("/api/custom-endpoint"),
220
+ });
215
221
  </script>
216
222
  ```
217
223
 
@@ -219,21 +225,20 @@ const { data, status, error } = useQuery({
219
225
 
220
226
  ```vue
221
227
  <script setup lang="ts">
222
- const client = useApiClient()
228
+ const client = useApiClient();
223
229
 
224
230
  const { mutateAsync, status } = useMutation({
225
231
  mutation: (body: CreateItemRequest) =>
226
- client<CreateItemResponse>('/api/items', {
227
- method: 'POST',
232
+ client<CreateItemResponse>("/api/items", {
233
+ method: "POST",
228
234
  body,
229
235
  }),
230
- })
236
+ });
231
237
 
232
238
  async function handleSubmit(data: CreateItemRequest) {
233
239
  try {
234
- const result = await mutateAsync(data)
235
- }
236
- catch (error) {
240
+ const result = await mutateAsync(data);
241
+ } catch (error) {
237
242
  // FetchError with parsed body in error.data
238
243
  }
239
244
  }
@@ -250,17 +255,17 @@ async function handleSubmit(data: CreateItemRequest) {
250
255
 
251
256
  #### Return Values
252
257
 
253
- | Property | Type | Description |
254
- |----------|------|-------------|
255
- | `signOut` | `() => Promise<void>` | Calls `POST /api/auth/logout` then redirects to `{PLATFORM_URL}/auth/login` |
256
- | `logoutStatus` | `Ref<'idle' \| 'pending' \| 'success' \| 'error'>` | [Mutation status](https://pinia-colada.esm.dev/guide/mutations.html) |
257
- | `logoutError` | `Ref<Error \| null>` | Error if logout failed |
258
+ | Property | Type | Description |
259
+ | -------------- | -------------------------------------------------- | --------------------------------------------------------------------------- |
260
+ | `signOut` | `() => Promise<void>` | Calls `POST /api/auth/logout` then redirects to `{PLATFORM_URL}/auth/login` |
261
+ | `logoutStatus` | `Ref<'idle' \| 'pending' \| 'success' \| 'error'>` | [Mutation status](https://pinia-colada.esm.dev/guide/mutations.html) |
262
+ | `logoutError` | `Ref<Error \| null>` | Error if logout failed |
258
263
 
259
264
  #### Usage
260
265
 
261
266
  ```vue
262
267
  <script setup lang="ts">
263
- const { signOut, logoutStatus } = useAuth()
268
+ const { signOut, logoutStatus } = useAuth();
264
269
  </script>
265
270
 
266
271
  <template>
@@ -280,42 +285,42 @@ Each query file also exports a **key factory** (e.g., `sessionKeys`, `billingOve
280
285
 
281
286
  ### Session & User
282
287
 
283
- | Composable | Endpoint | Response Type | Key Factory |
284
- |------------|----------|---------------|-------------|
285
- | `useSessionQuery()` | `GET /api/me` | `MeContextResponse` | `sessionKeys.me()` |
286
- | `usePermissionsQuery()` | `GET /api/me/permissions` | `PermissionsResponse` | `permissionsKeys.all()` |
288
+ | Composable | Endpoint | Response Type | Key Factory |
289
+ | ------------------------ | --------------------------- | ------------------------- | ------------------------ |
290
+ | `useSessionQuery()` | `GET /api/me` | `MeContextResponse` | `sessionKeys.me()` |
291
+ | `usePermissionsQuery()` | `GET /api/me/permissions` | `PermissionsResponse` | `permissionsKeys.all()` |
287
292
  | `useCrossSellingQuery()` | `GET /api/me/cross-selling` | `GetCrossSellingResponse` | `crossSellingKeys.all()` |
288
293
 
289
294
  ### Organization
290
295
 
291
- | Composable | Endpoint | Response Type | Key Factory |
292
- |------------|----------|---------------|-------------|
296
+ | Composable | Endpoint | Response Type | Key Factory |
297
+ | ----------------------------- | ----------------------- | ----------------------------- | ------------------------------ |
293
298
  | `useOrganizationQuery(orgId)` | `GET /api/orgs/{orgId}` | `OrganizationContextResponse` | `organizationKeys.byId(orgId)` |
294
299
 
295
300
  **Params:** `orgId: MaybeRefOrGetter<string>` — enabled only when `orgId` is truthy.
296
301
 
297
302
  ### Billing
298
303
 
299
- | Composable | Endpoint | Response Type | Key Factory |
300
- |------------|----------|---------------|-------------|
301
- | `useBillingOverviewQuery(orgId)` | `GET /api/orgs/{orgId}/billing` | `BillingOverviewResponse` | `billingOverviewKeys.byOrg(orgId)` |
302
- | `useBillingDocumentsQuery(orgId, params?)` | `GET /api/orgs/{orgId}/billing/documents` | `ListBillingDocumentsResponse` | `billingDocumentsKeys.byOrg(orgId, params)` |
303
- | `useLegalEntityQuery(orgId, id)` | `GET /api/orgs/{orgId}/billing/legal-entities/{id}` | `LegalEntityDetail` | `legalEntityKeys.byId(orgId, id)` |
304
- | `useBillingDocumentPdfQuery(orgId, type, id)` | `GET /api/orgs/{orgId}/billing/documents/{type}/{id}/pdf` | `BillingDocumentPdfResponse` | `billingDocumentPdfKeys.byId(orgId, type, id)` |
304
+ | Composable | Endpoint | Response Type | Key Factory |
305
+ | --------------------------------------------- | --------------------------------------------------------- | ------------------------------ | ---------------------------------------------- |
306
+ | `useBillingOverviewQuery(orgId)` | `GET /api/orgs/{orgId}/billing` | `BillingOverviewResponse` | `billingOverviewKeys.byOrg(orgId)` |
307
+ | `useBillingDocumentsQuery(orgId, params?)` | `GET /api/orgs/{orgId}/billing/documents` | `ListBillingDocumentsResponse` | `billingDocumentsKeys.byOrg(orgId, params)` |
308
+ | `useLegalEntityQuery(orgId, id)` | `GET /api/orgs/{orgId}/billing/legal-entities/{id}` | `LegalEntityDetail` | `legalEntityKeys.byId(orgId, id)` |
309
+ | `useBillingDocumentPdfQuery(orgId, type, id)` | `GET /api/orgs/{orgId}/billing/documents/{type}/{id}/pdf` | `BillingDocumentPdfResponse` | `billingDocumentPdfKeys.byId(orgId, type, id)` |
305
310
 
306
311
  #### Billing Documents Filter Params
307
312
 
308
313
  ```ts
309
314
  interface BillingDocumentsQueryParams {
310
- legalEntityId?: string
311
- customerId?: string
312
- limit?: number
313
- after?: string
314
- before?: string
315
- subscriptionIds?: string[]
316
- type?: string
317
- status?: string
318
- search?: string
315
+ legalEntityId?: string;
316
+ customerId?: string;
317
+ limit?: number;
318
+ after?: string;
319
+ before?: string;
320
+ subscriptionIds?: string[];
321
+ type?: string;
322
+ status?: string;
323
+ search?: string;
319
324
  }
320
325
  ```
321
326
 
@@ -323,20 +328,20 @@ interface BillingDocumentsQueryParams {
323
328
 
324
329
  ```vue
325
330
  <script setup lang="ts">
326
- const route = useRoute()
331
+ const route = useRoute();
327
332
 
328
333
  // Static query — fetches immediately
329
- const { data: session, status } = useSessionQuery()
334
+ const { data: session, status } = useSessionQuery();
330
335
 
331
336
  // Dynamic query — re-fetches when orgId changes
332
- const { data: org } = useOrganizationQuery(() => route.params.orgId as string)
337
+ const { data: org } = useOrganizationQuery(() => route.params.orgId as string);
333
338
 
334
339
  // Conditional query with filter params
335
- const filters = ref<BillingDocumentsQueryParams>({ limit: 20 })
340
+ const filters = ref<BillingDocumentsQueryParams>({ limit: 20 });
336
341
  const { data: docs } = useBillingDocumentsQuery(
337
342
  () => route.params.orgId as string,
338
343
  filters,
339
- )
344
+ );
340
345
  </script>
341
346
 
342
347
  <template>
@@ -352,16 +357,16 @@ Use the exported key factories with [`useQueryCache()`](https://pinia-colada.esm
352
357
 
353
358
  ```vue
354
359
  <script setup lang="ts">
355
- import { useQueryCache } from '@pinia/colada'
360
+ import { useQueryCache } from "@pinia/colada";
356
361
 
357
- const queryCache = useQueryCache()
362
+ const queryCache = useQueryCache();
358
363
 
359
- const { mutateAsync: updateProfile } = useUpdateProfileMutation()
364
+ const { mutateAsync: updateProfile } = useUpdateProfileMutation();
360
365
 
361
366
  async function handleSave(data: UpdateProfileRequest) {
362
- await updateProfile(data)
367
+ await updateProfile(data);
363
368
  // Invalidate session to refetch updated user data
364
- queryCache.invalidateQueries({ key: sessionKeys.me() })
369
+ queryCache.invalidateQueries({ key: sessionKeys.me() });
365
370
  }
366
371
  </script>
367
372
  ```
@@ -372,62 +377,62 @@ async function handleSave(data: UpdateProfileRequest) {
372
377
 
373
378
  All mutations use [`useMutation()`](https://pinia-colada.esm.dev/guide/mutations.html) from Pinia Colada. Each returns:
374
379
 
375
- | Property | Type | Description |
376
- |----------|------|-------------|
377
- | `mutate` | `(params) => void` | Fire-and-forget — errors handled via `onError` hook |
378
- | `mutateAsync` | `(params) => Promise<TResult>` | Returns promise — use `try/catch` for errors |
379
- | `status` | `Ref<'idle' \| 'pending' \| 'success' \| 'error'>` | Overall [mutation status](https://pinia-colada.esm.dev/guide/mutations.html) |
380
- | `asyncStatus` | `Ref<'idle' \| 'loading'>` | Whether a request is in-flight |
381
- | `data` | `Ref<TResult \| undefined>` | Last successful response |
382
- | `error` | `Ref<Error \| null>` | Last error |
383
- | `variables` | `Ref<TParams \| undefined>` | Last mutation parameters |
384
- | `reset` | `() => void` | Reset to initial state |
380
+ | Property | Type | Description |
381
+ | ------------- | -------------------------------------------------- | ---------------------------------------------------------------------------- |
382
+ | `mutate` | `(params) => void` | Fire-and-forget — errors handled via `onError` hook |
383
+ | `mutateAsync` | `(params) => Promise<TResult>` | Returns promise — use `try/catch` for errors |
384
+ | `status` | `Ref<'idle' \| 'pending' \| 'success' \| 'error'>` | Overall [mutation status](https://pinia-colada.esm.dev/guide/mutations.html) |
385
+ | `asyncStatus` | `Ref<'idle' \| 'loading'>` | Whether a request is in-flight |
386
+ | `data` | `Ref<TResult \| undefined>` | Last successful response |
387
+ | `error` | `Ref<Error \| null>` | Last error |
388
+ | `variables` | `Ref<TParams \| undefined>` | Last mutation parameters |
389
+ | `reset` | `() => void` | Reset to initial state |
385
390
 
386
391
  ### Authentication Mutations
387
392
 
388
- | Composable | Method | Endpoint | Request | Response |
389
- |------------|--------|----------|---------|----------|
390
- | `useLoginMutation()` | `POST` | `/api/auth/login` | `LoginRequest` | `void` |
391
- | `useLogoutMutation()` | `POST` | `/api/auth/logout` | — | `void` |
392
- | `useRefreshMutation()` | `POST` | `/api/auth/refresh` | — | `void` |
393
- | `useActivateMutation()` | `POST` | `/api/auth/activate` | `ActivateRequest` | `void` |
394
- | `useAccountingLoginMutation()` | `POST` | `/api/auth/accounting-login` | `AccountingLoginRequest` | `void` |
393
+ | Composable | Method | Endpoint | Request | Response |
394
+ | ------------------------------ | ------ | ---------------------------- | ------------------------ | -------- |
395
+ | `useLoginMutation()` | `POST` | `/api/auth/login` | `LoginRequest` | `void` |
396
+ | `useLogoutMutation()` | `POST` | `/api/auth/logout` | — | `void` |
397
+ | `useRefreshMutation()` | `POST` | `/api/auth/refresh` | — | `void` |
398
+ | `useActivateMutation()` | `POST` | `/api/auth/activate` | `ActivateRequest` | `void` |
399
+ | `useAccountingLoginMutation()` | `POST` | `/api/auth/accounting-login` | `AccountingLoginRequest` | `void` |
395
400
 
396
401
  ### MFA Mutations
397
402
 
398
- | Composable | Method | Endpoint | Request | Response |
399
- |------------|--------|----------|---------|----------|
400
- | `useMfaSetupMutation()` | `POST` | `/api/auth/mfa/setup` | — | `MfaSetupResponse` |
401
- | `useMfaStepUpMutation()` | `POST` | `/api/auth/otp` | `MfaStepUpRequest` | `MfaStepUpResponse` |
402
- | `useMfaFinalizeMutation()` | `POST` | `/api/auth/mfa/finalize` | `MfaFinalizeRequest` | `MfaFinalizeResponse` |
403
- | `useMfaDisableMutation()` | `POST` | `/api/auth/mfa/disable` | `MfaDisableRequest` | `MfaDisableResponse` |
403
+ | Composable | Method | Endpoint | Request | Response |
404
+ | ----------------------------------------- | ------ | ----------------------------------------- | ----------------------------------- | ------------------------------------ |
405
+ | `useMfaSetupMutation()` | `POST` | `/api/auth/mfa/setup` | — | `MfaSetupResponse` |
406
+ | `useMfaStepUpMutation()` | `POST` | `/api/auth/otp` | `MfaStepUpRequest` | `MfaStepUpResponse` |
407
+ | `useMfaFinalizeMutation()` | `POST` | `/api/auth/mfa/finalize` | `MfaFinalizeRequest` | `MfaFinalizeResponse` |
408
+ | `useMfaDisableMutation()` | `POST` | `/api/auth/mfa/disable` | `MfaDisableRequest` | `MfaDisableResponse` |
404
409
  | `useMfaRegenerateRecoveryCodesMutation()` | `POST` | `/api/auth/mfa/regenerate-recovery-codes` | `MfaRegenerateRecoveryCodesRequest` | `MfaRegenerateRecoveryCodesResponse` |
405
410
 
406
411
  ### Profile Mutations
407
412
 
408
- | Composable | Method | Endpoint | Request | Response |
409
- |------------|--------|----------|---------|----------|
410
- | `useUpdateProfileMutation()` | `PATCH` | `/api/me` | `UpdateProfileRequest` | `UpdateProfileResponse` |
411
- | `useChangePasswordMutation()` | `POST` | `/api/me/change-password` | `ChangePasswordRequest` | `{ ok: true }` |
412
- | `useRequestPasswordResetMutation()` | `POST` | `/api/auth/request-password-reset` | `ForgotPasswordRequest` | `void` |
413
- | `useResetPasswordMutation()` | `POST` | `/api/auth/reset-password` | `ResetPasswordRequest` | `void` |
413
+ | Composable | Method | Endpoint | Request | Response |
414
+ | ----------------------------------- | ------- | ---------------------------------- | ----------------------- | ----------------------- |
415
+ | `useUpdateProfileMutation()` | `PATCH` | `/api/me` | `UpdateProfileRequest` | `UpdateProfileResponse` |
416
+ | `useChangePasswordMutation()` | `POST` | `/api/me/change-password` | `ChangePasswordRequest` | `{ ok: true }` |
417
+ | `useRequestPasswordResetMutation()` | `POST` | `/api/auth/request-password-reset` | `ForgotPasswordRequest` | `void` |
418
+ | `useResetPasswordMutation()` | `POST` | `/api/auth/reset-password` | `ResetPasswordRequest` | `void` |
414
419
 
415
420
  ### Billing Mutations
416
421
 
417
- | Composable | Method | Endpoint | Request | Response |
418
- |------------|--------|----------|---------|----------|
419
- | `useCreatePaymentMethodMutation()` | `POST` | `/api/orgs/{orgId}/billing/payment-methods` | `{ orgId, body: CreatePaymentMethodRequest }` | `CreatePaymentMethodsResponse` |
420
- | `useSetPrimaryPaymentMethodMutation()` | `PATCH` | `/api/orgs/{orgId}/billing/payment-methods/primary` | `{ orgId, body: SetPrimaryPaymentMethodRequest }` | `SetPrimaryPaymentMethodResponse` |
421
- | `useRemovePaymentMethodMutation()` | `DELETE` | `/api/orgs/{orgId}/billing/payment-methods/{paymentMethodId}` | `{ orgId, paymentMethodId }` | `void` |
422
- | `useCreateSetupIntentMutation()` | `POST` | `/api/orgs/{orgId}/billing/payment-methods/setup-intent` | `{ orgId, body: CreateSetupIntentRequest }` | `CreateSetupIntentResponse` |
422
+ | Composable | Method | Endpoint | Request | Response |
423
+ | -------------------------------------- | -------- | ------------------------------------------------------------- | ------------------------------------------------- | --------------------------------- |
424
+ | `useCreatePaymentMethodMutation()` | `POST` | `/api/orgs/{orgId}/billing/payment-methods` | `{ orgId, body: CreatePaymentMethodRequest }` | `CreatePaymentMethodsResponse` |
425
+ | `useSetPrimaryPaymentMethodMutation()` | `PATCH` | `/api/orgs/{orgId}/billing/payment-methods/primary` | `{ orgId, body: SetPrimaryPaymentMethodRequest }` | `SetPrimaryPaymentMethodResponse` |
426
+ | `useRemovePaymentMethodMutation()` | `DELETE` | `/api/orgs/{orgId}/billing/payment-methods/{paymentMethodId}` | `{ orgId, paymentMethodId }` | `void` |
427
+ | `useCreateSetupIntentMutation()` | `POST` | `/api/orgs/{orgId}/billing/payment-methods/setup-intent` | `{ orgId, body: CreateSetupIntentRequest }` | `CreateSetupIntentResponse` |
423
428
 
424
429
  ### Organization Mutations
425
430
 
426
- | Composable | Method | Endpoint | Request | Response |
427
- |------------|--------|----------|---------|----------|
428
- | `useUpdateLegalEntityMutation()` | `PATCH` | `/api/orgs/{orgId}/billing/legal-entities/{id}` | `{ orgId, id, body: UpdateLegalEntityRequest }` | `LegalEntityDetail` |
429
- | `useRequestLegalEntityChangeMutation()` | `POST` | `/api/orgs/{orgId}/billing/legal-entities/{id}/change-request` | `{ orgId, id, body: RequestLegalEntityChangeRequest }` | `{ emails_status: 'sent' \| 'failed' }` |
430
- | `useRequestSubscriptionCancellationMutation()` | `POST` | `/api/orgs/{orgId}/subscriptions/request-cancellation` | `{ orgId, body: SubscriptionCancellationRequest }` | `SubscriptionCancellationResponse` |
431
+ | Composable | Method | Endpoint | Request | Response |
432
+ | ---------------------------------------------- | ------- | -------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------- |
433
+ | `useUpdateLegalEntityMutation()` | `PATCH` | `/api/orgs/{orgId}/billing/legal-entities/{id}` | `{ orgId, id, body: UpdateLegalEntityRequest }` | `LegalEntityDetail` |
434
+ | `useRequestLegalEntityChangeMutation()` | `POST` | `/api/orgs/{orgId}/billing/legal-entities/{id}/change-request` | `{ orgId, id, body: RequestLegalEntityChangeRequest }` | `{ emails_status: 'sent' \| 'failed' }` |
435
+ | `useRequestSubscriptionCancellationMutation()` | `POST` | `/api/orgs/{orgId}/subscriptions/request-cancellation` | `{ orgId, body: SubscriptionCancellationRequest }` | `SubscriptionCancellationResponse` |
431
436
 
432
437
  ### Mutation Usage Examples
433
438
 
@@ -435,14 +440,13 @@ All mutations use [`useMutation()`](https://pinia-colada.esm.dev/guide/mutations
435
440
 
436
441
  ```vue
437
442
  <script setup lang="ts">
438
- const { mutateAsync: login, status, error } = useLoginMutation()
443
+ const { mutateAsync: login, status, error } = useLoginMutation();
439
444
 
440
445
  async function handleLogin(email: string, password: string) {
441
446
  try {
442
- await login({ email, password })
443
- await navigateTo('/dashboard')
444
- }
445
- catch (err) {
447
+ await login({ email, password });
448
+ await navigateTo("/dashboard");
449
+ } catch (err) {
446
450
  // error.value is also set reactively
447
451
  }
448
452
  }
@@ -452,7 +456,7 @@ async function handleLogin(email: string, password: string) {
452
456
  <form @submit.prevent="handleLogin(email, password)">
453
457
  <p v-if="error">{{ error.message }}</p>
454
458
  <button :disabled="status === 'pending'" type="submit">
455
- {{ status === 'pending' ? 'Signing in...' : 'Sign In' }}
459
+ {{ status === "pending" ? "Signing in..." : "Sign In" }}
456
460
  </button>
457
461
  </form>
458
462
  </template>
@@ -462,14 +466,14 @@ async function handleLogin(email: string, password: string) {
462
466
 
463
467
  ```vue
464
468
  <script setup lang="ts">
465
- const { mutateAsync: setupMfa } = useMfaSetupMutation()
466
- const { mutateAsync: finalizeMfa } = useMfaFinalizeMutation()
469
+ const { mutateAsync: setupMfa } = useMfaSetupMutation();
470
+ const { mutateAsync: finalizeMfa } = useMfaFinalizeMutation();
467
471
 
468
472
  // Step 1: Get QR code / secret
469
- const setupData = await setupMfa()
473
+ const setupData = await setupMfa();
470
474
 
471
475
  // Step 2: User enters TOTP code, finalize
472
- await finalizeMfa({ otp: userCode })
476
+ await finalizeMfa({ otp: userCode });
473
477
  </script>
474
478
  ```
475
479
 
@@ -477,21 +481,22 @@ await finalizeMfa({ otp: userCode })
477
481
 
478
482
  ```vue
479
483
  <script setup lang="ts">
480
- import { useQueryCache } from '@pinia/colada'
484
+ import { useQueryCache } from "@pinia/colada";
481
485
 
482
- const queryCache = useQueryCache()
483
- const orgId = computed(() => route.params.orgId as string)
486
+ const queryCache = useQueryCache();
487
+ const orgId = computed(() => route.params.orgId as string);
484
488
 
485
- const { mutateAsync: removeMethod, status } = useRemovePaymentMethodMutation()
489
+ const { mutateAsync: removeMethod, status } = useRemovePaymentMethodMutation();
486
490
 
487
491
  async function handleRemove(paymentMethodId: string) {
488
492
  try {
489
- await removeMethod({ orgId: orgId.value, paymentMethodId })
493
+ await removeMethod({ orgId: orgId.value, paymentMethodId });
490
494
 
491
495
  // Refetch billing data after removing payment method
492
- queryCache.invalidateQueries({ key: billingOverviewKeys.byOrg(orgId.value) })
493
- }
494
- catch (err) {
496
+ queryCache.invalidateQueries({
497
+ key: billingOverviewKeys.byOrg(orgId.value),
498
+ });
499
+ } catch (err) {
495
500
  // Handle error
496
501
  }
497
502
  }
@@ -502,17 +507,16 @@ async function handleRemove(paymentMethodId: string) {
502
507
 
503
508
  ```vue
504
509
  <script setup lang="ts">
505
- const { mutate, mutateAsync, status, error } = useUpdateProfileMutation()
510
+ const { mutate, mutateAsync, status, error } = useUpdateProfileMutation();
506
511
 
507
512
  // Fire-and-forget — errors are swallowed, check error ref reactively
508
- mutate({ name: 'New Name' })
513
+ mutate({ name: "New Name" });
509
514
 
510
515
  // Async — errors rethrown, use try/catch
511
516
  try {
512
- const result = await mutateAsync({ name: 'New Name' })
513
- }
514
- catch (err) {
515
- console.error('Update failed:', err)
517
+ const result = await mutateAsync({ name: "New Name" });
518
+ } catch (err) {
519
+ console.error("Update failed:", err);
516
520
  }
517
521
  </script>
518
522
  ```
@@ -530,10 +534,10 @@ If your app already provides a module the layer includes, [disable it](https://n
530
534
  ```ts
531
535
  // nuxt.config.ts
532
536
  export default defineNuxtConfig({
533
- extends: ['@dev.smartpricing/platform-layer'],
534
- i18n: false, // disable layer's i18n
535
- pinia: false, // disable layer's pinia
536
- })
537
+ extends: ["@dev.smartpricing/platform-layer"],
538
+ i18n: false, // disable layer's i18n
539
+ pinia: false, // disable layer's pinia
540
+ });
537
541
  ```
538
542
 
539
543
  ### Override i18n Locales
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ import type { SuiteProduct } from 'nuxt-ui-layer/types'
3
+
4
+ const props = defineProps<{
5
+ product: SuiteProduct
6
+ collapsed?: boolean
7
+ }>()
8
+
9
+ const { enabledProducts, navigateToProduct } = useProductSwitcher()
10
+
11
+ const selectedProduct = shallowRef<SuiteProduct>(props.product)
12
+
13
+ watch(selectedProduct, (value) => {
14
+ if (value !== props.product)
15
+ navigateToProduct(value)
16
+ })
17
+ </script>
18
+
19
+ <template>
20
+ <SNavigationProducts
21
+ v-model="selectedProduct"
22
+ :products="enabledProducts"
23
+ :collapsed="collapsed"
24
+ />
25
+ </template>
@@ -1,10 +1,15 @@
1
1
  export function useApiClient() {
2
- const { BACKEND_BASE_URL, ENV_CSRF_COOKIE_NAME, PLATFORM_URL } = useRuntimeConfig().public
2
+ const { BACKEND_BASE_URL, CSRF_COOKIE_NAME, PLATFORM_URL, ENVIRONMENT } = useRuntimeConfig().public
3
+
4
+
5
+ const baseCsrfName = (CSRF_COOKIE_NAME as string | undefined) || 'smt_csrf'
6
+ const environment = (ENVIRONMENT as string | undefined) || 'local'
7
+ const csrfCookieName = environment === 'prod' ? baseCsrfName : `${baseCsrfName}_${environment}`
3
8
 
4
9
  return createApiClient({
5
10
  baseURL: BACKEND_BASE_URL as string,
6
11
  csrf: {
7
- cookieName: (ENV_CSRF_COOKIE_NAME as string) || 'smt_csrf',
12
+ cookieName: csrfCookieName
8
13
  },
9
14
  auth: {
10
15
  refreshBaseURL: BACKEND_BASE_URL as string,
@@ -16,7 +21,7 @@ export function useApiClient() {
16
21
  },
17
22
  },
18
23
  onRefreshFailed: () => {
19
- if (import.meta.client) {
24
+ if (import.meta.client && !window.location.pathname.startsWith('/auth/login')) {
20
25
  window.location.assign(`${PLATFORM_URL}/auth/login`)
21
26
  }
22
27
  },
@@ -0,0 +1,70 @@
1
+ import type { SuiteProduct } from 'nuxt-ui-layer/types'
2
+ import { computed } from 'vue'
3
+
4
+ export function useProductSwitcher() {
5
+ const { data: permissions, status } = usePermissionsQuery()
6
+ const config = useRuntimeConfig().public
7
+
8
+ const productUrls: Record<string, string> = {
9
+ config: config.PLATFORM_APP_URL,
10
+ pricing: config.PRICING_APP_URL,
11
+ connect: config.CONNECT_APP_URL,
12
+ chat: config.CHAT_APP_URL,
13
+ pms: config.PMS_APP_URL,
14
+ }
15
+
16
+ /** All enabled product keys (from /permission + always-on "config"). */
17
+ const enabledProducts = computed<SuiteProduct[]>(() => {
18
+ // "config" is a new SuiteProduct value added in smartness-nuxt-ui — cast
19
+ // needed until the updated nuxt-ui-layer package is published.
20
+ const enabled: SuiteProduct[] = ['config' as SuiteProduct]
21
+
22
+ if (!permissions.value)
23
+ return enabled
24
+
25
+ const products = permissions.value.products
26
+ const mapping: Record<string, SuiteProduct> = {
27
+ pricing: 'pricing',
28
+ connect: 'connect',
29
+ chat: 'chat',
30
+ smartpms: 'pms',
31
+ }
32
+
33
+ for (const [key, product] of Object.entries(products)) {
34
+ if (product.enabled) {
35
+ const mapped = mapping[key]
36
+ if (mapped)
37
+ enabled.push(mapped)
38
+ }
39
+ }
40
+
41
+ return enabled
42
+ })
43
+
44
+ const isLoading = computed(() => status.value === 'pending')
45
+
46
+ function navigateToProduct(product?: SuiteProduct) {
47
+ if (!product) return
48
+
49
+ const isEnabled = enabledProducts.value.includes(product)
50
+ if (!isEnabled) {
51
+ window.open(`${window.location.origin}/upgrade`, '_blank', 'noopener,noreferrer')
52
+ return
53
+ }
54
+
55
+ const url = productUrls[product]
56
+ if (url)
57
+ location.assign(url)
58
+ }
59
+
60
+ function getProductUrl(product: SuiteProduct): string | undefined {
61
+ return productUrls[product] || undefined
62
+ }
63
+
64
+ return {
65
+ enabledProducts,
66
+ isLoading,
67
+ navigateToProduct,
68
+ getProductUrl,
69
+ }
70
+ }
package/nuxt.config.ts CHANGED
@@ -46,8 +46,15 @@ export default defineNuxtConfig({
46
46
  runtimeConfig: {
47
47
  public: {
48
48
  BACKEND_BASE_URL: '',
49
- ENV_CSRF_COOKIE_NAME: '',
49
+ CSRF_COOKIE_NAME: '',
50
50
  PLATFORM_URL: '',
51
+ ENVIRONMENT: '',
52
+ // Standard product URL convention: {PRODUCT}_APP_URL
53
+ PLATFORM_APP_URL: '',
54
+ PRICING_APP_URL: '',
55
+ CONNECT_APP_URL: '',
56
+ CHAT_APP_URL: '',
57
+ PMS_APP_URL: '',
51
58
  },
52
59
  },
53
60
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev.smartpricing/platform-layer",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./nuxt.config.ts",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "nuxt": "^4.4.2",
22
- "nuxt-ui-layer": "github:smartpricing/smartness-nuxt-ui#v1.6.3",
22
+ "nuxt-ui-layer": "github:smartpricing/smartness-nuxt-ui#v1.6.19-platform.0",
23
23
  "vue": "latest"
24
24
  },
25
25
  "peerDependencies": {
@@ -29,6 +29,6 @@
29
29
  "dev": "nuxi dev .playground",
30
30
  "dev:prepare": "nuxt prepare .playground",
31
31
  "build": "nuxt build .playground",
32
- "postinstall": "nuxt prepare"
32
+ "postinstall": "nuxt prepare .playground"
33
33
  }
34
34
  }