@sbc-connect/nuxt-auth 0.1.4 → 0.1.6

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 (40) hide show
  1. package/.env.example +25 -1
  2. package/CHANGELOG.md +34 -0
  3. package/app/app.config.ts +14 -0
  4. package/app/components/Connect/Header/AccountLabel.vue +35 -0
  5. package/app/components/Connect/Header/AccountOptionsMenu.vue +52 -0
  6. package/app/components/Connect/Header/Auth.vue +14 -0
  7. package/app/components/Connect/Header/AuthenticatedOptions.vue +13 -0
  8. package/app/components/Connect/Header/CreateAccountButton.vue +13 -0
  9. package/app/components/Connect/Header/LoginMenu.vue +27 -0
  10. package/app/components/Connect/Header/Notifications.vue +30 -0
  11. package/app/components/Connect/Header/UnauthenticatedOptions.vue +15 -0
  12. package/app/components/HelloWorld/Auth.vue +2 -2
  13. package/app/composables/useConnectAuth.ts +111 -0
  14. package/app/composables/useConnectHeaderOptions.ts +233 -0
  15. package/app/enums/account-status.ts +9 -0
  16. package/app/enums/account-type.ts +6 -0
  17. package/app/enums/connect-auth-storage-keys.ts +5 -0
  18. package/app/enums/connect-error-category.ts +6 -0
  19. package/app/enums/connect-idp-hint.ts +6 -0
  20. package/app/enums/connect-login-source.ts +6 -0
  21. package/app/enums/user-settings-type.ts +5 -0
  22. package/app/interfaces/connect-account.ts +10 -0
  23. package/app/interfaces/connect-api-error.ts +6 -0
  24. package/app/interfaces/connect-auth-user.ts +10 -0
  25. package/app/interfaces/connect-user-settings.ts +10 -0
  26. package/app/layouts/ConnectAuth.vue +8 -0
  27. package/app/middleware/01.setup-accounts.global.ts +13 -0
  28. package/app/middleware/02.keycloak-params.global.ts +15 -0
  29. package/app/plugins/auth-api.ts +28 -0
  30. package/app/plugins/connect-auth.client.ts +105 -0
  31. package/app/stores/connect-account.ts +236 -0
  32. package/app/types/auth-app-config.d.ts +18 -0
  33. package/i18n/locales/en-CA.ts +31 -0
  34. package/i18n/locales/fr-CA.ts +1 -0
  35. package/modules/auth-assets/index.ts +15 -0
  36. package/modules/auth-assets/runtime/assets/connect-auth-tw.css +2 -0
  37. package/nuxt.config.ts +53 -6
  38. package/package.json +11 -7
  39. package/app/assets/css/tw-auth.css +0 -2
  40. package/app.config.ts +0 -5
package/.env.example CHANGED
@@ -1 +1,25 @@
1
- NUXT_BASE_URL="http://localhost:3000/"
1
+ # Web Urls
2
+ NUXT_PUBLIC_BASE_URL="http://localhost:3000/"
3
+ NUXT_PUBLIC_REGISTRY_HOME_URL="https://dev.bcregistry.gov.bc.ca/"
4
+ NUXT_PUBLIC_AUTH_WEB_URL="https://dev.account.bcregistry.gov.bc.ca/"
5
+
6
+ # API Key
7
+ NUXT_PUBLIC_X_API_KEY=""
8
+
9
+ # API Urls
10
+ # Status API
11
+ NUXT_PUBLIC_STATUS_API_URL="https://status-api-dev.apps.gold.devops.gov.bc.ca"
12
+ NUXT_PUBLIC_STATUS_API_VERSION="/api/v1"
13
+
14
+ # Auth API
15
+ NUXT_PUBLIC_AUTH_API_URL="https://test.api.connect.gov.bc.ca/auth-dev"
16
+ NUXT_PUBLIC_AUTH_API_VERSION="/api/v1"
17
+
18
+ # LaunchDarkly
19
+ NUXT_PUBLIC_LD_CLIENT_ID=""
20
+
21
+ # Identity Provider
22
+ NUXT_PUBLIC_IDP_URL="https://dev.loginproxy.gov.bc.ca/auth"
23
+ NUXT_PUBLIC_IDP_REALM="bcregistry"
24
+ NUXT_PUBLIC_IDP_CLIENTID="connect-web"
25
+ NUXT_PUBLIC_SITEMINDER_LOGOUT_URL="https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi"
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @sbc-connect/nuxt-auth
2
2
 
