@sbc-connect/nuxt-auth 0.4.1 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @sbc-connect/nuxt-auth
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#127](https://github.com/bcgov/connect-nuxt/pull/127) [`63409ea`](https://github.com/bcgov/connect-nuxt/commit/63409eab3ef9072fe8e05662b44f61bb8be6cff4) Thanks [@cameron-eyds](https://github.com/cameron-eyds)! - Improved name lookup validations
8
+
9
+ - [#125](https://github.com/bcgov/connect-nuxt/pull/125) [`fda1d48`](https://github.com/bcgov/connect-nuxt/commit/fda1d48ed55838aeece70aa04a82440153a3db24) Thanks [@cameron-eyds](https://github.com/cameron-eyds)! - Includes contact POST and minor redirect refactors
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [[`8cc8cc0`](https://github.com/bcgov/connect-nuxt/commit/8cc8cc09aad741dc841a864998129c6ac5a6af2f)]:
14
+ - @sbc-connect/nuxt-base@0.6.1
15
+ - @sbc-connect/nuxt-forms@0.4.1
16
+
17
+ ## 0.5.0
18
+
19
+ ### Minor Changes
20
+
21
+ - [#123](https://github.com/bcgov/connect-nuxt/pull/123) [`3c33132`](https://github.com/bcgov/connect-nuxt/commit/3c331327a27cfbd0613c268abf97842288d10f8c) Thanks [@cameron-eyds](https://github.com/cameron-eyds)! - Auth Redirect Enhancements
22
+
3
23
  ## 0.4.1
4
24
 
5
25
  ### Patch Changes
@@ -24,9 +24,16 @@ const formErrors = computed<{
24
24
  }
25
25
  })
26
26
 
27
- async function validate() {
28
- return formRef.value?.validate({ silent: true })
27
+ async function validate(fieldName?: keyof AccountProfileSchema) {
28
+ return formRef.value?.validate({ name: fieldName, silent: true })
29
29
  }
30
+
31
+ watch(() => statusCode.value, async () => {
32
+ if (statusCode.value !== undefined) {
33
+ await nextTick()
34
+ await validate('accountName')
35
+ }
36
+ })
30
37
  </script>
31
38
 
32
39
  <template>
@@ -1,5 +1,3 @@
1
- import type { ConnectCreateAccount } from '#auth/app/interfaces/connect-account'
2
-
3
1
  export const useAuthApi = () => {
4
2
  const { $authApi } = useNuxtApp()
5
3
  const queryCache = useQueryCache()
@@ -34,7 +32,10 @@ export const useAuthApi = () => {
34
32
  */
35
33
  const useCreateAccount = defineMutation(() => {
36
34
  const { mutateAsync, ...mutation } = useMutation({
37
- mutation: (vars: { payload: ConnectCreateAccount, successCb?: () => Promise<unknown> }) => {
35
+ mutation: (vars: {
36
+ payload: ConnectCreateAccount
37
+ successCb?: (createRes: ConnectAuthProfile) => Promise<unknown>
38
+ }) => {
38
39
  return $authApi<ConnectAuthProfile>('/orgs', {
39
40
  method: 'POST',
40
41
  body: vars.payload
@@ -48,7 +49,7 @@ export const useAuthApi = () => {
48
49
  onSuccess: async (_, _vars) => {
49
50
  await queryCache.invalidateQueries({ key: ['auth-user-profile'], exact: true })
50
51
  if (_vars.successCb) {
51
- await _vars.successCb()
52
+ await _vars.successCb(_)
52
53
  }
53
54
  }
54
55
  })
@@ -63,17 +64,18 @@ export const useAuthApi = () => {
63
64
  * Updates a users contact by PUTing the given payload to `/users/contacts`.
64
65
  * @returns Object containing mutation state and `updateUserContact` function.
65
66
  */
66
- const useUpdateUserContact = defineMutation(() => {
67
+ const useUpdateOrCreateUserContact = defineMutation(() => {
67
68
  const { mutateAsync, ...mutation } = useMutation({
68
69
  mutation: (vars: {
69
70
  email: string
70
71
  phone: string
71
72
  phoneExtension: string | undefined
73
+ method?: 'POST' | 'PUT'
72
74
  successCb?: () => Promise<unknown>
73
75
  errorCb?: (error: unknown) => Promise<unknown>
74
76
  }) => {
75
77
  return $authApi<ConnectAuthProfile>('/users/contacts', {
76
- method: 'PUT',
78
+ method: vars.method || 'PUT',
77
79
  body: {
78
80
  email: vars.email,
79
81
  phone: vars.phone,
@@ -100,7 +102,7 @@ export const useAuthApi = () => {
100
102
 
101
103
  return {
102
104
  ...mutation,
103
- updateUserContact: mutateAsync
105
+ updateOrCreateUserContact: mutateAsync
104
106
  }
105
107
  })
106
108
 
@@ -148,7 +150,7 @@ export const useAuthApi = () => {
148
150
  getTermsOfUse,
149
151
  useCreateAccount,
150
152
  usePatchTermsOfUse,
151
- useUpdateUserContact,
153
+ useUpdateOrCreateUserContact,
152
154
  verifyAccountName
153
155
  }
154
156
  }
@@ -1,32 +1,48 @@
1
1
  import type { RouteLocationNormalizedGeneric } from '#vue-router'
2
2
 
3
3
  export const useConnectAccountFlowRedirect = () => {
4
- function finalRedirect(route: RouteLocationNormalizedGeneric) {
4
+ function finalRedirect(route: RouteLocationNormalizedGeneric, manageAccount = false) {
5
+ const { authUser } = useConnectAuth()
6
+ const { currentAccount, userAccounts } = useConnectAccountStore()
5
7
  const localePath = useLocalePath()
6
8
  const ac = useAppConfig().connect.login
7
9
  const externalRedirectUrl = route.query.return as string | undefined
8
10
  const internalRedirectUrl = ac.redirect
9
-
10
11
  const query = { ...route.query }
11
12
 
12
- if (query.return) {
13
- delete query.return
13
+ const bypassAccounts = userAccounts.length === 1 && !query.populate
14
+ const isNonStaffAccount = ![AccountType.STAFF, AccountType.SBC_STAFF].includes(currentAccount.accountType)
15
+ const createOrSelectAccount = manageAccount && isNonStaffAccount && !bypassAccounts
16
+
17
+ if (createOrSelectAccount) {
18
+ const isBcscUserWithNoAccounts = (!userAccounts.length || userAccounts.length === 0)
19
+ && authUser.value.loginSource === ConnectLoginSource.BCSC
20
+ const redirectPath = isBcscUserWithNoAccounts ? '/auth/account/create' : '/auth/account/select'
21
+
22
+ return navigateTo({ path: localePath(redirectPath), query })
14
23
  }
15
24
 
16
25
  if (externalRedirectUrl) {
26
+ const cleanQuery = { ...query }
27
+ delete cleanQuery.return
28
+
17
29
  return navigateTo(
18
30
  {
19
- path: appendUrlParam(externalRedirectUrl, 'accountid', useConnectAccountStore().currentAccount.id),
20
- query
31
+ path: appendUrlParam(
32
+ externalRedirectUrl,
33
+ 'accountid',
34
+ currentAccount.id
35
+ ),
36
+ query: cleanQuery
21
37
  },
22
38
  { external: true }
23
39
  )
24
- } else {
25
- return navigateTo({
26
- path: localePath(internalRedirectUrl),
27
- query
28
- })
29
40
  }
41
+
42
+ return navigateTo({
43
+ path: localePath(internalRedirectUrl),
44
+ query
45
+ })
30
46
  }
31
47
 
32
48
  return {
@@ -33,7 +33,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
33
33
  if (idpEnforcement && authUser.value?.loginSource) {
34
34
  // User's IDP is not allowed, log them out and redirect to login page
35
35
  if (!allowedIdps?.includes(authUser.value?.loginSource.toLowerCase() as unknown as ConnectIdpHint)) {
36
- showInvalidIdpModal()
36
+ await showInvalidIdpModal()
37
37
  }
38
38
  }
39
39
  })
@@ -5,27 +5,33 @@ export default defineNuxtRouteMiddleware(async (to) => {
5
5
  const authApi = useAuthApi()
6
6
  const { finalRedirect } = useConnectAccountFlowRedirect()
7
7
 
8
- if (!isAuthenticated.value && !rtc.playwright) {
9
- return navigateTo({
10
- path: localePath('/auth/login'),
11
- query: {
12
- // include preset when present
13
- ...(to.query.preset ? { preset: String(to.query.preset) } : {}),
14
- return: `${rtc.baseUrl}${to.fullPath.slice(1)}`
8
+ const isLoginPage = to.meta.connectLogin === true
9
+ const isTosPage = to.meta.connectTosPage === true
10
+
11
+ if (!isAuthenticated.value && !isLoginPage && !rtc.playwright) {
12
+ const defaultReturn = `${rtc.baseUrl}${to.fullPath.slice(1)}`
13
+ const returnUrl = (to.query.return && String(to.query.return)) || defaultReturn
14
+
15
+ return navigateTo(
16
+ {
17
+ path: localePath('/auth/login'),
18
+ query: {
19
+ ...to.query,
20
+ return: returnUrl
21
+ }
15
22
  }
16
- })
23
+ )
17
24
  }
18
25
 
19
26
  if (isAuthenticated.value) {
20
27
  const { data, refresh } = await authApi.getAuthUserProfile()
21
28
  await refresh()
22
29
  const hasAccepted = data.value?.userTerms.isTermsOfUseAccepted
23
- const isTosPage = to.meta.connectTosPage === true
24
30
  if (!hasAccepted && !isTosPage) {
25
31
  const query = { ...to.query }
26
32
  return navigateTo({ path: localePath('/auth/terms-of-use'), query })
27
- } else if (hasAccepted && isTosPage) {
28
- return finalRedirect(to)
33
+ } else if (hasAccepted && (isTosPage || isLoginPage)) {
34
+ return finalRedirect(to, true)
29
35
  }
30
36
  }
31
37
 
@@ -12,7 +12,8 @@ useHead({
12
12
  definePageMeta({
13
13
  layout: 'connect-auth',
14
14
  hideBreadcrumbs: true,
15
- middleware: 'connect-login-page'
15
+ middleware: 'connect-auth',
16
+ connectLogin: true
16
17
  })
17
18
 
18
19
  const isSessionExpired = sessionStorage.getItem(ConnectAuthStorageKey.CONNECT_SESSION_EXPIRED)
@@ -70,7 +70,7 @@ const disableButtons = computed<boolean>(() => {
70
70
  @submit="patchTermsOfUse({
71
71
  accepted: true,
72
72
  version: data!.versionId,
73
- successCb: async () => await finalRedirect(useRoute()),
73
+ successCb: async () => await finalRedirect(useRoute(), true),
74
74
  })"
75
75
  />
76
76
  </UContainer>
@@ -1,12 +1,11 @@
1
1
  import { getAccountCreateSchema } from '#auth/app/utils/schemas/account'
2
2
  import type { AccountProfileSchema } from '#auth/app/utils/schemas/account'
3
- import type { ConnectCreateAccount } from '#auth/app/interfaces/connect-account'
4
3
 
5
4
  export const useConnectAccountStore = defineStore('connect-auth-account-store', () => {
6
5
  const { $authApi } = useNuxtApp()
7
6
  const rtc = useRuntimeConfig().public
8
7
  const { authUser } = useConnectAuth()
9
- const { useCreateAccount, useUpdateUserContact, getAuthUserProfile } = useAuthApi()
8
+ const { useCreateAccount, useUpdateOrCreateUserContact, getAuthUserProfile } = useAuthApi()
10
9
  const { finalRedirect } = useConnectAccountFlowRedirect()
11
10
 
12
11
  // selected user account
@@ -22,7 +21,7 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
22
21
  // Create account
23
22
  const isLoading = ref<boolean>(false)
24
23
  const { createAccount } = useCreateAccount()
25
- const { updateUserContact } = useUpdateUserContact()
24
+ const { updateOrCreateUserContact } = useUpdateOrCreateUserContact()
26
25
  const createAccountProfileSchema = getAccountCreateSchema()
27
26
  const accountFormState = reactive<AccountProfileSchema>(createAccountProfileSchema.parse({}))
28
27
  /**
@@ -88,13 +87,23 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
88
87
  await createAccount({
89
88
  payload,
90
89
  // Update User Contact Info on create account success
91
- successCb: async () => await updateUserContact({
92
- email: accountFormState.emailAddress,
93
- phone: accountFormState.phone.phoneNumber,
94
- phoneExtension: accountFormState.phone.ext,
95
- successCb: async () => await finalRedirect(useRoute()),
96
- errorCb: async () => await finalRedirect(useRoute())
97
- })
90
+ successCb: async (createResponse: ConnectAuthProfile) => {
91
+ // Refresh and switch to new account prior to redirect
92
+ if (createResponse?.id) {
93
+ await setAccountInfo()
94
+ switchCurrentAccount(createResponse.id)
95
+ }
96
+
97
+ // Update or create user contact and then redirect regardless of success or failure
98
+ await updateOrCreateUserContact({
99
+ email: accountFormState.emailAddress,
100
+ phone: accountFormState.phone.phoneNumber,
101
+ phoneExtension: accountFormState.phone.ext,
102
+ method: userAccounts.value.length === 1 ? 'POST' : 'PUT', // if only 1 account exists then contact is new
103
+ successCb: async () => await finalRedirect(useRoute()),
104
+ errorCb: async () => await finalRedirect(useRoute())
105
+ })
106
+ }
98
107
  })
99
108
  } catch (error) {
100
109
  // Error handled in useAuthApi
@@ -6,7 +6,7 @@ export function getAccountNameSchema(status?: number | undefined) {
6
6
 
7
7
  return z.string().min(1, t('connect.validation.requiredAccountName'))
8
8
  .superRefine((val, ctx) => {
9
- // Only run if we have a value and a statusCode
9
+ // Validate account name uniqueness based on API response status code
10
10
  if (val && status !== undefined) {
11
11
  if (status === 200) {
12
12
  ctx.addIssue({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sbc-connect/nuxt-auth",
3
3
  "type": "module",
4
- "version": "0.4.1",
4
+ "version": "0.6.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.2",
23
23
  "pinia": "3.0.4",
24
24
  "pinia-plugin-persistedstate": "4.7.1",
25
- "@sbc-connect/nuxt-base": "0.6.0",
26
- "@sbc-connect/nuxt-forms": "0.4.0"
25
+ "@sbc-connect/nuxt-base": "0.6.1",
26
+ "@sbc-connect/nuxt-forms": "0.4.1"
27
27
  },
28
28
  "scripts": {
29
29
  "preinstall": "npx only-allow pnpm",
@@ -1 +1,2 @@
1
+ export * from './profile'
1
2
  export * from './settings'
@@ -0,0 +1,9 @@
1
+ import fs from 'fs'
2
+ import { createResolver } from 'nuxt/kit'
3
+
4
+ const { resolve } = createResolver(import.meta.url)
5
+
6
+ export const getUserProfileMock = () => {
7
+ const json: ConnectAuthProfile = JSON.parse(fs.readFileSync(resolve('./json/PUBLIC_USER.json'), 'utf8'))
8
+ return json
9
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "contacts": [
3
+ {
4
+ "email": "v9x3z!q__mock@fuzzmail.zzz",
5
+ "phone": "(902) 555-xyzz",
6
+ "phoneExtension": "7q1"
7
+ }
8
+ ],
9
+ "created": "2024-03-19T04:92:17+00:00",
10
+ "firstname": "Nxlqrp3##",
11
+ "id": 88422,
12
+ "idpUserid": "ZZ91XQWPMR7T4!$CBA9900PLMNXXQTT",
13
+ "keycloakGuid": "xxxxxxxx-bb77-49c1-f01a-xxxxxxxxx",
14
+ "lastname": "T'W&&two--",
15
+ "loginSource": "BCSC",
16
+ "loginTime": "2026-01-23T28:77:59+00:00",
17
+ "modified": "2026-01-23T28:77:59+00:00",
18
+ "modifiedBy": "Nxlqrp3## T'W&&two--",
19
+ "type": "PUBLIC_USER",
20
+ "userStatus": 1,
21
+ "userTerms": {
22
+ "isTermsOfUseAccepted": true,
23
+ "termsOfUseAcceptedVersion": "999x"
24
+ },
25
+ "username": "bcsc/zz91xqwpmr7t4!$cba9900plmnxxqtt",
26
+ "verified": true,
27
+ "version": 9999
28
+ }
29
+
@@ -1,11 +1,14 @@
1
1
  import type { Page } from '@playwright/test'
2
2
  import { AccountType } from '#auth/app/enums/account-type'
3
- import { getUserSettingsMock } from '#auth/testMocks/auth'
3
+ import { getUserProfileMock, getUserSettingsMock } from '#auth/testMocks/auth'
4
4
 
5
5
  export const mockApiCallsForSetAccount = async (
6
6
  page: Page,
7
7
  accountType: AccountType = AccountType.PREMIUM
8
8
  ) => {
9
+ page.route('**/users/@me', async (route) => {
10
+ await route.fulfill({ json: getUserProfileMock() })
11
+ })
9
12
  page.route('**/users/**/settings', async (route) => {
10
13
  await route.fulfill({ json: getUserSettingsMock(accountType) })
11
14
  })
@@ -1,26 +0,0 @@
1
- export default defineNuxtRouteMiddleware((to) => {
2
- const { isAuthenticated, authUser } = useConnectAuth()
3
- const accountStore = useConnectAccountStore()
4
- const localePath = useLocalePath()
5
- const { finalRedirect } = useConnectAccountFlowRedirect()
6
-
7
- const numAccounts = accountStore.userAccounts.length
8
-
9
- if (isAuthenticated.value) {
10
- if (numAccounts === 1) {
11
- return finalRedirect(to)
12
- }
13
-
14
- if (numAccounts === 0 && authUser.value.loginSource === ConnectLoginSource.BCSC) {
15
- return navigateTo({
16
- path: localePath('/auth/account/create'),
17
- query: to.query
18
- })
19
- }
20
-
21
- return navigateTo({
22
- path: localePath('/auth/account/select'),
23
- query: to.query
24
- })
25
- }
26
- })