@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,30 +1,19 @@
1
- import { getAccountCreateSchema } from '#auth/app/utils/schemas/account'
2
- import type { AccountProfileSchema } from '#auth/app/utils/schemas/account'
3
-
4
1
  export const useConnectAccountStore = defineStore('connect-auth-account-store', () => {
5
- const { $authApi } = useNuxtApp()
6
2
  const rtc = useRuntimeConfig().public
7
3
  const { authUser } = useConnectAuth()
8
- const { useCreateAccount, useUpdateOrCreateUserContact, getAuthUserProfile } = useAuthApi()
9
- const { finalRedirect } = useConnectAccountFlowRedirect()
4
+ const service = useConnectAuthService()
5
+ // FUTURE: uncomment when fix is made in auth api - ticket: 33711
6
+ // const queryCache = useQueryCache()
7
+ // const { keys } = useConnectAuthQueryKeys()
10
8
 
11
- // selected user account
12
9
  const currentAccount = ref<ConnectAccount>({} as ConnectAccount)
13
10
  const userAccounts = ref<ConnectAccount[]>([])
14
11
  const currentAccountName = computed<string>(() => currentAccount.value?.label || '')
15
- const pendingApprovalCount = ref<number>(0)
16
- const user = computed(() => authUser.value)
17
12
  const userEmail = ref<string>('')
18
- const userFirstName = ref<string>(user.value?.firstName || '-')
19
- const userLastName = ref<string>(user.value?.lastName || '')
13
+ const userFirstName = ref<string>(authUser.value?.firstName || '-')
14
+ const userLastName = ref<string>(authUser.value?.lastName || '')
20
15
  const userFullName = computed(() => `${userFirstName.value} ${userLastName.value}`)
21
16
 
22
- // Create account
23
- const isLoading = ref<boolean>(false)
24
- const { createAccount } = useCreateAccount()
25
- const { updateOrCreateUserContact } = useUpdateOrCreateUserContact()
26
- const createAccountProfileSchema = getAccountCreateSchema()
27
- const accountFormState = reactive<AccountProfileSchema>(createAccountProfileSchema.parse({}))
28
17
  /**
29
18
  * Checks if the current account or the Keycloak user has any of the specified roles.
30
19
  *
@@ -52,103 +41,37 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
52
41
  return accountId === currentAccount.value.id
53
42
  }
54
43
 
55
- /** Update user information in AUTH with current token info */
56
- async function updateAuthUserInfo(): Promise<void> {
57
- await $authApi('/users', {
58
- method: 'POST',
59
- body: { isLogin: true }
60
- })
61
- }
44
+ /** Set user name and default email from profile */
45
+ async function syncUserProfile() {
46
+ const profile = await service.updateAuthUserProfile().catch(() => undefined)
62
47
 
63
- /** Map AccountFormState -> CreateAccountPayload */
64
- function createAccountPayload(): ConnectCreateAccount {
65
- return {
66
- accessType: ConnectAccessType.REGULAR,
67
- mailingAddress: {
68
- city: accountFormState.address.city,
69
- country: accountFormState.address.country,
70
- region: accountFormState.address.region ?? '',
71
- postalCode: accountFormState.address.postalCode ?? '',
72
- street: accountFormState.address.street,
73
- streetAdditional: accountFormState.address.streetAdditional || '',
74
- deliveryInstructions: accountFormState.address.locationDescription || ''
75
- },
76
- name: accountFormState.accountName,
77
- paymentInfo: { paymentMethod: ConnectPaymentMethod.DIRECT_PAY },
78
- productSubscriptions: [{ productCode: ConnectProductCode.BUSINESS }]
48
+ if (!profile) {
49
+ return
79
50
  }
80
- }
81
51
 
82
- /** Submit create account and user contact update requests */
83
- async function submitCreateAccount(): Promise<void> {
84
- try {
85
- isLoading.value = true
86
- // Create Account
87
- const payload = createAccountPayload()
88
- await createAccount({
89
- payload,
90
- // Update User Contact Info on create account success
91
- successCb: async (createResponse: ConnectAuthProfile) => {
92
- // Refresh and switch to new account prior to redirect
93
- if (createResponse?.id) {
94
- await setAccountInfo()
95
- await switchCurrentAccount(createResponse.id)
96
- }
52
+ const { firstname, lastname, contacts } = profile
97
53
 
98
- // Update or create user contact and then redirect regardless of success or failure
99
- await updateOrCreateUserContact({
100
- email: accountFormState.emailAddress,
101
- phone: accountFormState.phone.phoneNumber,
102
- phoneExtension: accountFormState.phone.ext,
103
- method: userAccounts.value.length === 1 ? 'POST' : 'PUT', // if only 1 account exists then contact is new
104
- successCb: async () => await finalRedirect(useRoute()),
105
- errorCb: async () => await finalRedirect(useRoute())
106
- })
107
- }
108
- })
109
- } catch (error) {
110
- // Error handled in useAuthApi
111
- console.error('Account Create Submission Error: ', error)
112
- } finally {
113
- isLoading.value = false
114
- }
115
- }
116
-
117
- /** Set user name and default email from profile */
118
- async function setUserName() {
119
- const { data, refresh } = await getAuthUserProfile()
120
- await refresh()
121
- if (data.value?.firstname && data.value?.lastname) {
122
- userFirstName.value = data.value.firstname
123
- userLastName.value = data.value.lastname
54
+ if (firstname && lastname) {
55
+ userFirstName.value = firstname
56
+ userLastName.value = lastname
124
57
  } else {
125
- userFirstName.value = user.value?.firstName || '-'
126
- userLastName.value = user.value?.lastName || ''
58
+ userFirstName.value = authUser.value?.firstName || '-'
59
+ userLastName.value = authUser.value?.lastName || ''
127
60
  }
128
61
 
129
- // Pre-populate email from the user's existing contact if available
130
- const contactEmail = data.value?.contacts?.[0]?.email
62
+ // set email from the user's existing contact if available
63
+ const contactEmail = contacts?.[0]?.email
131
64
  if (contactEmail) {
132
65
  userEmail.value = contactEmail
133
- if (!accountFormState.emailAddress) {
134
- accountFormState.emailAddress = contactEmail
135
- }
136
66
  }
137
- }
138
67
 
139
- /** Get the user's account list */
140
- async function getUserAccounts(): Promise<ConnectAccount[] | undefined> {
141
- if (!authUser.value?.keycloakGuid && !rtc.playwright) {
142
- return undefined
143
- }
144
- // TODO: use orgs fetch instead to get branch name ? $authApi<UserSettings[]>('/users/orgs')
145
- const response = await $authApi<ConnectUserSettings[]>(`/users/${authUser.value.keycloakGuid}/settings`)
146
- return response?.filter(setting => setting.type === UserSettingsType.ACCOUNT) as ConnectAccount[]
68
+ // add user profile response to cache // FUTURE: uncomment when fix is made in auth api - ticket: 33711
69
+ // queryCache.setQueryData(keys.userProfile(), profile)
147
70
  }
148
71
 
149
72
  /** Set the user account list and current account */
150
- async function setAccountInfo(): Promise<void> {
151
- const accounts = await getUserAccounts()
73
+ async function loadUserAccounts(force = false): Promise<void> {
74
+ const accounts = await service.getUserAccounts(force).catch(() => undefined)
152
75
  if (accounts && accounts[0]) {
153
76
  userAccounts.value = accounts
154
77
  if (!currentAccount.value.id || !userAccounts.value.some(account => account.id === currentAccount.value.id)) {
@@ -158,25 +81,15 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
158
81
  }
159
82
 
160
83
  /** Switch the current account to the given account ID if it exists in the user's account list */
161
- async function switchCurrentAccount(accountId: number) {
84
+ function switchCurrentAccount(accountId: number) {
162
85
  const account = userAccounts.value.find(account => account.id === accountId)
163
86
  if (account) {
164
87
  currentAccount.value = account
165
- await checkAccountStatus()
166
- }
167
- }
168
-
169
- async function getPendingApprovalCount(): Promise<void> {
170
- const accountId = currentAccount.value?.id
171
- const keycloakGuid = authUser.value?.keycloakGuid
172
- if (!accountId || !keycloakGuid) {
173
- return
88
+ return checkAccountStatus()
174
89
  }
175
- const response = await $authApi<{ count: number }>(`/users/${keycloakGuid}/org/${accountId}/notifications`)
176
- pendingApprovalCount.value = response?.count || 0
177
90
  }
178
91
 
179
- async function checkAccountStatus() {
92
+ function checkAccountStatus() {
180
93
  // redirect if account status is suspended or in review
181
94
  if ([AccountStatus.NSF_SUSPENDED, AccountStatus.SUSPENDED].includes(currentAccount.value?.accountStatus)) {
182
95
  // Avoid redirecting when navigating back from PAYBC for NSF or signout.
@@ -187,7 +100,7 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
187
100
  const redirectUrl = `${rtc.authWebUrl}account-freeze`
188
101
  // TODO: should probably change this to check 'appName' when auth starts using the core layer
189
102
  const external = rtc.authWebUrl !== rtc.baseUrl
190
- await navigateTo(redirectUrl, { external })
103
+ return navigateTo(redirectUrl, { external })
191
104
  }
192
105
  } else if (currentAccount.value?.accountStatus === AccountStatus.PENDING_STAFF_REVIEW) {
193
106
  // check the path is allowed for pending approval account
@@ -203,27 +116,20 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
203
116
  const redirectUrl = `${rtc.authWebUrl}pendingapproval/${accountNameEncoded}/true`
204
117
  // TODO: should probably change this to check 'appName' when auth starts using the core layer
205
118
  const external = rtc.authWebUrl !== rtc.baseUrl
206
- await navigateTo(redirectUrl, { external })
119
+ return navigateTo(redirectUrl, { external })
207
120
  }
208
121
  }
209
122
  }
210
123
 
211
- async function initAccountStore(): Promise<void> {
124
+ async function initAccountStore() {
212
125
  try {
213
- await Promise.all([
214
- setAccountInfo(),
215
- updateAuthUserInfo(),
216
- setUserName()
217
- ])
126
+ await loadUserAccounts()
218
127
 
219
128
  if (currentAccount.value.id) {
220
- await Promise.all([
221
- checkAccountStatus(),
222
- getPendingApprovalCount()
223
- ])
129
+ return checkAccountStatus()
224
130
  }
225
131
  } catch (e) {
226
- logFetchError(e, '[Account Store] - Error during initialization')
132
+ logFetchError(e, '[Account Store] - Failed to load user acccounts.')
227
133
  }
228
134
  }
229
135
 
@@ -231,38 +137,24 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
231
137
  sessionStorage.removeItem('connect-auth-account-store')
232
138
  currentAccount.value = {} as ConnectAccount
233
139
  userAccounts.value = []
234
- pendingApprovalCount.value = 0
235
- userFirstName.value = user.value?.firstName || '-'
236
- userLastName.value = user.value?.lastName || ''
237
- clearAccountState()
238
- }
239
-
240
- function clearAccountState() {
241
- Object.assign(accountFormState, createAccountProfileSchema.parse({}))
242
- if (userEmail.value) {
243
- accountFormState.emailAddress = userEmail.value
244
- }
140
+ userFirstName.value = authUser.value?.firstName || '-'
141
+ userLastName.value = authUser.value?.lastName || ''
142
+ userEmail.value = ''
245
143
  }
246
144
 
247
145
  return {
248
- accountFormState,
249
146
  checkAccountStatus,
250
- clearAccountState,
251
- submitCreateAccount,
252
- isLoading,
253
147
  currentAccount,
254
148
  currentAccountName,
255
- getPendingApprovalCount,
256
- getUserAccounts,
257
149
  hasRoles,
258
150
  initAccountStore,
259
151
  isCurrentAccount,
260
- pendingApprovalCount,
261
- setAccountInfo,
262
- setUserName,
152
+ loadUserAccounts,
153
+ syncUserProfile,
263
154
  switchCurrentAccount,
264
155
  userAccounts,
265
156
  userFullName,
157
+ userEmail,
266
158
  $reset
267
159
  }
268
160
  },
@@ -0,0 +1,7 @@
1
+ import type { HookResult } from '@nuxt/schema'
2
+
3
+ declare module '#app' {
4
+ interface RuntimeNuxtHooks {
5
+ 'connect:auth:refresh': (payload: { token: string | undefined }) => HookResult
6
+ }
7
+ }
@@ -1 +1,2 @@
1
1
  export * from './constants'
2
+ export * from './schemas'
@@ -63,7 +63,7 @@ export function getAccountCreateSchema(status: number | undefined = undefined) {
63
63
  locationDescription: ''
64
64
  }),
65
65
  accountName: getAccountNameSchema(status).default(''),
66
- emailAddress: z.string().email().default(''),
66
+ emailAddress: z.email().default(''),
67
67
  phone: getPhoneSchema().default({
68
68
  countryIso2: 'CA',
69
69
  countryCode: '1',
@@ -74,3 +74,34 @@ export function getAccountCreateSchema(status: number | undefined = undefined) {
74
74
  }
75
75
 
76
76
  export type AccountProfileSchema = z.output<ReturnType<typeof getAccountCreateSchema>>
77
+
78
+ export function formatCreateAccountPayload(
79
+ data: AccountProfileSchema
80
+ ): {
81
+ accountPayload: ConnectCreateAccount
82
+ contactPayload: { email: string, phone: string, phoneExtension: string | undefined }
83
+ } {
84
+ const accountPayload = {
85
+ accessType: ConnectAccessType.REGULAR,
86
+ mailingAddress: {
87
+ city: data.address.city,
88
+ country: data.address.country,
89
+ region: data.address.region ?? '',
90
+ postalCode: data.address.postalCode ?? '',
91
+ street: data.address.street,
92
+ streetAdditional: data.address.streetAdditional || '',
93
+ deliveryInstructions: data.address.locationDescription || ''
94
+ },
95
+ name: data.accountName,
96
+ paymentInfo: { paymentMethod: ConnectPaymentMethod.DIRECT_PAY },
97
+ productSubscriptions: [{ productCode: ConnectProductCode.BUSINESS }]
98
+ }
99
+
100
+ const contactPayload = {
101
+ email: data.emailAddress,
102
+ phone: data.phone.phoneNumber,
103
+ phoneExtension: data.phone.ext
104
+ }
105
+
106
+ return { accountPayload, contactPayload }
107
+ }
@@ -0,0 +1 @@
1
+ export * from './account'
package/nuxt.config.ts CHANGED
@@ -13,7 +13,7 @@ export default defineNuxtConfig({
13
13
  extends: ['@sbc-connect/nuxt-base', '@sbc-connect/nuxt-forms'],
14
14
 
15
15
  imports: {
16
- dirs: ['interfaces', 'types', 'enums', 'stores']
16
+ dirs: ['interfaces', 'types', 'enums', 'stores', 'services']
17
17
  },
18
18
 
19
19
  modules: [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sbc-connect/nuxt-auth",
3
3
  "type": "module",
4
- "version": "0.11.1",
4
+ "version": "0.13.0",
5
5
  "repository": "github:bcgov/connect-nuxt",
6
6
  "license": "BSD-3-Clause",
7
7
  "main": "./nuxt.config.ts",
@@ -22,8 +22,8 @@
22
22
  "keycloak-js": "26.2.4",
23
23
  "pinia": "3.0.4",
24
24
  "pinia-plugin-persistedstate": "4.7.1",
25
- "@sbc-connect/nuxt-base": "0.10.0",
26
- "@sbc-connect/nuxt-forms": "0.7.5"
25
+ "@sbc-connect/nuxt-forms": "0.7.5",
26
+ "@sbc-connect/nuxt-base": "0.10.0"
27
27
  },
28
28
  "scripts": {
29
29
  "preinstall": "npx only-allow pnpm",
@@ -1,156 +0,0 @@
1
- export const useAuthApi = () => {
2
- const { $authApi } = useNuxtApp()
3
- const queryCache = useQueryCache()
4
-
5
- async function getAuthUserProfile() {
6
- const query = defineQuery({
7
- key: ['auth-user-profile'],
8
- query: () => $authApi<ConnectAuthProfile>('/users/@me', {
9
- parseResponse: JSON.parse
10
- }),
11
- staleTime: 300000
12
- })
13
- return query()
14
- }
15
-
16
- /**
17
- * Validates whether an account name is available by sending a request to the AUTH service.
18
- * @param {string} accountName - The account name to validate for uniqueness.
19
- */
20
- function verifyAccountName(accountName: string) {
21
- const query = defineQuery({
22
- key: ['auth-account-name', accountName],
23
- query: () => $authApi.raw(`/orgs?validateName=true&name=${encodeURIComponent(accountName)}`),
24
- staleTime: 300000
25
- })
26
- return query()
27
- }
28
-
29
- /**
30
- * Creates an account by POSTing the given payload to `/orgs`.
31
- * @returns Object containing mutation state and `createAccount` function.
32
- */
33
- const useCreateAccount = defineMutation(() => {
34
- const { mutateAsync, ...mutation } = useMutation({
35
- mutation: (vars: {
36
- payload: ConnectCreateAccount
37
- successCb?: (createRes: ConnectAuthProfile) => Promise<unknown>
38
- }) => {
39
- return $authApi<ConnectAuthProfile>('/orgs', {
40
- method: 'POST',
41
- body: vars.payload
42
- })
43
- },
44
- onError: (error) => {
45
- // TODO: FUTURE - add api error message to modal content - remove console.error
46
- console.error('ERROR: ', error)
47
- useConnectAuthModals().openCreateAccountModal()
48
- },
49
- onSuccess: async (_, _vars) => {
50
- await queryCache.invalidateQueries({ key: ['auth-user-profile'], exact: true })
51
- if (_vars.successCb) {
52
- await _vars.successCb(_)
53
- }
54
- }
55
- })
56
-
57
- return {
58
- ...mutation,
59
- createAccount: mutateAsync
60
- }
61
- })
62
-
63
- /**
64
- * Updates a users contact by PUTing the given payload to `/users/contacts`.
65
- * @returns Object containing mutation state and `updateUserContact` function.
66
- */
67
- const useUpdateOrCreateUserContact = defineMutation(() => {
68
- const { mutateAsync, ...mutation } = useMutation({
69
- mutation: (vars: {
70
- email: string
71
- phone: string
72
- phoneExtension: string | undefined
73
- method?: 'POST' | 'PUT'
74
- successCb?: () => Promise<unknown>
75
- errorCb?: (error: unknown) => Promise<unknown>
76
- }) => {
77
- return $authApi<ConnectAuthProfile>('/users/contacts', {
78
- method: vars.method || 'PUT',
79
- body: {
80
- email: vars.email,
81
- phone: vars.phone,
82
- phoneExtension: vars.phoneExtension
83
- }
84
- })
85
- },
86
- onError: async (error, _vars) => {
87
- // TODO: FUTURE - add api error message to modal content - remove console.error
88
- console.error('ERROR: ', error)
89
- await useConnectAuthModals().openUpdateUserContactModal()
90
-
91
- if (_vars.errorCb) {
92
- await queryCache.invalidateQueries({ key: ['auth-user-profile'], exact: true })
93
- await _vars.errorCb(error)
94
- }
95
- },
96
- onSuccess: async (_, _vars) => {
97
- if (_vars.successCb) {
98
- await _vars.successCb()
99
- }
100
- }
101
- })
102
-
103
- return {
104
- ...mutation,
105
- updateOrCreateUserContact: mutateAsync
106
- }
107
- })
108
-
109
- async function getTermsOfUse() {
110
- const query = defineQuery({
111
- key: ['auth-terms-of-use'],
112
- query: () => $authApi<ConnectTermsOfUse>('/documents/termsofuse'),
113
- staleTime: 300000
114
- })
115
- return query()
116
- }
117
-
118
- const usePatchTermsOfUse = defineMutation(() => {
119
- const { mutateAsync, ...mutation } = useMutation({
120
- mutation: (vars: { accepted: boolean, version: string, successCb?: () => Promise<unknown> }) => {
121
- return $authApi<ConnectAuthProfile>('/users/@me', {
122
- method: 'PATCH',
123
- body: {
124
- istermsaccepted: vars.accepted,
125
- termsversion: vars.version
126
- }
127
- })
128
- },
129
- onError: (error) => {
130
- // TODO: FUTURE - add api error message to modal content - remove console.error
131
- console.error('ERROR: ', error)
132
- useConnectAuthModals().openPatchTosErrorModal()
133
- },
134
- onSuccess: async (_, _vars) => {
135
- await queryCache.invalidateQueries({ key: ['auth-user-profile'], exact: true })
136
- if (_vars.successCb) {
137
- await _vars.successCb()
138
- }
139
- }
140
- })
141
-
142
- return {
143
- ...mutation,
144
- patchTermsOfUse: mutateAsync
145
- }
146
- })
147
-
148
- return {
149
- getAuthUserProfile,
150
- getTermsOfUse,
151
- useCreateAccount,
152
- usePatchTermsOfUse,
153
- useUpdateOrCreateUserContact,
154
- verifyAccountName
155
- }
156
- }
@@ -1,13 +0,0 @@
1
- export default defineNuxtRouteMiddleware(async (to) => {
2
- if (import.meta.client) { // only run on client
3
- const { isAuthenticated } = useConnectAuth()
4
- if (isAuthenticated.value) {
5
- const accountStore = useConnectAccountStore()
6
- await accountStore.initAccountStore()
7
-
8
- if (to.query.accountid) {
9
- await accountStore.switchCurrentAccount(parseInt(to.query.accountid as string))
10
- }
11
- }
12
- }
13
- })