3
+ ## 0.1.6
4
+
5
+ ### Patch Changes
6
+
7
+ - [#21](https://github.com/bcgov/connect-nuxt/pull/21) [`db2159e`](https://github.com/bcgov/connect-nuxt/commit/db2159ebc4b310c1c24986ca8ef85b5435fd50c8) Thanks [@deetz99](https://github.com/deetz99)! - Init auth plugin
8
+
9
+ - [#26](https://github.com/bcgov/connect-nuxt/pull/26) [`65ae301`](https://github.com/bcgov/connect-nuxt/commit/65ae301972b39cfed8550e49c1209133674528a4) Thanks [@deetz99](https://github.com/deetz99)! - Update CSS variables to match Nuxt UI naming convention. Issue: bcgov/entity#29335
10
+
11
+ - [#23](https://github.com/bcgov/connect-nuxt/pull/23) [`b257d0c`](https://github.com/bcgov/connect-nuxt/commit/b257d0c874138e56ae0b5d79ec6e5a7b30acec8b) Thanks [@deetz99](https://github.com/deetz99)! - - begin initial account store
12
+
13
+ - auth composable
14
+ - clear keycloak params in url
15
+ - init account in middleware
16
+ - several enums/types to support accounts
17
+ - update nuxt config with auto imports and pinia plugin module
18
+
19
+ issue: bcgov/entity#29335
20
+
21
+ - [#25](https://github.com/bcgov/connect-nuxt/pull/25) [`aba11d1`](https://github.com/bcgov/connect-nuxt/commit/aba11d1303ab1b19b3a51c27959766c4ee0cd5d8) Thanks [@deetz99](https://github.com/deetz99)! - - create auth header components
22
+
23
+ - connect-auth layout
24
+
25
+ issue: bcgov/entity#29335
26
+
27
+ - Updated dependencies [[`65ae301`](https://github.com/bcgov/connect-nuxt/commit/65ae301972b39cfed8550e49c1209133674528a4), [`09fa4f1`](https://github.com/bcgov/connect-nuxt/commit/09fa4f1b4b2c65d189a6477c9c5f2d44607b543d), [`aba11d1`](https://github.com/bcgov/connect-nuxt/commit/aba11d1303ab1b19b3a51c27959766c4ee0cd5d8)]:
28
+ - @sbc-connect/nuxt-base@0.1.6
29
+
30
+ ## 0.1.5
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [[`4231254`](https://github.com/bcgov/connect-nuxt/commit/42312540f5eec65f5d3979d5492bdfaa9bb0b079)]:
35
+ - @sbc-connect/nuxt-base@0.1.5
36
+
3
37
  ## 0.1.4
4
38
 
5
39
  ### Patch Changes
@@ -0,0 +1,14 @@
1
+ export default defineAppConfig({
2
+ connect: {
3
+ login: {
4
+ redirectPath: '',
5
+ idps: ['bcsc', 'bceid', 'idir']
6
+ },
7
+ header: {
8
+ loginMenu: true,
9
+ createAccount: true,
10
+ notifications: true,
11
+ accountOptionsMenu: true
12
+ }
13
+ }
14
+ })
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ defineProps({
3
+ accountName: { default: 'N/A', type: String },
4
+ username: { default: 'N/A', type: String },
5
+ theme: { default: 'header', type: String } // header or dropdown
6
+ })
7
+ </script>
8
+
9
+ <template>
10
+ <div class="flex items-center gap-1">
11
+ <UAvatar
12
+ :text="username[0] ? username[0].toLocaleUpperCase($i18n.locale) : 'U'"
13
+ size="md"
14
+ class="text-inverted self-start sm:self-center"
15
+ :ui="{ root: 'bg-blue-300 rounded-none text-lg', fallback: 'text-inverted font-bold' }"
16
+ />
17
+ <div class="mx-2 flex flex-col text-left font-normal tracking-wide">
18
+ <span
19
+ class="text-sm"
20
+ :class="{
21
+ 'line-clamp-1 overflow-hidden text-ellipsis text-inverted max-w-64': theme === 'header',
22
+ 'text-neutral-highlighted max-w-80': theme === 'dropdown',
23
+ }"
24
+ >
25
+ {{ username }}
26
+ </span>
27
+ <span
28
+ class="line-clamp-1 overflow-hidden text-ellipsis text-xs opacity-75"
29
+ :class="{ 'text-line': theme === 'header', 'text-neutral': theme === 'dropdown' }"
30
+ >
31
+ {{ accountName }}
32
+ </span>
33
+ </div>
34
+ </div>
35
+ </template>
@@ -0,0 +1,52 @@
1
+ <script setup lang="ts">
2
+ const { loggedInUserOptions } = useConnectHeaderOptions()
3
+ const { authUser } = useConnectAuth()
4
+ const accountStore = useConnectAccountStore()
5
+ const isLargeScreen = useMediaQuery('(min-width: 1024px)')
6
+ </script>
7
+
8
+ <template>
9
+ <UDropdownMenu
10
+ id="account-options-menu"
11
+ :items="loggedInUserOptions"
12
+ :ui="{
13
+ content: 'max-w-[90dvw]',
14
+ }"
15
+ >
16
+ <UButton
17
+ id="account-options-button"
18
+ color="secondary"
19
+ variant="ghost"
20
+ :aria-label="$t('connect.label.accountOptionsMenu')"
21
+ :icon="isLargeScreen ? 'i-mdi-caret-down' : ''"
22
+ trailing
23
+ class="px-2 py-1"
24
+ >
25
+ <ConnectHeaderAccountLabel
26
+ v-if="isLargeScreen"
27
+ :username="parseSpecialCharacters(authUser.fullName, 'USER')"
28
+ :account-name="accountStore.currentAccount.label
29
+ ? parseSpecialCharacters(accountStore.currentAccount.label, 'ACCOUNT')
30
+ : ''
31
+ "
32
+ />
33
+
34
+ <UAvatar
35
+ v-if="!isLargeScreen"
36
+ :text="parseSpecialCharacters(authUser.fullName, 'U')[0]!.toUpperCase()"
37
+ size="md"
38
+ :ui="{ root: 'bg-blue-300 rounded-none text-lg', fallback: 'text-inverted font-bold' }"
39
+ />
40
+ </UButton>
41
+ <template #account>
42
+ <ConnectHeaderAccountLabel
43
+ :username="parseSpecialCharacters(authUser.fullName, 'USER')"
44
+ :account-name="accountStore.currentAccount.label
45
+ ? parseSpecialCharacters(accountStore.currentAccount.label, 'ACCOUNT')
46
+ : ''
47
+ "
48
+ theme="dropdown"
49
+ />
50
+ </template>
51
+ </UDropdownMenu>
52
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ const ac = useAppConfig().connect.header
3
+ const { isAuthenticated } = useConnectAuth()
4
+ </script>
5
+
6
+ <template>
7
+ <ConnectHeader>
8
+ <div class="flex gap-4">
9
+ <ConnectHeaderAuthenticatedOptions v-if="isAuthenticated" />
10
+ <ConnectHeaderUnauthenticatedOptions v-else />
11
+ <ConnectLocaleSelect v-if="ac.localeSelect" />
12
+ </div>
13
+ </ConnectHeader>
14
+ </template>
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ const ac = useAppConfig().connect.header
3
+ </script>
4
+
5
+ <template>
6
+ <div
7
+ id="connect-header-authenticated-options"
8
+ class="flex gap-4"
9
+ >
10
+ <ConnectHeaderNotifications v-if="ac.notifications" />
11
+ <ConnectHeaderAccountOptionsMenu v-if="ac.accountOptionsMenu" />
12
+ </div>
13
+ </template>
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ const { createAccountUrl } = useConnectHeaderOptions()
3
+ </script>
4
+
5
+ <template>
6
+ <UButton
7
+ variant="ghost"
8
+ color="secondary"
9
+ class="px-2 py-1 text-sm"
10
+ :label="$t('connect.label.createAccount')"
11
+ :to="createAccountUrl()"
12
+ />
13
+ </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import { useStorage } from '@vueuse/core'
3
+
4
+ const { loggedOutUserOptions, loggedOutUserOptionsMobile } = useConnectHeaderOptions()
5
+ const isLargeScreen = useMediaQuery('(min-width: 1024px)')
6
+ const whatsNew = useStorage<ConnectWhatsNewState>('connect-whats-new', { viewed: false, items: [] })
7
+ </script>
8
+
9
+ <template>
10
+ <UDropdownMenu
11
+ id="login-menu"
12
+ :items="isLargeScreen ? loggedOutUserOptions : loggedOutUserOptionsMobile"
13
+ >
14
+ <UButton
15
+ variant="ghost"
16
+ color="secondary"
17
+ :label="isLargeScreen ? $t('connect.label.login') : undefined"
18
+ :aria-label="isLargeScreen ? $t('connect.label.selectLoginMethod') : $t('connect.label.mainMenu')"
19
+ :icon="isLargeScreen ? 'i-mdi-caret-down' : 'i-mdi-menu'"
20
+ trailing
21
+ class="px-2 py-1 text-sm"
22
+ />
23
+ <template #whatsnew-trailing>
24
+ <span v-if="!whatsNew.viewed && whatsNew.items.length > 0" class="size-2 rounded-full bg-error" />
25
+ </template>
26
+ </UDropdownMenu>
27
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ const { notificationsOptions } = useConnectHeaderOptions()
3
+ const accountStore = useConnectAccountStore()
4
+ const isLargeScreen = useMediaQuery('(min-width: 1024px)')
5
+ </script>
6
+
7
+ <template>
8
+ <UDropdownMenu :items="notificationsOptions">
9
+ <UChip
10
+ color="error"
11
+ position="top-left"
12
+ inset
13
+ :show="accountStore.pendingApprovalCount > 0"
14
+ >
15
+ <UButton
16
+ variant="ghost"
17
+ color="secondary"
18
+ :label="isLargeScreen ? $t('connect.label.notifications') : undefined"
19
+ :aria-label="$t('connect.label.notificationsAria', { count: accountStore.pendingApprovalCount })"
20
+ :icon="isLargeScreen ? 'i-mdi-caret-down' : ''"
21
+ trailing
22
+ class="px-2 py-1 text-sm"
23
+ >
24
+ <template #leading>
25
+ <UIcon name="i-mdi-bell-outline" class="size-6 shrink-0" />
26
+ </template>
27
+ </UButton>
28
+ </UChip>
29
+ </UDropdownMenu>
30
+ </template>
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ const ac = useAppConfig().connect.header
3
+ const isLargeScreen = useMediaQuery('(min-width: 1024px)')
4
+ </script>
5
+
6
+ <template>
7
+ <div
8
+ id="connect-header-unauthenticated-options"
9
+ class="flex gap-4"
10
+ >
11
+ <ConnectHeaderWhatsNew v-if="ac.whatsNew && isLargeScreen" />
12
+ <ConnectHeaderLoginMenu v-if="ac.loginMenu" />
13
+ <ConnectHeaderCreateAccountButton v-if="ac.createAccount && isLargeScreen" />
14
+ </div>
15
+ </template>
@@ -5,8 +5,8 @@
5
5
  <h3 class="text-red-700">
6
6
  Auth - should be red-700
7
7
  </h3>
8
- <p class="text-brandLight">
9
- Auth - should be brandLight
8
+ <p class="text-brand-inverted text-xl lg:text-3xl">
9
+ Auth - should be brand-inverted
10
10
  </p>
11
11
  </div>
12
12
  </template>
@@ -0,0 +1,111 @@
1
+ export const useConnectAuth = () => {
2
+ const { $connectAuth } = useNuxtApp()
3
+ const rtc = useRuntimeConfig().public
4
+
5
+ /**
6
+ * Logs the user in using the idpHint 'bcsc', 'idir' or 'bceid' and an optional redirect URL.
7
+ * @param idpHint - The identity provider hint to use for login.
8
+ * @param redirect - An optional URL to redirect to after login. Defaults to location.origin + the current locale
9
+ * @returns A promise that resolves when login is complete.
10
+ */
11
+ function login(idpHint: ConnectIdpHint, redirect?: string): Promise<void> {
12
+ const loginRedirectUrl = sessionStorage.getItem(ConnectAuthStorageKeys.LOGIN_REDIRECT_URL)
13
+ const redirectUri = redirect ?? loginRedirectUrl ?? window.location.href
14
+
15
+ return $connectAuth.login(
16
+ {
17
+ idpHint,
18
+ redirectUri
19
+ }
20
+ )
21
+ }
22
+
23
+ /**
24
+ * Logs the user out with an optional redirect URL.
25
+ * @param redirect - An optional URL to redirect to after logout. Defaults to location.origin + the current locale
26
+ * @returns A promise that resolves when logout is complete.
27
+ */
28
+ function logout(redirect?: string): Promise<void> {
29
+ const siteminderUrl = rtc.siteminderLogoutUrl
30
+ const logoutRedirectUrl = sessionStorage.getItem(ConnectAuthStorageKeys.LOGOUT_REDIRECT_URL)
31
+ let redirectUri = redirect ?? logoutRedirectUrl ?? window.location.href
32
+
33
+ if (siteminderUrl) {
34
+ redirectUri = `${siteminderUrl}?returl=${redirectUri.replace(/(https?:\/\/)|(\/)+/g, '$1$2')}&retnow=1`
35
+ }
36
+
37
+ // resetPiniaStores()
38
+ return $connectAuth.logout({
39
+ redirectUri
40
+ })
41
+ }
42
+
43
+ const isAuthenticated = computed<boolean | undefined>(() => {
44
+ if (!$connectAuth) {
45
+ return false
46
+ }
47
+ return $connectAuth.authenticated
48
+ })
49
+
50
+ const authUser = computed<ConnectAuthUser>(() => {
51
+ if ($connectAuth && $connectAuth.tokenParsed) {
52
+ return {
53
+ firstName: $connectAuth.tokenParsed.firstname,
54
+ lastName: $connectAuth.tokenParsed.lastname,
55
+ fullName: $connectAuth.tokenParsed.name,
56
+ userName: $connectAuth.tokenParsed.username,
57
+ email: $connectAuth.tokenParsed.email,
58
+ keycloakGuid: $connectAuth.tokenParsed.sub || '',
59
+ loginSource: $connectAuth.tokenParsed.loginSource,
60
+ roles: $connectAuth.tokenParsed.realm_access?.roles || []
61
+ }
62
+ }
63
+ return {} as ConnectAuthUser
64
+ })
65
+
66
+ /**
67
+ * Retrieves the current session token, with an optional force refresh.
68
+ * @param forceRefresh - A boolean to force a token refresh.
69
+ * @returns The session token or undefined if the token can't be retrieved.
70
+ */
71
+ async function getToken(forceRefresh = false): Promise<string | undefined> {
72
+ const minValidity = forceRefresh ? -1 : 30
73
+ return await $connectAuth
74
+ .updateToken(minValidity)
75
+ .then((_refreshed) => {
76
+ return $connectAuth.token
77
+ })
78
+ .catch((error) => {
79
+ console.error(`Failed to get session token: ${error}`)
80
+ return undefined
81
+ })
82
+ }
83
+
84
+ function setLoginRedirectUrl(url: string) {
85
+ sessionStorage.setItem(ConnectAuthStorageKeys.LOGIN_REDIRECT_URL, url)
86
+ }
87
+
88
+ function setLogoutRedirectUrl(url: string) {
89
+ sessionStorage.setItem(ConnectAuthStorageKeys.LOGOUT_REDIRECT_URL, url)
90
+ }
91
+
92
+ function clearLoginRedirectUrl() {
93
+ sessionStorage.removeItem(ConnectAuthStorageKeys.LOGIN_REDIRECT_URL)
94
+ }
95
+
96
+ function clearLogoutRedirectUrl() {
97
+ sessionStorage.removeItem(ConnectAuthStorageKeys.LOGOUT_REDIRECT_URL)
98
+ }
99
+
100
+ return {
101
+ login,
102
+ logout,
103
+ getToken,
104
+ clearLoginRedirectUrl,
105
+ clearLogoutRedirectUrl,
106
+ setLoginRedirectUrl,
107
+ setLogoutRedirectUrl,
108
+ isAuthenticated,
109
+ authUser
110
+ }
111
+ }
@@ -0,0 +1,233 @@
1
+ import type { DropdownMenuItem } from '@nuxt/ui'
2
+ import { useStorage } from '@vueuse/core'
3
+ import { ConnectSlideoverWhatsNew } from '#components'
4
+
5
+ // handle navigation items and functionality
6
+ export function useConnectHeaderOptions() {
7
+ const rtc = useRuntimeConfig().public
8
+ const authWebUrl = rtc.authWebUrl
9
+ const appBaseUrl = rtc.baseUrl
10
+ const ac = useAppConfig().connect
11
+ const route = useRoute()
12
+ const overlay = useOverlay()
13
+ const { t, locale: { value: locale } } = useNuxtApp().$i18n
14
+ const { login, logout, isAuthenticated, authUser } = useConnectAuth()
15
+ const accountStore = useConnectAccountStore()
16
+
17
+ const whatsNew = useStorage<ConnectWhatsNewState>('connect-whats-new', { viewed: false, items: [] })
18
+ const slideover = overlay.create(ConnectSlideoverWhatsNew)
19
+
20
+ /** return the correct account creation link based on auth state */
21
+ function createAccountUrl(): string {
22
+ if (isAuthenticated.value) {
23
+ return authWebUrl + 'setup-account'
24
+ } else {
25
+ return authWebUrl + 'choose-authentication-method'
26
+ }
27
+ }
28
+
29
+ const basicAccountOptions = computed<DropdownMenuItem[]>(() => {
30
+ const options: DropdownMenuItem[] = [{ slot: 'account', type: 'label' }]
31
+ if ([ConnectLoginSource.BCEID, ConnectLoginSource.BCSC].includes(authUser?.value.loginSource)) {
32
+ options.push({
33
+ label: t('connect.label.editProfile'),
34
+ icon: 'i-mdi-account-outline',
35
+ to: authWebUrl + 'userprofile'
36
+ })
37
+ }
38
+ options.push({
39
+ label: t('connect.label.logout'),
40
+ icon: 'i-mdi-logout-variant',
41
+ onSelect: () => logout()
42
+ })
43
+ return options
44
+ })
45
+
46
+ const accountSettingsOptions = computed<DropdownMenuItem[]>(() => {
47
+ const accountId = accountStore.currentAccount.id
48
+ const options: DropdownMenuItem[] = [
49
+ {
50
+ label: t('connect.label.accountSettings'),
51
+ type: 'label'
52
+ },
53
+ {
54
+ label: t('connect.label.accountInfo'),
55
+ icon: 'i-mdi-information-outline',
56
+ to: authWebUrl + `account/${accountId}/settings/account-info`
57
+ },
58
+ {
59
+ label: t('connect.label.teamMembers'),
60
+ icon: 'i-mdi-account-group-outline',
61
+ to: authWebUrl + `account/${accountId}/settings/team-members`
62
+ }
63
+ ]
64
+ // TODO: remove staff checks?
65
+ if (
66
+ [AccountType.PREMIUM, AccountType.SBC_STAFF, AccountType.STAFF].includes(accountStore.currentAccount.accountType)
67
+ ) {
68
+ options.push({
69
+ label: t('connect.label.transactions'),
70
+ icon: 'i-mdi-file-document-outline',
71
+ to: authWebUrl + `account/${accountId}/settings/transactions`
72
+ })
73
+ }
74
+ return options
75
+ })
76
+
77
+ const switchAccountOptions = computed<DropdownMenuItem[]>(() => {
78
+ const options: DropdownMenuItem[] = []
79
+
80
+ if (accountStore.userAccounts.length > 1) {
81
+ options.push({ label: t('connect.label.switchAccount'), type: 'label' })
82
+
83
+ accountStore.userAccounts.forEach((account) => {
84
+ const isActive = accountStore.currentAccount.id === account.id
85
+ options.push({
86
+ label: account.label,
87
+ onSelect: () => {
88
+ if (!isActive && account.id) {
89
+ if (route.meta.onAccountChange) {
90
+ // TODO: add route meta option
91
+ const allowAccountChange = true
92
+ // const allowAccountChange = route.meta.onAccountChange(accountStore.currentAccount, account)
93
+ if (allowAccountChange) {
94
+ accountStore.switchCurrentAccount(account.id)
95
+ }
96
+ } else {
97
+ accountStore.switchCurrentAccount(account.id)
98
+ }
99
+ }
100
+ },
101
+ slot: 'account-item',
102
+ icon: isActive ? 'i-mdi-check' : '',
103
+ class: isActive ? 'bg-shade text-primary' : '',
104
+ ui: {
105
+ itemLabel: isActive ? '' : 'pl-6',
106
+ itemLeadingIcon: isActive ? 'size-5 text-primary shrink-0' : ''
107
+ }
108
+ })
109
+ })
110
+ }
111
+
112
+ return options
113
+ })
114
+
115
+ const createAccountOptions = computed<DropdownMenuItem[]>(() => {
116
+ if ([ConnectLoginSource.BCROS, ConnectLoginSource.IDIR].includes(authUser?.value.loginSource)) {
117
+ return []
118
+ }
119
+ return [{ label: t('connect.label.createAccount'), icon: 'i-mdi-plus', to: createAccountUrl() }]
120
+ })
121
+
122
+ const loggedInUserOptions = computed<DropdownMenuItem[][]>(() => {
123
+ const options = [
124
+ basicAccountOptions.value,
125
+ accountSettingsOptions.value
126
+ ]
127
+
128
+ if (switchAccountOptions.value.length > 0) {
129
+ options.push(switchAccountOptions.value)
130
+ }
131
+
132
+ if (createAccountOptions.value.length > 0) {
133
+ options.push(createAccountOptions.value)
134
+ }
135
+
136
+ return options
137
+ })
138
+
139
+ const loginRedirectUrl = ac.login.redirectPath
140
+ ? appBaseUrl + locale + ac.login.redirectPath
141
+ : undefined
142
+
143
+ const loginOptionsMap: Record<'bcsc' | 'bceid' | 'idir',
144
+ { label: string, icon: string, onSelect: () => Promise<void> }
145
+ > = {
146
+ bcsc: {
147
+ label: t('connect.label.bcsc'),
148
+ icon: 'i-mdi-account-card-details-outline',
149
+ onSelect: () => login(ConnectIdpHint.BCSC, loginRedirectUrl)
150
+ },
151
+ bceid: {
152
+ label: t('connect.label.bceid'),
153
+ icon: 'i-mdi-two-factor-authentication',
154
+ onSelect: () => login(ConnectIdpHint.BCEID, loginRedirectUrl)
155
+ },
156
+ idir: {
157
+ label: t('connect.label.idir'),
158
+ icon: 'i-mdi-account-group-outline',
159
+ onSelect: () => login(ConnectIdpHint.IDIR, loginRedirectUrl)
160
+ }
161
+ }
162
+
163
+ const loggedOutUserOptions = computed<DropdownMenuItem[][]>(() => {
164
+ const options: DropdownMenuItem[][] = [[{ label: t('connect.label.selectLoginMethod'), type: 'label' }]]
165
+
166
+ const idps = ac.login.idps.map(key => loginOptionsMap[key as keyof typeof loginOptionsMap])
167
+
168
+ options.push(idps)
169
+
170
+ return options
171
+ })
172
+
173
+ const loggedOutUserOptionsMobile = computed<DropdownMenuItem[][]>(() => {
174
+ const config = ac.header
175
+ const options: DropdownMenuItem[][] = []
176
+
177
+ if (config.loginMenu) {
178
+ options.push(...loggedOutUserOptions.value)
179
+ }
180
+ if (config.whatsNew) {
181
+ options.push([
182
+ {
183
+ 'label': t('connect.label.whatsNew'),
184
+ 'aria-label': t('connect.label.whatsNewAria', {
185
+ count: whatsNew.value.viewed ? 0 : whatsNew.value.items.length }
186
+ ),
187
+ 'slot': 'whatsnew' as const,
188
+ 'icon': 'i-mdi-new-box',
189
+ 'onSelect': () => {
190
+ slideover.open({
191
+ items: whatsNew.value.items
192
+ })
193
+ whatsNew.value.viewed = true
194
+ }
195
+ }
196
+ ])
197
+ }
198
+ if (config.createAccount) {
199
+ options.push([{ label: t('connect.label.createAccount'), icon: 'i-mdi-plus', to: createAccountUrl() }])
200
+ }
201
+
202
+ return options
203
+ })
204
+
205
+ const notificationsOptions = computed<DropdownMenuItem[][]>(() => {
206
+ const count = accountStore.pendingApprovalCount
207
+ const options = []
208
+ if (count > 0) {
209
+ options.push([{
210
+ to: authWebUrl + `account/${accountStore.currentAccount.id}/settings/team-members`,
211
+ label: t('connect.text.notifications.teamMemberApproval', {
212
+ count: accountStore.pendingApprovalCount
213
+ }, accountStore.pendingApprovalCount
214
+ )
215
+ }])
216
+ } else {
217
+ options.push([{ label: t('connect.text.notifications.none') }])
218
+ }
219
+ return options
220
+ })
221
+
222
+ return {
223
+ basicAccountOptions,
224
+ accountSettingsOptions,
225
+ switchAccountOptions,
226
+ createAccountOptions,
227
+ loggedInUserOptions,
228
+ loggedOutUserOptions,
229
+ loggedOutUserOptionsMobile,
230
+ notificationsOptions,
231
+ createAccountUrl
232
+ }
233
+ }
@@ -0,0 +1,9 @@
1
+ export enum AccountStatus {
2
+ ACTIVE = 'ACTIVE',
3
+ INACTIVE = 'INACTIVE',
4
+ NSF_SUSPENDED = 'NSF_SUSPENDED',
5
+ PENDING_STAFF_REVIEW = 'PENDING_STAFF_REVIEW',
6
+ PENDING_ACTIVATION = 'PENDING_ACTIVATION',
7
+ REJECTED = 'REJECTED',
8
+ SUSPENDED = 'SUSPENDED'
9
+ }
@@ -0,0 +1,6 @@
1
+ export enum AccountType {
2
+ BASIC = 'BASIC',
3
+ PREMIUM = 'PREMIUM',
4
+ SBC_STAFF = 'SBC_STAFF',
5
+ STAFF = 'STAFF'
6
+ }
@@ -0,0 +1,5 @@
1
+ export enum ConnectAuthStorageKeys {
2
+ LOGIN_REDIRECT_URL = 'sbc-connect-login-redirect-url',
3
+ LOGOUT_REDIRECT_URL = 'sbc-connect-logout-redirect-url',
4
+ CONNECT_SESSION_EXPIRED = 'connect-session-expired'
5
+ }
@@ -0,0 +1,6 @@
1
+ export enum ConnectErrorCategory {
2
+ USER_ACCOUNTS = 'user-accounts',
3
+ ACCOUNT_LIST = 'account-list',
4
+ PENDING_APPROVAL_COUNT = 'pending-approval-count',
5
+ USER_INFO = 'user-info'
6
+ }