@dev.smartpricing/platform-layer 0.0.2
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/_shared/billingCreditNotes.ts +24 -0
- package/_shared/billingDocumentSubscriptions.ts +36 -0
- package/_shared/billingDocuments.ts +32 -0
- package/_shared/billingInvoices.ts +24 -0
- package/_shared/billingLegalEntities.ts +109 -0
- package/_shared/billingOverview.ts +15 -0
- package/_shared/billingPaymentMethods.ts +89 -0
- package/_shared/billingSubscriptions.ts +54 -0
- package/_shared/common.ts +34 -0
- package/_shared/crossSelling.ts +21 -0
- package/_shared/index.ts +22 -0
- package/_shared/meContext.ts +15 -0
- package/_shared/members.ts +23 -0
- package/_shared/mfa.ts +75 -0
- package/_shared/organizationContext.ts +18 -0
- package/_shared/organizations.ts +46 -0
- package/_shared/password.ts +22 -0
- package/_shared/permissions.ts +16 -0
- package/_shared/products.ts +49 -0
- package/_shared/properties.ts +16 -0
- package/_shared/subscriptionCancellation.ts +17 -0
- package/_shared/unitTypes.ts +16 -0
- package/_shared/user.ts +77 -0
- package/app/components/.gitkeep +0 -0
- package/app/composables/apiClient.composable.ts +25 -0
- package/app/composables/useAuth.composable.ts +17 -0
- package/app/mutations/useAccountingLogin.mutation.ts +10 -0
- package/app/mutations/useActivate.mutation.ts +10 -0
- package/app/mutations/useChangePassword.mutation.ts +10 -0
- package/app/mutations/useCreatePaymentMethod.mutation.ts +13 -0
- package/app/mutations/useCreateSetupIntent.mutation.ts +13 -0
- package/app/mutations/useLogin.mutation.ts +10 -0
- package/app/mutations/useLogout.mutation.ts +7 -0
- package/app/mutations/useMfaDisable.mutation.ts +10 -0
- package/app/mutations/useMfaFinalize.mutation.ts +10 -0
- package/app/mutations/useMfaRegenerateRecoveryCodes.mutation.ts +10 -0
- package/app/mutations/useMfaSetup.mutation.ts +10 -0
- package/app/mutations/useMfaStepUp.mutation.ts +10 -0
- package/app/mutations/useRefresh.mutation.ts +7 -0
- package/app/mutations/useRemovePaymentMethod.mutation.ts +11 -0
- package/app/mutations/useRequestLegalEntityChange.mutation.ts +13 -0
- package/app/mutations/useRequestPasswordReset.mutation.ts +10 -0
- package/app/mutations/useRequestSubscriptionCancellation.mutation.ts +13 -0
- package/app/mutations/useResetPassword.mutation.ts +10 -0
- package/app/mutations/useSetPrimaryPaymentMethod.mutation.ts +13 -0
- package/app/mutations/useUpdateLegalEntity.mutation.ts +13 -0
- package/app/mutations/useUpdateProfile.mutation.ts +10 -0
- package/app/queries/useBillingDocumentPdf.query.ts +22 -0
- package/app/queries/useBillingDocuments.query.ts +35 -0
- package/app/queries/useBillingOverview.query.ts +17 -0
- package/app/queries/useCrossSelling.query.ts +14 -0
- package/app/queries/useLegalEntity.query.ts +20 -0
- package/app/queries/useOrganization.query.ts +17 -0
- package/app/queries/usePermissions.query.ts +14 -0
- package/app/queries/useSession.query.ts +14 -0
- package/app/utils/apiClient.utils.ts +134 -0
- package/nuxt.config.ts +50 -0
- package/package.json +34 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MeContextResponse } from '@package/platform-shared'
|
|
2
|
+
|
|
3
|
+
export const sessionKeys = {
|
|
4
|
+
me: () => ['session', 'me'] as const,
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function useSessionQuery() {
|
|
8
|
+
const client = useApiClient()
|
|
9
|
+
|
|
10
|
+
return useQuery({
|
|
11
|
+
key: sessionKeys.me,
|
|
12
|
+
query: () => client<MeContextResponse>('/api/me'),
|
|
13
|
+
})
|
|
14
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { FetchError } from 'ofetch'
|
|
2
|
+
|
|
3
|
+
export interface ApiClientCsrfConfig {
|
|
4
|
+
cookieName: string
|
|
5
|
+
headerName?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ApiClientMfaConfig {
|
|
9
|
+
shouldChallenge: (status: number, data: unknown) => boolean
|
|
10
|
+
challengePath?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ApiClientAuthConfig {
|
|
14
|
+
refreshBaseURL: string
|
|
15
|
+
refreshEndpoint?: string
|
|
16
|
+
platformURL: string
|
|
17
|
+
shouldRefresh?: (status: number, data: unknown) => boolean
|
|
18
|
+
onRefreshFailed?: () => void
|
|
19
|
+
mfa?: ApiClientMfaConfig
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ApiClientOptions {
|
|
23
|
+
baseURL: string
|
|
24
|
+
csrf?: ApiClientCsrfConfig
|
|
25
|
+
auth?: ApiClientAuthConfig
|
|
26
|
+
errorSerializer?: (context: { status: number; statusText: string; data: unknown }) => unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function defaultShouldRefresh(status: number, data: unknown): boolean {
|
|
30
|
+
if (status !== 401) return false
|
|
31
|
+
const code = (data as { error?: { code?: string } } | undefined)?.error?.code
|
|
32
|
+
return code === 'session.expired' || code === 'session.missing'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Module-level refresh dedup — safe because refresh is client-only
|
|
36
|
+
let refreshPromise: Promise<boolean> | null = null
|
|
37
|
+
|
|
38
|
+
export function createApiClient(options: ApiClientOptions) {
|
|
39
|
+
const client = $fetch.create({
|
|
40
|
+
baseURL: options.baseURL,
|
|
41
|
+
credentials: 'include',
|
|
42
|
+
onRequest({ options: reqOpts }) {
|
|
43
|
+
// SSR: forward cookies from incoming request
|
|
44
|
+
if (import.meta.server) {
|
|
45
|
+
const { cookie } = useRequestHeaders(['cookie'])
|
|
46
|
+
if (cookie) {
|
|
47
|
+
reqOpts.headers.set('cookie', cookie)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// CSRF: inject token header
|
|
52
|
+
if (options.csrf) {
|
|
53
|
+
const token = useCookie(options.csrf.cookieName).value
|
|
54
|
+
if (token) {
|
|
55
|
+
reqOpts.headers.set(options.csrf.headerName ?? 'X-CSRF-Token', token)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// No auth handling needed — return raw $fetch instance
|
|
62
|
+
if (!options.auth) return client
|
|
63
|
+
|
|
64
|
+
const auth = options.auth
|
|
65
|
+
const shouldRefresh = auth.shouldRefresh ?? defaultShouldRefresh
|
|
66
|
+
|
|
67
|
+
function attemptRefresh(): Promise<boolean> {
|
|
68
|
+
if (refreshPromise) return refreshPromise
|
|
69
|
+
refreshPromise = $fetch(auth.refreshEndpoint ?? '/api/auth/refresh', {
|
|
70
|
+
baseURL: auth.refreshBaseURL,
|
|
71
|
+
method: 'POST',
|
|
72
|
+
credentials: 'include',
|
|
73
|
+
})
|
|
74
|
+
.then(() => true)
|
|
75
|
+
.catch(() => false)
|
|
76
|
+
.finally(() => { refreshPromise = null })
|
|
77
|
+
return refreshPromise
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function serializeAndThrow(error: unknown): never {
|
|
81
|
+
if (options.errorSerializer && error instanceof FetchError) {
|
|
82
|
+
throw options.errorSerializer({
|
|
83
|
+
status: error.statusCode ?? 0,
|
|
84
|
+
statusText: error.statusMessage ?? '',
|
|
85
|
+
data: error.data,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
throw error
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Wrap client with refresh + MFA logic (client-only features)
|
|
92
|
+
async function apiFetch<T = unknown>(
|
|
93
|
+
url: string,
|
|
94
|
+
fetchOptions?: Parameters<typeof client>[1],
|
|
95
|
+
): Promise<T> {
|
|
96
|
+
try {
|
|
97
|
+
return await client(url, fetchOptions) as T
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (!(error instanceof FetchError) || !import.meta.client) {
|
|
101
|
+
serializeAndThrow(error)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const status = error.statusCode ?? 0
|
|
105
|
+
const data = error.data
|
|
106
|
+
|
|
107
|
+
// MFA challenge redirect
|
|
108
|
+
if (auth.mfa?.shouldChallenge(status, data)) {
|
|
109
|
+
const returnUrl = encodeURIComponent(window.location.href)
|
|
110
|
+
const path = auth.mfa.challengePath ?? '/auth/mfa-challenge'
|
|
111
|
+
window.location.assign(`${auth.platformURL}${path}?redirect=${returnUrl}`)
|
|
112
|
+
return new Promise<T>(() => {}) // suspend — page is navigating away
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Token refresh + single retry
|
|
116
|
+
if (shouldRefresh(status, data)) {
|
|
117
|
+
const refreshed = await attemptRefresh()
|
|
118
|
+
if (refreshed) {
|
|
119
|
+
try {
|
|
120
|
+
return await client(url, fetchOptions) as T
|
|
121
|
+
}
|
|
122
|
+
catch (retryError) {
|
|
123
|
+
serializeAndThrow(retryError)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
auth.onRefreshFailed?.()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
serializeAndThrow(error)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return apiFetch as typeof client
|
|
134
|
+
}
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
|
|
4
|
+
const currentDir = dirname(fileURLToPath(import.meta.url))
|
|
5
|
+
|
|
6
|
+
export default defineNuxtConfig({
|
|
7
|
+
extends: ['nuxt-ui-layer'],
|
|
8
|
+
|
|
9
|
+
alias: {
|
|
10
|
+
'@package/platform-shared': join(currentDir, '_shared'),
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
modules: [
|
|
14
|
+
'@pinia/nuxt',
|
|
15
|
+
'@pinia/colada-nuxt',
|
|
16
|
+
'@nuxtjs/i18n',
|
|
17
|
+
'@vueuse/nuxt',
|
|
18
|
+
],
|
|
19
|
+
|
|
20
|
+
i18n: {
|
|
21
|
+
strategy: 'no_prefix',
|
|
22
|
+
locales: [
|
|
23
|
+
{ code: 'en', name: 'English', language: 'en' },
|
|
24
|
+
{ code: 'it', name: 'Italiano', language: 'it' },
|
|
25
|
+
{ code: 'de', name: 'Deutsch', language: 'de' },
|
|
26
|
+
{ code: 'es', name: 'Español', language: 'es' },
|
|
27
|
+
],
|
|
28
|
+
defaultLocale: 'en',
|
|
29
|
+
compilation: {
|
|
30
|
+
strictMessage: false,
|
|
31
|
+
},
|
|
32
|
+
detectBrowserLanguage: {
|
|
33
|
+
useCookie: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
imports: {
|
|
38
|
+
dirs: [
|
|
39
|
+
'queries','mutations',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
runtimeConfig: {
|
|
44
|
+
public: {
|
|
45
|
+
BACKEND_BASE_URL: '',
|
|
46
|
+
ENV_CSRF_COOKIE_NAME: '',
|
|
47
|
+
PLATFORM_URL: '',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dev.smartpricing/platform-layer",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": false,
|
|
6
|
+
"main": "./nuxt.config.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"app",
|
|
9
|
+
"_shared",
|
|
10
|
+
"nuxt.config.ts"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@nuxtjs/i18n": "^10.3.0",
|
|
14
|
+
"@pinia/colada": "^1.3.1",
|
|
15
|
+
"@pinia/colada-nuxt": "^1.0.1",
|
|
16
|
+
"@pinia/nuxt": "^0.11.3",
|
|
17
|
+
"@vueuse/core": "^13.9.0",
|
|
18
|
+
"@vueuse/nuxt": "^13.9.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"nuxt": "^4.4.2",
|
|
22
|
+
"nuxt-ui-layer": "github:smartpricing/smartness-nuxt-ui#v1.6.3",
|
|
23
|
+
"vue": "latest"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"nuxt-ui-layer": ">=1.3.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"dev": "nuxi dev .playground",
|
|
30
|
+
"dev:prepare": "nuxt prepare .playground",
|
|
31
|
+
"build": "nuxt build .playground",
|
|
32
|
+
"postinstall": "nuxt prepare"
|
|
33
|
+
}
|
|
34
|
+
}
|