@sbc-connect/nuxt-auth 0.1.7 → 0.1.9
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 +7 -1
- package/CHANGELOG.md +26 -0
- package/README.md +2 -9
- package/app/app.config.ts +4 -1
- package/app/components/Connect/Header/AccountLabel.vue +1 -1
- package/app/components/Connect/Modal/SessionExpired.vue +93 -0
- package/app/composables/useConnectAuth.ts +3 -25
- package/app/composables/useConnectHeaderOptions.ts +6 -7
- package/app/composables/useConnectLaunchDarkly.ts +245 -0
- package/app/enums/connect-auth-storage-key.ts +3 -0
- package/app/layouts/ConnectAuth.vue +11 -0
- package/app/middleware/connect-auth.ts +9 -0
- package/app/pages/auth/login.vue +99 -0
- package/app/pages/auth/logout.vue +23 -0
- package/app/plugins/connect-auth.client.ts +15 -21
- package/app/stores/connect-account.ts +25 -73
- package/app/types/auth-app-config.d.ts +4 -1
- package/app/types/auth-page-meta.d.ts +8 -0
- package/app/utils/resetPiniaStores.ts +33 -0
- package/app/utils/setOnBeforeSessionExpired.ts +23 -0
- package/i18n/locales/en-CA.ts +23 -0
- package/nuxt.config.ts +5 -1
- package/package.json +5 -4
- package/public/img/BCReg_Generic_Login_image.jpg +0 -0
- package/app/enums/connect-auth-storage-keys.ts +0 -5
- package/app/enums/connect-error-category.ts +0 -6
- package/app/interfaces/connect-api-error.ts +0 -6
package/.env.example
CHANGED
|
@@ -22,4 +22,10 @@ NUXT_PUBLIC_LD_CLIENT_ID=""
|
|
|
22
22
|
NUXT_PUBLIC_IDP_URL="https://dev.loginproxy.gov.bc.ca/auth"
|
|
23
23
|
NUXT_PUBLIC_IDP_REALM="bcregistry"
|
|
24
24
|
NUXT_PUBLIC_IDP_CLIENTID="connect-web"
|
|
25
|
-
NUXT_PUBLIC_SITEMINDER_LOGOUT_URL="https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi"
|
|
25
|
+
NUXT_PUBLIC_SITEMINDER_LOGOUT_URL="https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi"
|
|
26
|
+
|
|
27
|
+
# Session Timeout
|
|
28
|
+
NUXT_PUBLIC_TOKEN_REFRESH_INTERVAL=30000
|
|
29
|
+
NUXT_PUBLIC_TOKEN_MIN_VALIDITY=120000
|
|
30
|
+
NUXT_PUBLIC_SESSION_INACTIVITY_TIMEOUT=1800000
|
|
31
|
+
NUXT_PUBLIC_SESSION_MODAL_TIMEOUT=120000
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @sbc-connect/nuxt-auth
|
|
2
2
|
|
|
3
|
+
## 0.1.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#40](https://github.com/bcgov/connect-nuxt/pull/40) [`f4b62e1`](https://github.com/bcgov/connect-nuxt/commit/f4b62e19570ed062399ce7d23ce07abcf682285f) Thanks [@deetz99](https://github.com/deetz99)! - Add login page and auth middleware issue: bcgov/entity#29335
|
|
8
|
+
|
|
9
|
+
- [#38](https://github.com/bcgov/connect-nuxt/pull/38) [`8b8f067`](https://github.com/bcgov/connect-nuxt/commit/8b8f067aba4cda2cd2cd8de5c6f74ccc24eaf822) Thanks [@deetz99](https://github.com/deetz99)! - Add LaunchDarkly composable with user context. issue: bcgov/entity#29335
|
|
10
|
+
|
|
11
|
+
## 0.1.8
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#34](https://github.com/bcgov/connect-nuxt/pull/34) [`b2a0458`](https://github.com/bcgov/connect-nuxt/commit/b2a04587d5408d213d463ef6161b701ca597ef86) Thanks [@deetz99](https://github.com/deetz99)! - - handle user session expiry in auth plugin
|
|
16
|
+
|
|
17
|
+
- onBeforeSessionExpiry route meta
|
|
18
|
+
- onAccountChange route meta
|
|
19
|
+
- resetPiniaStores util and reset on logout
|
|
20
|
+
- add account id to layouts/ConnectAuth breadcrumb slot
|
|
21
|
+
|
|
22
|
+
issue: bcgov/entity#29335
|
|
23
|
+
|
|
24
|
+
- [#32](https://github.com/bcgov/connect-nuxt/pull/32) [`66d83d1`](https://github.com/bcgov/connect-nuxt/commit/66d83d14b2ec7950057dd39a4d876a8c4096923f) Thanks [@kialj876](https://github.com/kialj876)! - Pay layer cleanup bcgov/entity#29337
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [[`66d83d1`](https://github.com/bcgov/connect-nuxt/commit/66d83d14b2ec7950057dd39a4d876a8c4096923f)]:
|
|
27
|
+
- @sbc-connect/nuxt-base@0.1.8
|
|
28
|
+
|
|
3
29
|
## 0.1.7
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ This package provides the necessary composables, plugins, and logic to integrate
|
|
|
16
16
|
- login() and logout() helper functions.
|
|
17
17
|
- Automatic token refreshing and session validation.
|
|
18
18
|
|
|
19
|
-
For detailed usage and documentation, please see the [Auth Layer Docs](../../../docs/packages/layers/auth/
|
|
19
|
+
For detailed usage and documentation, please see the [Auth Layer Docs](../../../docs/packages/layers/auth/overview.md).
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
@@ -46,14 +46,7 @@ Create a file named .env in the root of the project.
|
|
|
46
46
|
Copy the contents of the .env.example file into your new .env file.
|
|
47
47
|
|
|
48
48
|
### Local Development
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
# .env
|
|
53
|
-
NUXT_PUBLIC_AUTH_PROVIDER_URL="https://dev.oidc.gov.bc.ca/auth"
|
|
54
|
-
NUXT_PUBLIC_AUTH_PROVIDER_REALM="your-realm-name"
|
|
55
|
-
NUXT_PUBLIC_AUTH_PROVIDER_CLIENT_ID="your-client-id"
|
|
56
|
-
```
|
|
49
|
+
Copy the contents of the **.env.example** file into your new .env file.
|
|
57
50
|
|
|
58
51
|
### Production Environments
|
|
59
52
|
> [!IMPORTANT]
|
package/app/app.config.ts
CHANGED
|
@@ -26,7 +26,7 @@ defineProps({
|
|
|
26
26
|
</span>
|
|
27
27
|
<span
|
|
28
28
|
class="line-clamp-1 overflow-hidden text-ellipsis text-xs opacity-75"
|
|
29
|
-
:class="{ 'text-line': theme === 'header', 'text-neutral': theme === 'dropdown' }"
|
|
29
|
+
:class="{ 'text-line-muted': theme === 'header', 'text-neutral': theme === 'dropdown' }"
|
|
30
30
|
>
|
|
31
31
|
{{ accountName }}
|
|
32
32
|
</span>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const isSmallScreen = useMediaQuery('(max-width: 640px)')
|
|
3
|
+
const rtc = useRuntimeConfig().public
|
|
4
|
+
const modalTimeout = rtc.sessionModalTimeout ? Number(rtc.sessionModalTimeout) : 120000
|
|
5
|
+
const { t } = useI18n()
|
|
6
|
+
const route = useRoute()
|
|
7
|
+
|
|
8
|
+
const emit = defineEmits<{ close: [] }>()
|
|
9
|
+
|
|
10
|
+
const timeRemaining = ref(toValue((modalTimeout) / 1000))
|
|
11
|
+
|
|
12
|
+
const intervalId = setInterval(async () => {
|
|
13
|
+
const value = timeRemaining.value - 1
|
|
14
|
+
timeRemaining.value = value < 0 ? 0 : value
|
|
15
|
+
|
|
16
|
+
if (value === 0) {
|
|
17
|
+
if (route.meta.onBeforeSessionExpired) {
|
|
18
|
+
await route.meta.onBeforeSessionExpired()
|
|
19
|
+
}
|
|
20
|
+
sessionStorage.setItem(ConnectAuthStorageKey.CONNECT_SESSION_EXPIRED, 'true')
|
|
21
|
+
await useConnectAuth().logout()
|
|
22
|
+
}
|
|
23
|
+
}, 1000)
|
|
24
|
+
|
|
25
|
+
const ariaCountdownText = computed(() => {
|
|
26
|
+
if (timeRemaining.value === 30) { // trigger aria alert when 30 seconds remain
|
|
27
|
+
return t('connect.sessionExpiry.content', { count: timeRemaining.value })
|
|
28
|
+
} else if (timeRemaining.value === 2) { // trigger aria alert when session expires
|
|
29
|
+
return t('connect.sessionExpiry.sessionExpired')
|
|
30
|
+
} else {
|
|
31
|
+
return ''
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
function closeModal() {
|
|
36
|
+
clearInterval(intervalId)
|
|
37
|
+
emit('close')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onMounted(() => {
|
|
41
|
+
// allow any keypress to close the modal
|
|
42
|
+
window.addEventListener('keydown', closeModal)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
onUnmounted(() => {
|
|
46
|
+
// cleanup
|
|
47
|
+
window.removeEventListener('keydown', closeModal)
|
|
48
|
+
})
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<UModal
|
|
53
|
+
id="session-expired-dialog"
|
|
54
|
+
overlay
|
|
55
|
+
:title="$t('connect.sessionExpiry.title')"
|
|
56
|
+
:description="$t('connect.sessionExpiry.content', { count: timeRemaining })"
|
|
57
|
+
@after:leave="closeModal"
|
|
58
|
+
>
|
|
59
|
+
<template #content>
|
|
60
|
+
<div class="p-10 flex flex-col gap-6">
|
|
61
|
+
<div role="alert">
|
|
62
|
+
<h2
|
|
63
|
+
id="session-expired-dialog-title"
|
|
64
|
+
class="text-xl font-bold text-neutral-highlighted"
|
|
65
|
+
>
|
|
66
|
+
{{ $t('connect.sessionExpiry.title') }}
|
|
67
|
+
</h2>
|
|
68
|
+
</div>
|
|
69
|
+
<div>
|
|
70
|
+
<ConnectI18nHelper
|
|
71
|
+
id="session-expired-dialog-description"
|
|
72
|
+
translation-path="connect.sessionExpiry.content"
|
|
73
|
+
:count="timeRemaining"
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<div role="alert">
|
|
77
|
+
<span class="sr-only">{{ ariaCountdownText }}</span>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="flex flex-wrap items-center justify-center gap-4">
|
|
81
|
+
<UButton
|
|
82
|
+
:block="isSmallScreen"
|
|
83
|
+
:label="$t('connect.sessionExpiry.continueBtn.main')"
|
|
84
|
+
:aria-label="$t('connect.sessionExpiry.continueBtn.aria')"
|
|
85
|
+
size="xl"
|
|
86
|
+
class="font-bold"
|
|
87
|
+
@click="closeModal"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
</UModal>
|
|
93
|
+
</template>
|
|
@@ -9,8 +9,7 @@ export const useConnectAuth = () => {
|
|
|
9
9
|
* @returns A promise that resolves when login is complete.
|
|
10
10
|
*/
|
|
11
11
|
function login(idpHint: ConnectIdpHint, redirect?: string): Promise<void> {
|
|
12
|
-
const
|
|
13
|
-
const redirectUri = redirect ?? loginRedirectUrl ?? window.location.href
|
|
12
|
+
const redirectUri = redirect ?? window.location.href
|
|
14
13
|
|
|
15
14
|
return $connectAuth.login(
|
|
16
15
|
{
|
|
@@ -27,14 +26,13 @@ export const useConnectAuth = () => {
|
|
|
27
26
|
*/
|
|
28
27
|
function logout(redirect?: string): Promise<void> {
|
|
29
28
|
const siteminderUrl = rtc.siteminderLogoutUrl
|
|
30
|
-
|
|
31
|
-
let redirectUri = redirect ?? logoutRedirectUrl ?? window.location.href
|
|
29
|
+
let redirectUri = redirect ?? window.location.href
|
|
32
30
|
|
|
33
31
|
if (siteminderUrl) {
|
|
34
32
|
redirectUri = `${siteminderUrl}?returl=${redirectUri.replace(/(https?:\/\/)|(\/)+/g, '$1$2')}&retnow=1`
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
resetPiniaStores()
|
|
38
36
|
return $connectAuth.logout({
|
|
39
37
|
redirectUri
|
|
40
38
|
})
|
|
@@ -81,30 +79,10 @@ export const useConnectAuth = () => {
|
|
|
81
79
|
})
|
|
82
80
|
}
|
|
83
81
|
|
|
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
82
|
return {
|
|
101
83
|
login,
|
|
102
84
|
logout,
|
|
103
85
|
getToken,
|
|
104
|
-
clearLoginRedirectUrl,
|
|
105
|
-
clearLogoutRedirectUrl,
|
|
106
|
-
setLoginRedirectUrl,
|
|
107
|
-
setLogoutRedirectUrl,
|
|
108
86
|
isAuthenticated,
|
|
109
87
|
authUser
|
|
110
88
|
}
|
|
@@ -11,8 +11,9 @@ export function useConnectHeaderOptions() {
|
|
|
11
11
|
const route = useRoute()
|
|
12
12
|
const overlay = useOverlay()
|
|
13
13
|
const { t, locale: { value: locale } } = useNuxtApp().$i18n
|
|
14
|
-
const { login,
|
|
14
|
+
const { login, isAuthenticated, authUser } = useConnectAuth()
|
|
15
15
|
const accountStore = useConnectAccountStore()
|
|
16
|
+
const localePath = useLocalePath()
|
|
16
17
|
|
|
17
18
|
const whatsNew = useStorage<ConnectWhatsNewState>('connect-whats-new', { viewed: false, items: [] })
|
|
18
19
|
const slideover = overlay.create(ConnectSlideoverWhatsNew)
|
|
@@ -38,7 +39,7 @@ export function useConnectHeaderOptions() {
|
|
|
38
39
|
options.push({
|
|
39
40
|
label: t('connect.label.logout'),
|
|
40
41
|
icon: 'i-mdi-logout-variant',
|
|
41
|
-
onSelect: () => logout
|
|
42
|
+
onSelect: () => navigateTo(localePath('/auth/logout'))
|
|
42
43
|
})
|
|
43
44
|
return options
|
|
44
45
|
})
|
|
@@ -87,9 +88,7 @@ export function useConnectHeaderOptions() {
|
|
|
87
88
|
onSelect: () => {
|
|
88
89
|
if (!isActive && account.id) {
|
|
89
90
|
if (route.meta.onAccountChange) {
|
|
90
|
-
|
|
91
|
-
const allowAccountChange = true
|
|
92
|
-
// const allowAccountChange = route.meta.onAccountChange(accountStore.currentAccount, account)
|
|
91
|
+
const allowAccountChange = route.meta.onAccountChange(accountStore.currentAccount, account)
|
|
93
92
|
if (allowAccountChange) {
|
|
94
93
|
accountStore.switchCurrentAccount(account.id)
|
|
95
94
|
}
|
|
@@ -136,8 +135,8 @@ export function useConnectHeaderOptions() {
|
|
|
136
135
|
return options
|
|
137
136
|
})
|
|
138
137
|
|
|
139
|
-
const loginRedirectUrl = ac.login.
|
|
140
|
-
? appBaseUrl + locale + ac.login.
|
|
138
|
+
const loginRedirectUrl = ac.login.redirect
|
|
139
|
+
? appBaseUrl + locale + ac.login.redirect
|
|
141
140
|
: undefined
|
|
142
141
|
|
|
143
142
|
const loginOptionsMap: Record<'bcsc' | 'bceid' | 'idir',
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { initialize } from 'launchdarkly-js-client-sdk'
|
|
2
|
+
import type { LDClient, LDFlagSet, LDOptions, LDMultiKindContext } from 'launchdarkly-js-client-sdk'
|
|
3
|
+
import { isEqual } from 'es-toolkit'
|
|
4
|
+
|
|
5
|
+
// default anon context
|
|
6
|
+
const anonymousContext: LDMultiKindContext = {
|
|
7
|
+
kind: 'multi',
|
|
8
|
+
org: { key: 'anonymous' },
|
|
9
|
+
user: { key: 'anonymous' }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// state
|
|
13
|
+
// Defined outside of composable, so they are created only once and shared
|
|
14
|
+
const ldClient = shallowRef<LDClient | null>(null)
|
|
15
|
+
const ldFlagSet = shallowRef<LDFlagSet>({})
|
|
16
|
+
const ldContext = shallowRef<LDMultiKindContext>(anonymousContext)
|
|
17
|
+
const ldInitialized = ref(false)
|
|
18
|
+
const isInitializing = ref(false)
|
|
19
|
+
|
|
20
|
+
function _createLdContext(): LDMultiKindContext {
|
|
21
|
+
const appName = useRuntimeConfig().public.appName
|
|
22
|
+
const { authUser, isAuthenticated } = useConnectAuth()
|
|
23
|
+
const account = useConnectAccountStore().currentAccount
|
|
24
|
+
|
|
25
|
+
if (!isAuthenticated.value) {
|
|
26
|
+
return anonymousContext
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create user context
|
|
30
|
+
const user = {
|
|
31
|
+
key: authUser.value.keycloakGuid,
|
|
32
|
+
firstName: authUser.value.firstName,
|
|
33
|
+
lastName: authUser.value.lastName,
|
|
34
|
+
email: authUser.value.email,
|
|
35
|
+
roles: authUser.value.roles,
|
|
36
|
+
loginSource: authUser.value.loginSource,
|
|
37
|
+
appSource: appName
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Default org to user key if no account
|
|
41
|
+
let org: Partial<ConnectAccount & { key: string, appSource: string }> = { key: user.key, appSource: appName }
|
|
42
|
+
|
|
43
|
+
// Use account info if available
|
|
44
|
+
if (account.id) {
|
|
45
|
+
org = {
|
|
46
|
+
key: String(account.id),
|
|
47
|
+
accountType: account.accountType,
|
|
48
|
+
accountStatus: account.accountStatus,
|
|
49
|
+
type: account.type,
|
|
50
|
+
label: account.label,
|
|
51
|
+
appSource: appName
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { kind: 'multi', org, user }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _updateLdContext() {
|
|
59
|
+
if (!ldClient.value) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const newContext = _createLdContext()
|
|
64
|
+
if (isEqual(ldContext.value, newContext)) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ldContext.value = newContext
|
|
69
|
+
ldClient.value.identify(newContext).then(() => {
|
|
70
|
+
ldFlagSet.value = ldClient.value?.allFlags() || {}
|
|
71
|
+
}).catch((error) => {
|
|
72
|
+
console.error('LaunchDarkly: Failed to update context.', error)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Initializes the LaunchDarkly client.
|
|
78
|
+
*/
|
|
79
|
+
function _init(): void {
|
|
80
|
+
const rtc = useRuntimeConfig().public
|
|
81
|
+
|
|
82
|
+
// Prevent re-initialization
|
|
83
|
+
if (ldInitialized.value || isInitializing.value) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
// Prevent initialization if missing client ID
|
|
87
|
+
if (!rtc.ldClientId) {
|
|
88
|
+
console.error('LaunchDarkly: ldClientId is not configured.')
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
isInitializing.value = true
|
|
93
|
+
|
|
94
|
+
ldContext.value = _createLdContext()
|
|
95
|
+
|
|
96
|
+
const options: LDOptions = {
|
|
97
|
+
streaming: false,
|
|
98
|
+
useReport: false,
|
|
99
|
+
diagnosticOptOut: true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
ldClient.value = initialize(rtc.ldClientId, ldContext.value, options)
|
|
104
|
+
|
|
105
|
+
ldClient.value.on('initialized', () => {
|
|
106
|
+
ldInitialized.value = true
|
|
107
|
+
isInitializing.value = false
|
|
108
|
+
ldFlagSet.value = ldClient.value?.allFlags() || {}
|
|
109
|
+
console.info('LaunchDarkly: Anonymous initialization complete.')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
ldClient.value.on('error', (error) => {
|
|
113
|
+
console.error('LaunchDarkly: Initialization error.', error)
|
|
114
|
+
isInitializing.value = false
|
|
115
|
+
})
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('LaunchDarkly: Failed to initialize.', error)
|
|
118
|
+
isInitializing.value = false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Composable for the LaunchDarkly service.
|
|
124
|
+
*/
|
|
125
|
+
export const useConnectLaunchDarkly = () => {
|
|
126
|
+
// initialize only once
|
|
127
|
+
if (!ldInitialized.value && !isInitializing.value && import.meta.client) {
|
|
128
|
+
_init()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const { isAuthenticated } = useConnectAuth()
|
|
132
|
+
const accountStore = useConnectAccountStore()
|
|
133
|
+
|
|
134
|
+
watch(
|
|
135
|
+
[isAuthenticated, () => accountStore.currentAccount],
|
|
136
|
+
() => {
|
|
137
|
+
_updateLdContext()
|
|
138
|
+
},
|
|
139
|
+
{ immediate: true }
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Returns a flag's value. Can operate in two modes.
|
|
144
|
+
* @param name The name of the feature flag.
|
|
145
|
+
* @param defaultValue The value to use until the flag is loaded.
|
|
146
|
+
* @param mode - 'reactive' (default) returns a ref that updates automatically.
|
|
147
|
+
* 'await' returns a promise that resolves when the client is ready.
|
|
148
|
+
* @returns A readonly ref or a promise resolving to the flag's value.
|
|
149
|
+
*/
|
|
150
|
+
function getFeatureFlag<T>(
|
|
151
|
+
name: string,
|
|
152
|
+
defaultValue: T,
|
|
153
|
+
mode: 'await'
|
|
154
|
+
): Promise<T>
|
|
155
|
+
function getFeatureFlag<T>(
|
|
156
|
+
name: string,
|
|
157
|
+
defaultValue: T,
|
|
158
|
+
mode?: 'reactive'
|
|
159
|
+
): Readonly<Ref<T>>
|
|
160
|
+
function getFeatureFlag<T>(
|
|
161
|
+
name: string,
|
|
162
|
+
defaultValue?: T,
|
|
163
|
+
mode?: 'reactive'
|
|
164
|
+
): Readonly<Ref<T | undefined>>
|
|
165
|
+
function getFeatureFlag<T>(
|
|
166
|
+
name: string,
|
|
167
|
+
defaultValue?: T,
|
|
168
|
+
mode: 'reactive' | 'await' = 'reactive'
|
|
169
|
+
): Readonly<Ref<T | undefined>> | Promise<T | undefined> {
|
|
170
|
+
if (mode === 'await') {
|
|
171
|
+
if (!ldClient.value) {
|
|
172
|
+
return Promise.resolve(defaultValue)
|
|
173
|
+
}
|
|
174
|
+
return ldClient.value.waitUntilReady()
|
|
175
|
+
.then(() => ldClient.value ? ldClient.value.variation(name, defaultValue) : defaultValue)
|
|
176
|
+
.catch((error) => {
|
|
177
|
+
console.error(`LaunchDarkly: Error waiting for client while getting flag "${name}".`, error)
|
|
178
|
+
return defaultValue
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return readonly(computed(() => {
|
|
183
|
+
if (!ldClient.value || !ldInitialized.value || !ldFlagSet.value) {
|
|
184
|
+
return defaultValue
|
|
185
|
+
}
|
|
186
|
+
return ldClient.value.variation(name, defaultValue)
|
|
187
|
+
}))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Returns a flag's value from the locally stored flag set. Can operate in two modes.
|
|
192
|
+
* @param name The name of the feature flag.
|
|
193
|
+
* @param defaultValue The value to use until the flag is loaded.
|
|
194
|
+
* @param mode - 'reactive' (default) returns a ref that updates automatically.
|
|
195
|
+
* 'await' returns a promise that resolves when the client is ready.
|
|
196
|
+
* @returns A readonly ref or a promise resolving to the flag's value.
|
|
197
|
+
*/
|
|
198
|
+
async function getStoredFlag<T>(name: string, defaultValue: T, mode: 'await'): Promise<T>
|
|
199
|
+
function getStoredFlag<T>(name: string, defaultValue?: T, mode?: 'reactive'): Readonly<Ref<T>>
|
|
200
|
+
function getStoredFlag<T>(
|
|
201
|
+
name: string,
|
|
202
|
+
defaultValue: T,
|
|
203
|
+
mode: 'reactive' | 'await' = 'reactive'
|
|
204
|
+
): Readonly<Ref<T>> | Promise<T> {
|
|
205
|
+
if (mode === 'await') {
|
|
206
|
+
if (!ldClient.value) {
|
|
207
|
+
return Promise.resolve(defaultValue)
|
|
208
|
+
}
|
|
209
|
+
return ldClient.value.waitUntilReady()
|
|
210
|
+
.then(() => ldFlagSet.value[name] ?? defaultValue)
|
|
211
|
+
.catch((error) => {
|
|
212
|
+
console.error(`LaunchDarkly: Error waiting for client while getting stored flag "${name}".`, error)
|
|
213
|
+
return defaultValue
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// reactive mode
|
|
218
|
+
return readonly(computed(() => {
|
|
219
|
+
if (!ldInitialized.value || !ldFlagSet.value) {
|
|
220
|
+
return defaultValue
|
|
221
|
+
}
|
|
222
|
+
return ldFlagSet.value[name] ?? defaultValue
|
|
223
|
+
}))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Resets the LaunchDarkly state and closes the client connection.
|
|
228
|
+
*/
|
|
229
|
+
const $reset = () => {
|
|
230
|
+
ldInitialized.value = false
|
|
231
|
+
isInitializing.value = false
|
|
232
|
+
ldClient.value?.close()
|
|
233
|
+
ldClient.value = null
|
|
234
|
+
ldFlagSet.value = {}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
getFeatureFlag,
|
|
239
|
+
getStoredFlag,
|
|
240
|
+
ldInitialized: readonly(ldInitialized),
|
|
241
|
+
ldFlagSet: readonly(ldFlagSet),
|
|
242
|
+
ldClient: readonly(ldClient),
|
|
243
|
+
$reset
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const accountStore = useConnectAccountStore()
|
|
3
|
+
</script>
|
|
4
|
+
|
|
1
5
|
<template>
|
|
2
6
|
<ConnectLayout>
|
|
3
7
|
<template #header>
|
|
4
8
|
<ConnectHeaderAuth />
|
|
5
9
|
</template>
|
|
10
|
+
<template #breadcrumb>
|
|
11
|
+
<ConnectBreadcrumb
|
|
12
|
+
:account-id="accountStore.currentAccount.id
|
|
13
|
+
? String(accountStore.currentAccount.id)
|
|
14
|
+
: undefined"
|
|
15
|
+
/>
|
|
16
|
+
</template>
|
|
6
17
|
<slot />
|
|
7
18
|
</ConnectLayout>
|
|
8
19
|
</template>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export default defineNuxtRouteMiddleware((to) => {
|
|
2
|
+
const { isAuthenticated } = useConnectAuth()
|
|
3
|
+
const rtc = useRuntimeConfig().public
|
|
4
|
+
const localePath = useLocalePath()
|
|
5
|
+
|
|
6
|
+
if (!isAuthenticated.value) {
|
|
7
|
+
return navigateTo(localePath(`/auth/login?return=${rtc.baseUrl}${to.fullPath.slice(1)}`))
|
|
8
|
+
}
|
|
9
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import loginImage from '#auth/public/img/BCReg_Generic_Login_image.jpg'
|
|
3
|
+
|
|
4
|
+
const { t, locale } = useI18n()
|
|
5
|
+
const { login } = useConnectAuth()
|
|
6
|
+
const rtc = useRuntimeConfig().public
|
|
7
|
+
const ac = useAppConfig().connect
|
|
8
|
+
const route = useRoute()
|
|
9
|
+
|
|
10
|
+
useHead({
|
|
11
|
+
title: t('connect.page.login.title')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
definePageMeta({
|
|
15
|
+
layout: 'connect-auth',
|
|
16
|
+
hideBreadcrumbs: true,
|
|
17
|
+
middleware: async (to) => {
|
|
18
|
+
const { $connectAuth, $router, _appConfig } = useNuxtApp()
|
|
19
|
+
if ($connectAuth.authenticated) {
|
|
20
|
+
if (to.query.return) {
|
|
21
|
+
window.location.replace(to.query.return as string)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
await $router.push(_appConfig.connect.login.redirect || '/')
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const isSessionExpired = sessionStorage.getItem(ConnectAuthStorageKey.CONNECT_SESSION_EXPIRED)
|
|
30
|
+
|
|
31
|
+
const loginOptions = computed(() => {
|
|
32
|
+
const urlReturn = route.query.return
|
|
33
|
+
const redirectUrl = urlReturn !== undefined
|
|
34
|
+
? urlReturn as string
|
|
35
|
+
: `${rtc.baseUrl}${locale.value}${ac.login.redirect}`
|
|
36
|
+
|
|
37
|
+
const loginOptionsMap: Record<
|
|
38
|
+
'bcsc' | 'bceid' | 'idir',
|
|
39
|
+
{ label: string, icon: string, onClick: () => Promise<void> }
|
|
40
|
+
> = {
|
|
41
|
+
bcsc: {
|
|
42
|
+
label: t('connect.page.login.loginBCSC'),
|
|
43
|
+
icon: 'i-mdi-account-card-details-outline',
|
|
44
|
+
onClick: () => login(ConnectIdpHint.BCSC, redirectUrl)
|
|
45
|
+
},
|
|
46
|
+
bceid: {
|
|
47
|
+
label: t('connect.page.login.loginBCEID'),
|
|
48
|
+
icon: 'i-mdi-two-factor-authentication',
|
|
49
|
+
onClick: () => login(ConnectIdpHint.BCEID, redirectUrl)
|
|
50
|
+
},
|
|
51
|
+
idir: {
|
|
52
|
+
label: t('connect.page.login.loginIDIR'),
|
|
53
|
+
icon: 'i-mdi-account-group-outline',
|
|
54
|
+
onClick: () => login(ConnectIdpHint.IDIR, redirectUrl)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return ac.login.idps.map(key => loginOptionsMap[key as keyof typeof loginOptionsMap])
|
|
59
|
+
})
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<div class="flex grow flex-col items-center justify-center py-10">
|
|
64
|
+
<div class="flex flex-col items-center gap-10">
|
|
65
|
+
<h1>
|
|
66
|
+
{{ $t('connect.page.login.h1') }}
|
|
67
|
+
</h1>
|
|
68
|
+
<UAlert
|
|
69
|
+
v-if="isSessionExpired"
|
|
70
|
+
color="warning"
|
|
71
|
+
variant="subtle"
|
|
72
|
+
:title="$t('connect.page.login.sessionExpiredAlert.title')"
|
|
73
|
+
:description="$t('connect.page.login.sessionExpiredAlert.description')"
|
|
74
|
+
icon="i-mdi-alert"
|
|
75
|
+
/>
|
|
76
|
+
<UCard class="my-auto max-w-md">
|
|
77
|
+
<img
|
|
78
|
+
:src="loginImage"
|
|
79
|
+
class="pb-4"
|
|
80
|
+
:alt="$t('connect.text.imageAltGenericLogin')"
|
|
81
|
+
>
|
|
82
|
+
<div class="space-y-4 pt-2.5">
|
|
83
|
+
<div
|
|
84
|
+
v-for="(option, i) in loginOptions"
|
|
85
|
+
:key="option.label"
|
|
86
|
+
class="flex flex-col items-center gap-1"
|
|
87
|
+
>
|
|
88
|
+
<UButton
|
|
89
|
+
:variant="i === 0 ? 'solid' : 'outline'"
|
|
90
|
+
block
|
|
91
|
+
class="py-2.5"
|
|
92
|
+
v-bind="option"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</UCard>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const route = useRoute()
|
|
3
|
+
const rtc = useRuntimeConfig().public
|
|
4
|
+
const ac = useAppConfig().connect
|
|
5
|
+
const { locale } = useI18n()
|
|
6
|
+
const { logout } = useConnectAuth()
|
|
7
|
+
|
|
8
|
+
const redirectUrl = computed(() => {
|
|
9
|
+
const urlReturn = route.query.return
|
|
10
|
+
const url = urlReturn !== undefined
|
|
11
|
+
? urlReturn as string
|
|
12
|
+
: `${rtc.baseUrl}${locale.value}${ac.login.redirect}`
|
|
13
|
+
return url
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
onMounted(async () => {
|
|
17
|
+
await logout(redirectUrl.value)
|
|
18
|
+
})
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<ConnectSpinner fullscreen />
|
|
23
|
+
</template>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Keycloak from 'keycloak-js'
|
|
2
|
+
import { ConnectModalSessionExpired } from '#components'
|
|
2
3
|
|
|
3
4
|
export default defineNuxtPlugin(async () => {
|
|
4
5
|
const rtc = useRuntimeConfig().public
|
|
@@ -10,6 +11,10 @@ export default defineNuxtPlugin(async () => {
|
|
|
10
11
|
clientId: rtc.idpClientid
|
|
11
12
|
})
|
|
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
|
+
|
|
13
18
|
try {
|
|
14
19
|
// default behaviour when keycloak session expires
|
|
15
20
|
// try to update token - log out if token update fails
|
|
@@ -18,7 +23,7 @@ export default defineNuxtPlugin(async () => {
|
|
|
18
23
|
keycloak.onTokenExpired = async () => {
|
|
19
24
|
try {
|
|
20
25
|
console.info('[Auth] Token expired, refreshing token...')
|
|
21
|
-
await keycloak.updateToken(
|
|
26
|
+
await keycloak.updateToken(tokenMinValidity)
|
|
22
27
|
console.info('[Auth] Token refreshed.')
|
|
23
28
|
} catch (error) {
|
|
24
29
|
console.error('[Auth] Failed to refresh token on expiration; logging out.', error)
|
|
@@ -36,34 +41,24 @@ export default defineNuxtPlugin(async () => {
|
|
|
36
41
|
console.error('[Auth] Failed to initialize Keycloak adapter: ', error)
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
const refreshIntervalTimeout = 30000 // rtc.tokenRefreshInterval as number
|
|
41
|
-
const minValidity = 120 // toValue((rtc.tokenMinValidity as number) / 1000) // convert to seconds
|
|
42
|
-
const idleTimeout = 1800000 // rtc.sessionIdleTimeout as number
|
|
43
|
-
|
|
44
|
-
// const route = useRoute()
|
|
45
|
-
const { idle } = useIdle(idleTimeout)
|
|
44
|
+
const { idle } = useIdle(sessionInactivityTimeout)
|
|
46
45
|
|
|
47
46
|
// executed when user is authenticated and idle = true
|
|
48
|
-
// TODO: manage session expiry
|
|
49
47
|
async function sessionExpired() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// await useConnectModals().openSessionExpiringModal()
|
|
54
|
-
// }
|
|
55
|
-
console.info('TODO - MANAGE SESSION EXPIRY')
|
|
48
|
+
const overlay = useOverlay()
|
|
49
|
+
const modal = overlay.create(ConnectModalSessionExpired)
|
|
50
|
+
modal.open()
|
|
56
51
|
}
|
|
57
52
|
|
|
58
|
-
// refresh token if expiring within <
|
|
53
|
+
// refresh token if expiring within <tokenMinValidity> - checks every <tokenRefreshInterval>
|
|
59
54
|
function scheduleRefreshToken() {
|
|
60
55
|
console.info('[Auth] Verifying token validity.')
|
|
61
56
|
|
|
62
57
|
setTimeout(async () => {
|
|
63
|
-
if (keycloak.isTokenExpired(
|
|
58
|
+
if (keycloak.isTokenExpired(tokenMinValidity)) {
|
|
64
59
|
console.info('[Auth] Token set to expire soon. Refreshing token...')
|
|
65
60
|
try {
|
|
66
|
-
await keycloak.updateToken(
|
|
61
|
+
await keycloak.updateToken(tokenMinValidity)
|
|
67
62
|
console.info('[Auth] Token refreshed.')
|
|
68
63
|
} catch (error) {
|
|
69
64
|
console.error('[Auth] Failed to refresh token; logging out.', error)
|
|
@@ -72,7 +67,7 @@ export default defineNuxtPlugin(async () => {
|
|
|
72
67
|
}
|
|
73
68
|
|
|
74
69
|
scheduleRefreshToken()
|
|
75
|
-
},
|
|
70
|
+
}, tokenRefreshInterval)
|
|
76
71
|
}
|
|
77
72
|
|
|
78
73
|
// Watch for changes in authentication and idle state
|
|
@@ -82,8 +77,7 @@ export default defineNuxtPlugin(async () => {
|
|
|
82
77
|
[() => keycloak.authenticated, () => idle.value],
|
|
83
78
|
async ([isAuth, isIdle]) => {
|
|
84
79
|
if (isAuth) {
|
|
85
|
-
|
|
86
|
-
// sessionStorage.removeItem(ConnectStorageKeys.CONNECT_SESSION_EXPIRED)
|
|
80
|
+
sessionStorage.removeItem(ConnectAuthStorageKey.CONNECT_SESSION_EXPIRED)
|
|
87
81
|
if (!isIdle) {
|
|
88
82
|
console.info('[Auth] Starting token refresh schedule.')
|
|
89
83
|
scheduleRefreshToken()
|
|
@@ -11,7 +11,6 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
|
|
|
11
11
|
const userFirstName = ref<string>(user.value?.firstName || '-')
|
|
12
12
|
const userLastName = ref<string>(user.value?.lastName || '')
|
|
13
13
|
const userFullName = computed(() => `${userFirstName.value} ${userLastName.value}`)
|
|
14
|
-
const errors = ref<ConnectApiError[]>([])
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* Checks if the current account or the Keycloak user has any of the specified roles.
|
|
@@ -42,35 +41,17 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
|
|
|
42
41
|
|
|
43
42
|
/** Get user information from AUTH */
|
|
44
43
|
async function getAuthUserProfile(identifier: string): Promise<{ firstname: string, lastname: string } | undefined> {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
onResponseError({ response }) {
|
|
49
|
-
errors.value.push({
|
|
50
|
-
statusCode: response.status || 500,
|
|
51
|
-
message: response._data?.message || response._data?.description || 'Error fetching user info.',
|
|
52
|
-
detail: response._data.detail || '',
|
|
53
|
-
category: ConnectErrorCategory.USER_INFO
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
} catch (e) {
|
|
58
|
-
console.error('Error fetching user info.', e)
|
|
59
|
-
// logFetchError(e, 'Error fetching user info.')
|
|
60
|
-
}
|
|
44
|
+
return $authApi<{ firstname: string, lastname: string }>(`/users/${identifier}`, {
|
|
45
|
+
parseResponse: JSON.parse
|
|
46
|
+
})
|
|
61
47
|
}
|
|
62
48
|
|
|
63
49
|
/** Update user information in AUTH with current token info */
|
|
64
50
|
async function updateAuthUserInfo(): Promise<void> {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
})
|
|
70
|
-
} catch (e) {
|
|
71
|
-
console.error('Error updating auth user info', e)
|
|
72
|
-
// logFetchError(e, 'Error updating auth user info')
|
|
73
|
-
}
|
|
51
|
+
await $authApi('/users', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
body: { isLogin: true }
|
|
54
|
+
})
|
|
74
55
|
}
|
|
75
56
|
|
|
76
57
|
/** Set user name information */
|
|
@@ -90,25 +71,9 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
|
|
|
90
71
|
if (!authUser.value?.keycloakGuid) {
|
|
91
72
|
return undefined
|
|
92
73
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
onResponseError({ response }) {
|
|
97
|
-
errors.value.push({
|
|
98
|
-
statusCode: response.status || 500,
|
|
99
|
-
message: response._data?.message || 'Error retrieving user accounts.',
|
|
100
|
-
detail: response._data.detail || '',
|
|
101
|
-
category: ConnectErrorCategory.ACCOUNT_LIST
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
return response?.filter(setting => setting.type === UserSettingsType.ACCOUNT) as ConnectAccount[]
|
|
107
|
-
} catch (e) {
|
|
108
|
-
console.error('Error retrieving user accounts', e)
|
|
109
|
-
// logFetchError(e, 'Error retrieving user accounts')
|
|
110
|
-
return undefined
|
|
111
|
-
}
|
|
74
|
+
// TODO: use orgs fetch instead to get branch name ? $authApi<UserSettings[]>('/users/orgs')
|
|
75
|
+
const response = await $authApi<ConnectUserSettings[]>(`/users/${authUser.value.keycloakGuid}/settings`)
|
|
76
|
+
return response?.filter(setting => setting.type === UserSettingsType.ACCOUNT) as ConnectAccount[]
|
|
112
77
|
}
|
|
113
78
|
|
|
114
79
|
/** Set the user account list and current account */
|
|
@@ -136,23 +101,8 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
|
|
|
136
101
|
if (!accountId || !keycloakGuid) {
|
|
137
102
|
return
|
|
138
103
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
onResponseError({ response }) {
|
|
142
|
-
errors.value.push({
|
|
143
|
-
statusCode: response.status || 500,
|
|
144
|
-
message: response._data.message || 'Error retrieving pending approvals.',
|
|
145
|
-
detail: response._data.detail || '',
|
|
146
|
-
category: ConnectErrorCategory.PENDING_APPROVAL_COUNT
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
pendingApprovalCount.value = response?.count || 0
|
|
152
|
-
} catch (e) {
|
|
153
|
-
console.error('Error retrieving pending approvals', e)
|
|
154
|
-
// logFetchError(e, 'Error retrieving pending approvals')
|
|
155
|
-
}
|
|
104
|
+
const response = await $authApi<{ count: number }>(`/users/${keycloakGuid}/org/${accountId}/notifications`)
|
|
105
|
+
pendingApprovalCount.value = response?.count || 0
|
|
156
106
|
}
|
|
157
107
|
|
|
158
108
|
async function checkAccountStatus() {
|
|
@@ -188,17 +138,21 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
|
|
|
188
138
|
}
|
|
189
139
|
|
|
190
140
|
async function initAccountStore(): Promise<void> {
|
|
191
|
-
|
|
192
|
-
setAccountInfo(),
|
|
193
|
-
updateAuthUserInfo(),
|
|
194
|
-
setUserName()
|
|
195
|
-
])
|
|
196
|
-
|
|
197
|
-
if (currentAccount.value.id) {
|
|
141
|
+
try {
|
|
198
142
|
await Promise.all([
|
|
199
|
-
|
|
200
|
-
|
|
143
|
+
setAccountInfo(),
|
|
144
|
+
updateAuthUserInfo(),
|
|
145
|
+
setUserName()
|
|
201
146
|
])
|
|
147
|
+
|
|
148
|
+
if (currentAccount.value.id) {
|
|
149
|
+
await Promise.all([
|
|
150
|
+
checkAccountStatus(),
|
|
151
|
+
getPendingApprovalCount()
|
|
152
|
+
])
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
logFetchError(e, '[Account Store] - Error during initialization')
|
|
202
156
|
}
|
|
203
157
|
}
|
|
204
158
|
|
|
@@ -207,7 +161,6 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
|
|
|
207
161
|
currentAccount.value = {} as ConnectAccount
|
|
208
162
|
userAccounts.value = []
|
|
209
163
|
pendingApprovalCount.value = 0
|
|
210
|
-
errors.value = []
|
|
211
164
|
userFirstName.value = user.value?.firstName || '-'
|
|
212
165
|
userLastName.value = user.value?.lastName || ''
|
|
213
166
|
}
|
|
@@ -217,7 +170,6 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
|
|
|
217
170
|
currentAccountName,
|
|
218
171
|
userAccounts,
|
|
219
172
|
pendingApprovalCount,
|
|
220
|
-
errors,
|
|
221
173
|
userFullName,
|
|
222
174
|
checkAccountStatus,
|
|
223
175
|
setUserName,
|
|
@@ -2,9 +2,12 @@ declare module '@nuxt/schema' {
|
|
|
2
2
|
interface AppConfigInput {
|
|
3
3
|
connect?: {
|
|
4
4
|
login?: {
|
|
5
|
-
|
|
5
|
+
redirect?: string
|
|
6
6
|
idps?: Array<'bcsc' | 'bceid' | 'idir'>
|
|
7
7
|
}
|
|
8
|
+
logout?: {
|
|
9
|
+
redirect?: string
|
|
10
|
+
}
|
|
8
11
|
header?: {
|
|
9
12
|
loginMenu?: boolean
|
|
10
13
|
createAccount?: boolean
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Pinia, Store } from 'pinia'
|
|
2
|
+
import { getActivePinia } from 'pinia'
|
|
3
|
+
|
|
4
|
+
interface ExtendedPinia extends Pinia {
|
|
5
|
+
_s: Map<string, Store>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resets specific Pinia stores if store IDs are specified.
|
|
10
|
+
* If no IDs are provided, resets all stores that have a `$reset` method.
|
|
11
|
+
* @param {string[]} storeIds - Array of store IDs to reset. If empty, all stores will be reset.
|
|
12
|
+
*/
|
|
13
|
+
export function resetPiniaStores(storeIds: string[] = []): void {
|
|
14
|
+
const pinia = getActivePinia() as ExtendedPinia
|
|
15
|
+
|
|
16
|
+
if (!pinia && import.meta.env.DEV === true) {
|
|
17
|
+
console.warn('No pinia stores found.')
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// null check still fails so must catch error instead
|
|
22
|
+
pinia._s.forEach((store) => {
|
|
23
|
+
if (storeIds.length === 0 || storeIds.includes(store.$id)) {
|
|
24
|
+
try {
|
|
25
|
+
store.$reset()
|
|
26
|
+
} catch {
|
|
27
|
+
if (import.meta.env.DEV === true) {
|
|
28
|
+
console.warn(`Store "${store.$id}" does not implement $reset. Skipping reset.`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// cant use await inside definePageMeta so must use separate util to set page meta if await required
|
|
2
|
+
/**
|
|
3
|
+
* Sets the `onBeforeSessionExpired` callback for the current route.
|
|
4
|
+
*
|
|
5
|
+
* This function allows you to provide a callback (`cb`) that will be called before the session expires.
|
|
6
|
+
* The callback can either be synchronous (returning `void`) or asynchronous (returning a `Promise`).
|
|
7
|
+
*
|
|
8
|
+
* @param {() => T | Promise<T>} cb - The callback function to execute before session expiry.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* setOnBeforeSessionExpired(() => {
|
|
12
|
+
* // Do something before session expiry
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* setOnBeforeSessionExpired(() => submitApplication());
|
|
17
|
+
*/
|
|
18
|
+
export function setOnBeforeSessionExpired<T>(cb: () => T | Promise<T>) {
|
|
19
|
+
const route = useRoute()
|
|
20
|
+
route.meta.onBeforeSessionExpired = async () => {
|
|
21
|
+
await cb()
|
|
22
|
+
}
|
|
23
|
+
}
|
package/i18n/locales/en-CA.ts
CHANGED
|
@@ -21,7 +21,30 @@ export default {
|
|
|
21
21
|
teamMembers: 'Team Members',
|
|
22
22
|
transactions: 'Transactions'
|
|
23
23
|
},
|
|
24
|
+
page: {
|
|
25
|
+
login: {
|
|
26
|
+
h1: 'SBC Connect Account Login',
|
|
27
|
+
title: 'Log in - SBC Connect',
|
|
28
|
+
loginBCSC: 'Login with BC Services Card',
|
|
29
|
+
loginBCEID: 'Login with BCeID',
|
|
30
|
+
loginIDIR: 'Login with IDIR',
|
|
31
|
+
sessionExpiredAlert: {
|
|
32
|
+
title: 'Session Expired',
|
|
33
|
+
description: 'Your session has expired. Please log in again to continue.'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
sessionExpiry: {
|
|
38
|
+
title: 'Session Expiring Soon',
|
|
39
|
+
content: 'Your session is about to expire due to inactivity. You will be logged out in {boldStart}0{boldEnd} seconds. Press any key to continue your session. | Your session is about to expire due to inactivity. You will be logged out in {boldStart}1{boldEnd} second. Press any key to continue your session. | Your session is about to expire due to inactivity. You will be logged out in {boldStart}{count}{boldEnd} seconds. Press any key to continue your session.',
|
|
40
|
+
sessionExpired: 'Session Expired',
|
|
41
|
+
continueBtn: {
|
|
42
|
+
main: 'Continue Session',
|
|
43
|
+
aria: 'Your session is about to expire, press any key to continue your session.'
|
|
44
|
+
}
|
|
45
|
+
},
|
|
24
46
|
text: {
|
|
47
|
+
imageAltGenericLogin: 'Generic Login Image',
|
|
25
48
|
notifications: {
|
|
26
49
|
none: 'No Notifications',
|
|
27
50
|
teamMemberApproval: '{count} team member requires approval to access this account. | {count} team members require approval to access this account.'
|
package/nuxt.config.ts
CHANGED
|
@@ -71,7 +71,11 @@ export default defineNuxtConfig({
|
|
|
71
71
|
authApiUrl: '',
|
|
72
72
|
authApiVersion: '',
|
|
73
73
|
xApiKey: '',
|
|
74
|
-
authWebUrl: ''
|
|
74
|
+
authWebUrl: '',
|
|
75
|
+
tokenRefreshInterval: '',
|
|
76
|
+
tokenMinValidity: '',
|
|
77
|
+
sessionInactivityTimeout: '',
|
|
78
|
+
sessionModalTimeout: ''
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
})
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sbc-connect/nuxt-auth",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.9",
|
|
5
5
|
"repository": "github:bcgov/connect-nuxt",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"nuxt": "^4.0.2",
|
|
13
13
|
"typescript": "^5.8.3",
|
|
14
14
|
"vue-tsc": "^3.0.4",
|
|
15
|
+
"@sbc-connect/playwright-config": "0.0.6",
|
|
15
16
|
"@sbc-connect/eslint-config": "0.0.6",
|
|
16
|
-
"@sbc-connect/playwright-config": "0.0.5",
|
|
17
17
|
"@sbc-connect/vitest-config": "0.0.6"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"keycloak-js": "^26.2.0",
|
|
22
22
|
"pinia": "^3.0.3",
|
|
23
23
|
"pinia-plugin-persistedstate": "^4.4.1",
|
|
24
|
-
"@sbc-connect/nuxt-base": "0.1.
|
|
24
|
+
"@sbc-connect/nuxt-base": "0.1.8"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"preinstall": "npx only-allow pnpm",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"lint": "eslint .",
|
|
34
34
|
"lint:fix": "eslint --fix .",
|
|
35
35
|
"test": "pnpm run test:unit; pnpm run test:e2e",
|
|
36
|
-
"test:unit": "vitest run",
|
|
36
|
+
"test:unit": "vitest run --passWithNoTests",
|
|
37
|
+
"test:unit:watch": "vitest",
|
|
37
38
|
"test:e2e": "npx playwright test",
|
|
38
39
|
"typecheck": "npx nuxt typecheck"
|
|
39
40
|
}
|
|
Binary file
|