@sbc-connect/nuxt-auth 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -3
- package/CHANGELOG.md +19 -0
- package/app/components/Connect/Account/Create/Name.vue +24 -22
- package/app/components/Connect/Account/Create/index.vue +29 -21
- package/app/components/Connect/Header/Notifications.vue +26 -11
- package/app/composables/useConnectHeaderOptions.ts +2 -20
- package/app/middleware/connect-auth.ts +4 -5
- package/app/pages/auth/account/select.vue +51 -26
- package/app/pages/auth/terms-of-use.vue +9 -9
- package/app/plugins/auth-api.ts +26 -21
- package/app/plugins/connect-account-bootstrap.client.ts +33 -0
- package/app/plugins/connect-auth.client.ts +102 -88
- package/app/services/auth/index.ts +4 -0
- package/app/services/auth/mutate.ts +83 -0
- package/app/services/auth/query-keys.ts +26 -0
- package/app/services/auth/query.ts +87 -0
- package/app/services/auth/service.ts +125 -0
- package/app/services/helpers/get-cached-or-fetch.ts +43 -0
- package/app/services/helpers/index.ts +1 -0
- package/app/services/index.ts +1 -0
- package/app/stores/connect-account.ts +38 -146
- package/app/types/auth-nuxt-hooks.d.ts +7 -0
- package/app/utils/index.ts +1 -0
- package/app/utils/schemas/account.ts +32 -1
- package/app/utils/schemas/index.ts +1 -0
- package/nuxt.config.ts +1 -1
- package/package.json +5 -5
- package/app/composables/useAuthApi.ts +0 -156
- package/app/middleware/01.setup-accounts.global.ts +0 -13
- /package/app/middleware/{02.keycloak-params.global.ts → 01.keycloak-params.global.ts} +0 -0
- /package/app/middleware/{03.app-config-presets.global.ts → 02.app-config-presets.global.ts} +0 -0
- /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(
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
35
|
+
try {
|
|
36
|
+
// callbacks must be registered before 'init'
|
|
37
|
+
// https://www.keycloak.org/securing-apps/javascript-adapter#_callback_events
|
|
69
38
|
|
|
70
|
-
|
|
71
|
-
if
|
|
72
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
52
|
+
keycloak.onAuthSuccess = async () => {
|
|
53
|
+
await nuxtApp.callHook('connect:auth:refresh', { token: keycloak.token })
|
|
54
|
+
}
|
|
85
55
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
122
|
+
connectAuth: keycloak
|
|
123
|
+
}
|
|
110
124
|
}
|
|
111
125
|
}
|
|
112
126
|
})
|
|
@@ -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'
|