@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: [
|
|
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
|
|
50
|
-
|
|
51
|
-
| `NUXT_PUBLIC_BACKEND_BASE_URL` | Backend API base URL
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
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
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
| [`@pinia/nuxt`](https://pinia.vuejs.org/ssr/nuxt.html)
|
|
69
|
-
| [`@pinia/colada-nuxt`](https://pinia-colada.esm.dev)
|
|
70
|
-
| [`@nuxtjs/i18n`](https://i18n.nuxtjs.org)
|
|
71
|
-
| [`@vueuse/nuxt`](https://vueuse.org/nuxt/README.html)
|
|
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: {
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
| `baseURL`
|
|
100
|
-
| `csrf`
|
|
101
|
-
| `auth`
|
|
102
|
-
| `errorSerializer` | `(context) => unknown`
|
|
103
|
-
| `onRequest`
|
|
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
|
|
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
|
|
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:
|
|
152
|
-
csrf: { cookieName:
|
|
157
|
+
baseURL: "https://api.example.com",
|
|
158
|
+
csrf: { cookieName: "my_csrf" },
|
|
153
159
|
auth: {
|
|
154
|
-
refreshBaseURL:
|
|
155
|
-
platformURL:
|
|
156
|
-
onRefreshFailed: () => navigateTo(
|
|
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 ??
|
|
166
|
+
message: (data as any)?.error?.message ?? "Unknown error",
|
|
161
167
|
}),
|
|
162
|
-
})
|
|
168
|
+
});
|
|
163
169
|
|
|
164
|
-
const users = await client<User[]>(
|
|
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: [
|
|
213
|
-
query: () => client<MyData>(
|
|
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>(
|
|
227
|
-
method:
|
|
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
|
|
254
|
-
|
|
255
|
-
| `signOut`
|
|
256
|
-
| `logoutStatus` | `Ref<'idle' \| 'pending' \| 'success' \| 'error'>` | [Mutation status](https://pinia-colada.esm.dev/guide/mutations.html)
|
|
257
|
-
| `logoutError`
|
|
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
|
|
284
|
-
|
|
285
|
-
| `useSessionQuery()`
|
|
286
|
-
| `usePermissionsQuery()`
|
|
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
|
|
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
|
|
300
|
-
|
|
301
|
-
| `useBillingOverviewQuery(orgId)`
|
|
302
|
-
| `useBillingDocumentsQuery(orgId, params?)`
|
|
303
|
-
| `useLegalEntityQuery(orgId, id)`
|
|
304
|
-
| `useBillingDocumentPdfQuery(orgId, type, id)` | `GET /api/orgs/{orgId}/billing/documents/{type}/{id}/pdf` | `BillingDocumentPdfResponse`
|
|
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
|
|
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
|
|
376
|
-
|
|
377
|
-
| `mutate`
|
|
378
|
-
| `mutateAsync` | `(params) => Promise<TResult>`
|
|
379
|
-
| `status`
|
|
380
|
-
| `asyncStatus` | `Ref<'idle' \| 'loading'>`
|
|
381
|
-
| `data`
|
|
382
|
-
| `error`
|
|
383
|
-
| `variables`
|
|
384
|
-
| `reset`
|
|
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
|
|
389
|
-
|
|
390
|
-
| `useLoginMutation()`
|
|
391
|
-
| `useLogoutMutation()`
|
|
392
|
-
| `useRefreshMutation()`
|
|
393
|
-
| `useActivateMutation()`
|
|
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
|
|
399
|
-
|
|
400
|
-
| `useMfaSetupMutation()`
|
|
401
|
-
| `useMfaStepUpMutation()`
|
|
402
|
-
| `useMfaFinalizeMutation()`
|
|
403
|
-
| `useMfaDisableMutation()`
|
|
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
|
|
409
|
-
|
|
410
|
-
| `useUpdateProfileMutation()`
|
|
411
|
-
| `useChangePasswordMutation()`
|
|
412
|
-
| `useRequestPasswordResetMutation()` | `POST`
|
|
413
|
-
| `useResetPasswordMutation()`
|
|
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
|
|
418
|
-
|
|
419
|
-
| `useCreatePaymentMethodMutation()`
|
|
420
|
-
| `useSetPrimaryPaymentMethodMutation()` | `PATCH`
|
|
421
|
-
| `useRemovePaymentMethodMutation()`
|
|
422
|
-
| `useCreateSetupIntentMutation()`
|
|
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
|
|
427
|
-
|
|
428
|
-
| `useUpdateLegalEntityMutation()`
|
|
429
|
-
| `useRequestLegalEntityChangeMutation()`
|
|
430
|
-
| `useRequestSubscriptionCancellationMutation()` | `POST`
|
|
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(
|
|
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 ===
|
|
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
|
|
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({
|
|
493
|
-
|
|
494
|
-
|
|
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:
|
|
513
|
+
mutate({ name: "New Name" });
|
|
509
514
|
|
|
510
515
|
// Async — errors rethrown, use try/catch
|
|
511
516
|
try {
|
|
512
|
-
const result = await mutateAsync({ name:
|
|
513
|
-
}
|
|
514
|
-
|
|
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: [
|
|
534
|
-
i18n: false,
|
|
535
|
-
pinia: false,
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|