@sbc-connect/nuxt-auth 0.11.1 → 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.
Files changed (32) hide show
  1. package/.env.example +2 -3
  2. package/CHANGELOG.md +17 -0
  3. package/app/components/Connect/Account/Create/Name.vue +24 -22
  4. package/app/components/Connect/Account/Create/index.vue +29 -21
  5. package/app/components/Connect/Header/Notifications.vue +26 -11
  6. package/app/composables/useConnectHeaderOptions.ts +2 -20
  7. package/app/middleware/connect-auth.ts +4 -5
  8. package/app/pages/auth/account/select.vue +51 -26
  9. package/app/pages/auth/terms-of-use.vue +9 -9
  10. package/app/plugins/auth-api.ts +26 -21
  11. package/app/plugins/connect-account-bootstrap.client.ts +39 -0
  12. package/app/plugins/connect-auth.client.ts +102 -88
  13. package/app/services/auth/index.ts +4 -0
  14. package/app/services/auth/mutate.ts +83 -0
  15. package/app/services/auth/query-keys.ts +26 -0
  16. package/app/services/auth/query.ts +87 -0
  17. package/app/services/auth/service.ts +125 -0
  18. package/app/services/helpers/get-cached-or-fetch.ts +43 -0
  19. package/app/services/helpers/index.ts +1 -0
  20. package/app/services/index.ts +1 -0
  21. package/app/stores/connect-account.ts +38 -146
  22. package/app/types/auth-nuxt-hooks.d.ts +7 -0
  23. package/app/utils/index.ts +1 -0
  24. package/app/utils/schemas/account.ts +32 -1
  25. package/app/utils/schemas/index.ts +1 -0
  26. package/nuxt.config.ts +1 -1
  27. package/package.json +3 -3
  28. package/app/composables/useAuthApi.ts +0 -156
  29. package/app/middleware/01.setup-accounts.global.ts +0 -13
  30. /package/app/middleware/{02.keycloak-params.global.ts → 01.keycloak-params.global.ts} +0 -0
  31. /package/app/middleware/{03.app-config-presets.global.ts → 02.app-config-presets.global.ts} +0 -0
  32. /package/app/middleware/{04.idp-enforcement.global.ts → 03.idp-enforcement.global.ts} +0 -0
@@ -1,112 +1,126 @@
1
1
  import Keycloak from 'keycloak-js'
2
2
  import { ConnectModalSessionExpired } from '#components'
3
3
 
4
- export default defineNuxtPlugin(async () => {
5
- const rtc = useRuntimeConfig().public
6
-
7
- // define new keycloak
8
- const keycloak = new Keycloak({
9
- url: rtc.idpUrl,
10
- realm: rtc.idpRealm,
11
- clientId: rtc.idpClientid
12
- })
13
-
14
- const tokenRefreshInterval = rtc.tokenRefreshInterval ? Number(rtc.tokenRefreshInterval) : 30000
15
- const tokenMinValidity = rtc.tokenMinValidity ? Number(rtc.tokenMinValidity) / 1000 : 120
16
- const sessionInactivityTimeout = rtc.sessionInactivityTimeout ? Number(rtc.sessionInactivityTimeout) : 1800000
17
-
18
- // Logout via Keycloak, redirecting to the login page with a return param preserving the user's current URL.
19
- // The return value is double-encoded so it survives the Keycloak redirect decode
20
- // without the original query params bleeding into the login page's query string.
21
- function logoutWithReturn(kc: Keycloak) {
22
- const localePath = useLocalePath()
23
- const route = useRoute()
24
- const loginPath = localePath('/auth/login')
25
- const currentUrl = `${window.location.origin}${route.fullPath}`
26
- const returnUrl = encodeURIComponent(encodeURIComponent(currentUrl))
27
- const redirectUri = `${window.location.origin}${loginPath}?return=${returnUrl}`
28
- kc.logout({ redirectUri })
29
- }
4
+ export default defineNuxtPlugin({
5
+ name: 'connect-auth',
6
+ order: -20,
7
+ parallel: true,
8
+ async setup(nuxtApp) {
9
+ const rtc = nuxtApp.$config.public
30
10
 
31
- try {
32
- // default behaviour when keycloak session expires
33
- // try to update token - log out if token update fails
34
- // callbacks must be registered before 'init'
35
- // https://www.keycloak.org/securing-apps/javascript-adapter#_callback_events
36
- keycloak.onTokenExpired = async () => {
37
- try {
38
- console.info('[Auth] Token expired, refreshing token...')
39
- await keycloak.updateToken(tokenMinValidity)
40
- console.info('[Auth] Token refreshed.')
41
- } catch (error) {
42
- console.error('[Auth] Failed to refresh token on expiration; logging out.', error)
43
- logoutWithReturn(keycloak)
44
- }
45
- }
46
-
47
- // init keycloak instance
48
- await keycloak.init({
49
- onLoad: 'check-sso',
50
- responseMode: 'query',
51
- pkceMethod: 'S256'
11
+ // define new keycloak
12
+ const keycloak = new Keycloak({
13
+ url: rtc.idpUrl,
14
+ realm: rtc.idpRealm,
15
+ clientId: rtc.idpClientid
52
16
  })
53
- } catch (error) {
54
- console.error('[Auth] Failed to initialize Keycloak adapter: ', error)
55
- }
56
17
 
57
- const { idle } = useIdle(sessionInactivityTimeout)
18
+ const tokenRefreshInterval = rtc.tokenRefreshInterval ? Number(rtc.tokenRefreshInterval) : 30000
19
+ const tokenMinValidity = rtc.tokenMinValidity ? Number(rtc.tokenMinValidity) / 1000 : 120
20
+ const sessionInactivityTimeout = rtc.sessionInactivityTimeout ? Number(rtc.sessionInactivityTimeout) : 1800000
58
21
 
59
- // executed when user is authenticated and idle = true
60
- async function sessionExpired() {
61
- const overlay = useOverlay()
62
- const modal = overlay.create(ConnectModalSessionExpired)
63
- modal.open()
64
- }
22
+ // Logout via Keycloak, redirecting to the login page with a return param preserving the user's current URL.
23
+ // The return value is double-encoded so it survives the Keycloak redirect decode
24
+ // without the original query params bleeding into the login page's query string.
25
+ function logoutWithReturn(kc: Keycloak) {
26
+ const localePath = useLocalePath()
27
+ const route = useRoute()
28
+ const loginPath = localePath('/auth/login')
29
+ const currentUrl = `${window.location.origin}${route.fullPath}`
30
+ const returnUrl = encodeURIComponent(encodeURIComponent(currentUrl))
31
+ const redirectUri = `${window.location.origin}${loginPath}?return=${returnUrl}`
32
+ kc.logout({ redirectUri })
33
+ }
65
34
 
66
- // refresh token if expiring within <tokenMinValidity> - checks every <tokenRefreshInterval>
67
- function scheduleRefreshToken() {
68
- console.info('[Auth] Verifying token validity.')
35
+ try {
36
+ // callbacks must be registered before 'init'
37
+ // https://www.keycloak.org/securing-apps/javascript-adapter#_callback_events
69
38
 
70
- setTimeout(async () => {
71
- if (keycloak.isTokenExpired(tokenMinValidity)) {
72
- console.info('[Auth] Token set to expire soon. Refreshing token...')
39
+ // default behaviour when keycloak session expires
40
+ // try to update token - log out if token update fails
41
+ keycloak.onTokenExpired = async () => {
73
42
  try {
43
+ console.info('[Auth] Token expired, refreshing token...')
74
44
  await keycloak.updateToken(tokenMinValidity)
75
45
  console.info('[Auth] Token refreshed.')
76
46
  } catch (error) {
77
- console.error('[Auth] Failed to refresh token; logging out.', error)
47
+ console.error('[Auth] Failed to refresh token on expiration; logging out.', error)
78
48
  logoutWithReturn(keycloak)
79
49
  }
80
50
  }
81
51
 
82
- scheduleRefreshToken()
83
- }, tokenRefreshInterval)
84
- }
52
+ keycloak.onAuthSuccess = async () => {
53
+ await nuxtApp.callHook('connect:auth:refresh', { token: keycloak.token })
54
+ }
85
55
 
86
- // Watch for changes in authentication and idle state
87
- // When the user is authenticated and not idle, start the refresh schedule
88
- // Execute session expiry handling if user authenticated and inactive
89
- watch(
90
- [() => keycloak.authenticated, () => idle.value],
91
- async ([isAuth, isIdle]) => {
92
- if (isAuth) {
93
- sessionStorage.removeItem(ConnectAuthStorageKey.CONNECT_SESSION_EXPIRED)
94
- if (!isIdle) {
95
- console.info('[Auth] Starting token refresh schedule.')
96
- scheduleRefreshToken()
97
- } else {
98
- console.warn('[Auth] User session expired due to inactivity. Starting session expiry process.')
99
- await sessionExpired()
100
- }
56
+ keycloak.onAuthRefreshSuccess = async () => {
57
+ await nuxtApp.callHook('connect:auth:refresh', { token: keycloak.token })
101
58
  }
102
- },
103
- { immediate: true }
104
- )
105
59
 
106
- return {
107
- provide: {
60
+ // init keycloak instance
61
+ await keycloak.init({
62
+ onLoad: 'check-sso',
63
+ responseMode: 'query',
64
+ pkceMethod: 'S256'
65
+ })
66
+ } catch (error) {
67
+ console.error('[Auth] Failed to initialize Keycloak adapter: ', error)
68
+ }
69
+
70
+ const { idle } = useIdle(sessionInactivityTimeout)
71
+
72
+ // executed when user is authenticated and idle = true
73
+ async function sessionExpired() {
74
+ const overlay = useOverlay()
75
+ const modal = overlay.create(ConnectModalSessionExpired)
76
+ modal.open()
77
+ }
78
+
79
+ // refresh token if expiring within <tokenMinValidity> - checks every <tokenRefreshInterval>
80
+ function scheduleRefreshToken() {
81
+ console.info('[Auth] Verifying token validity.')
82
+
83
+ setTimeout(async () => {
84
+ if (keycloak.isTokenExpired(tokenMinValidity)) {
85
+ console.info('[Auth] Token set to expire soon. Refreshing token...')
86
+ try {
87
+ await keycloak.updateToken(tokenMinValidity)
88
+ console.info('[Auth] Token refreshed.')
89
+ } catch (error) {
90
+ console.error('[Auth] Failed to refresh token; logging out.', error)
91
+ logoutWithReturn(keycloak)
92
+ }
93
+ }
94
+
95
+ scheduleRefreshToken()
96
+ }, tokenRefreshInterval)
97
+ }
98
+
99
+ // Watch for changes in authentication and idle state
100
+ // When the user is authenticated and not idle, start the refresh schedule
101
+ // Execute session expiry handling if user authenticated and inactive
102
+ watch(
103
+ [() => keycloak.authenticated, () => idle.value],
104
+ async ([isAuth, isIdle]) => {
105
+ if (isAuth) {
106
+ sessionStorage.removeItem(ConnectAuthStorageKey.CONNECT_SESSION_EXPIRED)
107
+ if (!isIdle) {
108
+ console.info('[Auth] Starting token refresh schedule.')
109
+ scheduleRefreshToken()
110
+ } else {
111
+ console.warn('[Auth] User session expired due to inactivity. Starting session expiry process.')
112
+ await sessionExpired()
113
+ }
114
+ }
115
+ },
116
+ { immediate: true }
117
+ )
118
+
119
+ return {
120
+ provide: {
108
121
  // provide global auth instance
109
- connectAuth: keycloak
122
+ connectAuth: keycloak
123
+ }
110
124
  }
111
125
  }
112
126
  })
@@ -0,0 +1,4 @@
1
+ export * from './mutate'
2
+ export * from './query'
3
+ export * from './query-keys'
4
+ export * from './service'
@@ -0,0 +1,83 @@
1
+ // https://pinia-colada.esm.dev/guide/mutations.html
2
+
3
+ export const useConnectAuthMutation = () => {
4
+ const service = useConnectAuthService()
5
+ const { keys } = useConnectAuthQueryKeys()
6
+ const queryCache = useQueryCache()
7
+
8
+ /**
9
+ * Creates an account by POSTing the given payload to `/orgs`.
10
+ * @returns Object containing mutation state and `createAccount` function.
11
+ */
12
+ const createAccount = defineMutation({
13
+ mutation: (vars: {
14
+ payload: ConnectCreateAccount
15
+ silent?: boolean
16
+ successCb?: (createRes: ConnectAuthProfile) => Promise<unknown>
17
+ }) => service.createAccount(vars.payload),
18
+ onError: (_, _vars) => {
19
+ if (!_vars.silent) {
20
+ useConnectAuthModals().openCreateAccountModal()
21
+ }
22
+ },
23
+ onSuccess: async (_, _vars) => {
24
+ await queryCache.invalidateQueries({ key: keys.userSettings(), exact: true })
25
+ if (_vars.successCb) {
26
+ await _vars.successCb(_)
27
+ }
28
+ }
29
+ })
30
+
31
+ /**
32
+ * Updates a users contact by PUTing the given payload to `/users/contacts`.
33
+ * @returns Object containing mutation state and `updateUserContact` function.
34
+ */
35
+ const updateOrCreateUserContact = defineMutation({
36
+ mutation: (vars: {
37
+ payload: { email: string, phone: string, phoneExtension: string | undefined }
38
+ method?: 'POST' | 'PUT'
39
+ silent?: boolean
40
+ successCb?: () => Promise<unknown> | unknown
41
+ errorCb?: (error: unknown) => Promise<unknown> | unknown
42
+ }) => service.updateOrCreateUserContact(vars.payload, vars.method),
43
+ onError: async (error, _vars) => {
44
+ if (!_vars.silent) {
45
+ useConnectAuthModals().openUpdateUserContactModal()
46
+ }
47
+ if (_vars.errorCb) {
48
+ await _vars.errorCb(error)
49
+ }
50
+ },
51
+ onSuccess: async (_, _vars) => {
52
+ await queryCache.invalidateQueries({ key: keys.userProfile(), exact: true })
53
+ if (_vars.successCb) {
54
+ await _vars.successCb()
55
+ }
56
+ }
57
+ })
58
+
59
+ const updateTermsOfUse = defineMutation({
60
+ mutation: (vars: {
61
+ payload: { accepted: boolean, version: string }
62
+ silent?: boolean
63
+ successCb?: () => Promise<unknown> | unknown
64
+ }) => service.updateTermsOfUse(vars.payload),
65
+ onError: (_, _vars) => {
66
+ if (!_vars.silent) {
67
+ useConnectAuthModals().openPatchTosErrorModal()
68
+ }
69
+ },
70
+ onSuccess: async (_, _vars) => {
71
+ await queryCache.invalidateQueries({ key: keys.userProfile(), exact: true })
72
+ if (_vars.successCb) {
73
+ await _vars.successCb()
74
+ }
75
+ }
76
+ })
77
+
78
+ return {
79
+ createAccount,
80
+ updateOrCreateUserContact,
81
+ updateTermsOfUse
82
+ }
83
+ }
@@ -0,0 +1,26 @@
1
+ // https://pinia-colada.esm.dev/guide/query-keys.html
2
+
3
+ /**
4
+ * IMPORTANT: Query Key Hierarchy
5
+ * * Our cache follows a strict hierarchy: ['connect', 'auth', keycloakGuid, ...rest].
6
+ * * 1. GUID ISOLATION: The 'base' (keycloakGuid) MUST remain as the first dynamic segment.
7
+ * This allows us to invalidate an entire "folder" by targeting the parent key.
8
+ * * 2. ORDER: When updating keys, ensure the order of existing segments is preserved.
9
+ * Changing the order may break existing invalidation logic across the app.
10
+ */
11
+
12
+ export const useConnectAuthQueryKeys = () => {
13
+ const { authUser } = useConnectAuth()
14
+ const { currentAccount } = storeToRefs(useConnectAccountStore())
15
+
16
+ const base = () => ['connect', 'auth', authUser.value?.keycloakGuid] as const
17
+
18
+ const keys = {
19
+ userProfile: () => [...base(), 'user-profile'] as const,
20
+ pendingApprovals: () => [...base(), 'org', currentAccount.value?.id, 'pending-approvals'] as const,
21
+ termsOfUse: () => [...base(), 'terms-of-use'] as const,
22
+ userSettings: () => [...base(), 'user-settings'] as const
23
+ }
24
+
25
+ return { keys }
26
+ }
@@ -0,0 +1,87 @@
1
+ // IMPORTANT: Query definitions are for GET requests only - for all other methods define a mutation in the ./mutate file
2
+ // https://pinia-colada.esm.dev/guide/queries.html
3
+ import { useQuery } from '@pinia/colada'
4
+ import type { UseQueryOptions, DefineQueryOptions } from '@pinia/colada'
5
+
6
+ type QueryOptions<T> = Omit<UseQueryOptions<T>, 'key' | 'query'> & {
7
+ query?: UseQueryOptions<T>['query']
8
+ }
9
+
10
+ type DefineOptions<TData, TError = Error> = Omit<DefineQueryOptions<TData, TError>, 'key' | 'query'> & {
11
+ query?: DefineQueryOptions<TData, TError>['query']
12
+ }
13
+
14
+ const DEFAULT_STALE_TIME = 60000
15
+
16
+ export const useConnectAuthQuery = () => {
17
+ const { $authApi } = useNuxtApp()
18
+ const { keys } = useConnectAuthQueryKeys()
19
+
20
+ function pendingApprovalsOptions(options?: DefineOptions<{ count: number }>) {
21
+ const accountId = useConnectAccountStore().currentAccount?.id
22
+ const keycloakGuid = useConnectAuth().authUser.value?.keycloakGuid
23
+ return defineQueryOptions({
24
+ query: () => $authApi<{ count: number }>(`/users/${keycloakGuid}/org/${accountId}/notifications`),
25
+ staleTime: DEFAULT_STALE_TIME,
26
+ enabled: !!(accountId && keycloakGuid),
27
+ ...options,
28
+ key: keys.pendingApprovals()
29
+ })
30
+ }
31
+
32
+ function pendingApprovals(options?: QueryOptions<{ count: number }>) {
33
+ return useQuery(() => pendingApprovalsOptions(options as DefineOptions<{ count: number }>))
34
+ }
35
+
36
+ function termsOfUseOptions(options?: DefineOptions<ConnectTermsOfUse>) {
37
+ return defineQueryOptions({
38
+ query: () => $authApi<ConnectTermsOfUse>('/documents/termsofuse'),
39
+ staleTime: DEFAULT_STALE_TIME,
40
+ ...options,
41
+ key: keys.termsOfUse()
42
+ })
43
+ }
44
+
45
+ function termsOfUse(options?: QueryOptions<ConnectTermsOfUse>) {
46
+ return useQuery(() => termsOfUseOptions(options as DefineOptions<ConnectTermsOfUse>))
47
+ }
48
+
49
+ function userProfileOptions(options?: DefineOptions<ConnectAuthProfile>) {
50
+ return defineQueryOptions({
51
+ query: () => $authApi<ConnectAuthProfile>('/users/@me', { parseResponse: JSON.parse }),
52
+ staleTime: DEFAULT_STALE_TIME,
53
+ ...options,
54
+ key: keys.userProfile()
55
+ })
56
+ }
57
+
58
+ function userProfile(options?: QueryOptions<ConnectAuthProfile>) {
59
+ return useQuery(() => userProfileOptions(options as DefineOptions<ConnectAuthProfile>))
60
+ }
61
+
62
+ function userSettingsOptions(options?: DefineOptions<ConnectUserSettings[]>) {
63
+ const keycloakGuid = useConnectAuth().authUser.value?.keycloakGuid
64
+ return defineQueryOptions({
65
+ query: () => $authApi<ConnectUserSettings[]>(`/users/${keycloakGuid}/settings`),
66
+ staleTime: DEFAULT_STALE_TIME,
67
+ enabled: !!keycloakGuid,
68
+ ...options,
69
+ key: keys.userSettings()
70
+ })
71
+ }
72
+
73
+ function userSettings(options?: QueryOptions<ConnectUserSettings[]>) {
74
+ return useQuery(() => userSettingsOptions(options as DefineOptions<ConnectUserSettings[]>))
75
+ }
76
+
77
+ return {
78
+ pendingApprovalsOptions,
79
+ pendingApprovals,
80
+ termsOfUse,
81
+ termsOfUseOptions,
82
+ userProfileOptions,
83
+ userProfile,
84
+ userSettingsOptions,
85
+ userSettings
86
+ }
87
+ }
@@ -0,0 +1,125 @@
1
+ // IMPORTANT: This service is an abstraction layer for non-reactive contexts (stores, middleware, or sequential logic).
2
+ // IMPORTANT: Pure GET requests bound to UI components should NOT be defined here;
3
+ // define those using query options/composables directly.
4
+ import { getCachedOrFetch } from '../helpers'
5
+
6
+ /**
7
+ * AUTH SERVICE
8
+ *
9
+ * This service provides a way to execute auth requests while maintaining the cache layer
10
+ * outside of the reactive Vue lifecycle.
11
+ *
12
+ * * USE THIS SERVICE ONLY IF:
13
+ * - You are inside a pinia store and need to "await" data.
14
+ * - You are in a route guard/middleware and need to validate data before entry.
15
+ * - You need to await multiple API calls sequentially (promise.all/setup logic).
16
+ *
17
+ * * DO NOT USE THIS SERVICE IF:
18
+ * - You are inside a Vue Component.
19
+ * - You need to display "isLoading" or "error" states in the UI.
20
+ * - You want automatic background refetching and observer management.
21
+ *
22
+ * * COMPONENT USAGE:
23
+ * Always prefer queries or mutations directly in components.
24
+ */
25
+
26
+ // Example:
27
+
28
+ // Incorrect Component setup
29
+ //
30
+ // const service = useConnectAuthService()
31
+ // const loading = ref(true)
32
+ // const accounts = ref()
33
+ // We have to manually manage everything
34
+ // onMounted(async () => {
35
+ // try {
36
+ // accounts.value = await service.getUserAccounts()
37
+ // } catch (e) {
38
+ // ...handle error
39
+ // } finally {
40
+ // loading.value = false
41
+ // }
42
+ // })
43
+
44
+ // Correct Component setup
45
+ // const query = useConnectAuthQuery()
46
+ // One line handles loading, data, reactivity, and caching
47
+ // const { data: userSettings, isLoading } = query.getUserSettings()
48
+
49
+ export const useConnectAuthService = () => {
50
+ const query = useConnectAuthQuery()
51
+ const { $authApi } = useNuxtApp()
52
+
53
+ /* GET Requests */
54
+
55
+ async function getAuthUserProfile(force = false): Promise<ConnectAuthProfile> {
56
+ const options = query.userProfileOptions()
57
+ return await getCachedOrFetch(options, force)
58
+ }
59
+
60
+ async function getTermsOfUse(force = false): Promise<ConnectTermsOfUse> {
61
+ const options = query.termsOfUseOptions()
62
+ return await getCachedOrFetch(options, force)
63
+ }
64
+
65
+ async function getUserAccounts(force = false): Promise<ConnectAccount[]> {
66
+ const options = query.userSettingsOptions()
67
+ return await getCachedOrFetch(options, force)
68
+ .then(res => res?.filter(setting => setting.type === UserSettingsType.ACCOUNT)) as ConnectAccount[]
69
+ }
70
+
71
+ // Cache exception - do not cache to verify the account name accurately
72
+ async function verifyAccountName(accountName: string) {
73
+ return await $authApi.raw(`/orgs?validateName=true&name=${encodeURIComponent(accountName)}`)
74
+ }
75
+
76
+ /* POST, PUT, PATCH, DELETE Requests */
77
+
78
+ async function createAccount(payload: ConnectCreateAccount): Promise<ConnectAuthProfile> {
79
+ return $authApi<ConnectAuthProfile>('/orgs', {
80
+ method: 'POST',
81
+ body: payload
82
+ })
83
+ }
84
+
85
+ /** Update user information in AUTH with current token info */
86
+ async function updateAuthUserProfile(): Promise<ConnectAuthProfile> {
87
+ return await $authApi('/users', {
88
+ method: 'POST',
89
+ body: { isLogin: true }
90
+ })
91
+ }
92
+
93
+ async function updateOrCreateUserContact(
94
+ payload: { email: string, phone: string, phoneExtension: string | undefined },
95
+ method?: 'POST' | 'PUT'
96
+ ): Promise<ConnectAuthProfile> {
97
+ return $authApi<ConnectAuthProfile>('/users/contacts', {
98
+ method: method || 'PUT',
99
+ body: payload
100
+ })
101
+ }
102
+
103
+ async function updateTermsOfUse(payload: { accepted: boolean, version: string }): Promise<ConnectAuthProfile> {
104
+ return await $authApi<ConnectAuthProfile>('/users/@me', {
105
+ method: 'PATCH',
106
+ body: {
107
+ istermsaccepted: payload.accepted,
108
+ termsversion: payload.version
109
+ }
110
+ })
111
+ }
112
+
113
+ return {
114
+ /* GET Requests */
115
+ getAuthUserProfile,
116
+ getTermsOfUse,
117
+ getUserAccounts,
118
+ verifyAccountName,
119
+ /* POST, PUT, PATCH, DELETE Requests */
120
+ createAccount,
121
+ updateAuthUserProfile,
122
+ updateOrCreateUserContact,
123
+ updateTermsOfUse
124
+ }
125
+ }
@@ -0,0 +1,43 @@
1
+ import type { DefineQueryOptions } from '@pinia/colada'
2
+
3
+ /**
4
+ * Resolves a query from the cache or fetches it from the network if necessary.
5
+ * * This utility ensures a cache entry exists and handles data fetching
6
+ * based on the current cache state and the `force` flag.
7
+ *
8
+ * @template TData - The expected shape of the data returned by the query.
9
+ * @template TError - The error type if the query fails. Defaults to `Error`.
10
+ * * @param options - The query definition object
11
+ * containing the key, query function, and cache settings (e.g., staleTime).
12
+ * @param [force=false] - When true, bypasses the cache and triggers
13
+ * a network call (cache.fetch). When false, returns cached data if fresh,
14
+ * or background refreshes if stale or in error state (cache.refresh).
15
+ * * @returns A promise that resolves to the query data.
16
+ * @throws throws the underlying network error if the fetch or refresh fails.
17
+ * * @example
18
+ * // uses cache if fresh
19
+ * const business = await getCachedOrFetch(businessOptions('BC123'));
20
+ * * // Forced fetch: ignores cache and hits API
21
+ * const freshData = await getCachedOrFetch(businessOptions('BC123'), true);
22
+ */
23
+ export async function getCachedOrFetch<TData>(
24
+ options: DefineQueryOptions<TData>,
25
+ force = false
26
+ ): Promise<TData> {
27
+ const cache = useQueryCache()
28
+ // NB: Required to prevent calls within 1ms causing a promise to return before the cached response is complete.
29
+ // i.e. without below:
30
+ // -> Call 1 for BC1234567 data
31
+ // -> Call 2 for BC1234567 data within 1ms
32
+ // -> Call 1 is cancelled
33
+ // -> Promise returns with undefined response from Call 1 instead of awaiting Call 2
34
+ await new Promise(resolve => setTimeout(resolve, 1))
35
+ // Ensures a query entry is present in the cache.
36
+ // will create one if it doesn't exist
37
+ const entry = cache.ensure(options)
38
+ const result = force
39
+ ? await cache.fetch(entry)
40
+ : await cache.refresh(entry)
41
+
42
+ return result.data!
43
+ }
@@ -0,0 +1 @@
1
+ export * from './get-cached-or-fetch'
@@ -0,0 +1 @@
1
+ export * from './auth'