@sbc-connect/nuxt-auth 0.1.32 → 0.2.1

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,37 @@
1
1
  # @sbc-connect/nuxt-auth
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`f5d34df`](https://github.com/bcgov/connect-nuxt/commit/f5d34df0bbb2b37cbeda8a9dd39c6bce098c564f)]:
8
+ - @sbc-connect/nuxt-base@0.4.0
9
+
10
+ ## 0.2.0
11
+
12
+ ### Minor Changes
13
+
14
+ - [#104](https://github.com/bcgov/connect-nuxt/pull/104) [`5f1b383`](https://github.com/bcgov/connect-nuxt/commit/5f1b3839ed993654a7f20fe161047ba38ab86274) Thanks [@deetz99](https://github.com/deetz99)! - - Terms of Use page
15
+
16
+ - check user terms of use in auth middleware
17
+ - useAuthApi composable using pinia-colada for api methods
18
+ - terms of use error/decline modals
19
+ - auth contact, auth profile and terms of use interfaces
20
+ - update account store to use pinia-colada query for setUserName method
21
+ - related i18n translations
22
+ - add '@pinia/colada-nuxt' nuxt module
23
+
24
+ - [#102](https://github.com/bcgov/connect-nuxt/pull/102) [`d714785`](https://github.com/bcgov/connect-nuxt/commit/d71478573f1fe11be34d5d588d68fb75eb5fd159) Thanks [@deetz99](https://github.com/deetz99)! - - allowed-idps middleware
25
+ - login-page middleware
26
+ - add explicit typing for allowed idps in app config
27
+ - util getValidIdps constant
28
+ - Choose existing account page (WIP)
29
+
30
+ ### Patch Changes
31
+
32
+ - Updated dependencies [[`d714785`](https://github.com/bcgov/connect-nuxt/commit/d71478573f1fe11be34d5d588d68fb75eb5fd159), [`5f1b383`](https://github.com/bcgov/connect-nuxt/commit/5f1b3839ed993654a7f20fe161047ba38ab86274)]:
33
+ - @sbc-connect/nuxt-base@0.3.0
34
+
3
35
  ## 0.1.32
4
36
 
5
37
  ### Patch Changes
package/app/app.config.ts CHANGED
@@ -1,8 +1,12 @@
1
+ import { ConnectIdpHint } from '#auth/app/enums/connect-idp-hint'
2
+
1
3
  export default defineAppConfig({
2
4
  connect: {
3
5
  login: {
4
- redirect: '',
5
- idps: ['bcsc', 'bceid', 'idir']
6
+ redirect: '/',
7
+ idps: [ConnectIdpHint.BCSC, ConnectIdpHint.BCEID, ConnectIdpHint.IDIR],
8
+ skipAccountRedirect: false
9
+ // idpEnforcement: 'strict' - future potentially
6
10
  },
7
11
  logout: {
8
12
  redirect: ''
@@ -0,0 +1,10 @@
1
+ <template>
2
+ <UAlert
3
+ color="warning"
4
+ variant="subtle"
5
+ >
6
+ <template #description>
7
+ <ConnectI18nHelper translation-path="connect.text.alertExistingAccountFound" />
8
+ </template>
9
+ </UAlert>
10
+ </template>
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ account: ConnectAccount
4
+ }>()
5
+
6
+ defineEmits<{
7
+ select: [id: number]
8
+ }>()
9
+ </script>
10
+
11
+ <template>
12
+ <li class="flex flex-col items-start justify-between gap-4 py-8 first:pt-0 last:pb-0 sm:flex-row sm:items-center">
13
+ <div
14
+ class="flex flex-row items-center gap-4 sm:gap-6"
15
+ :class="{
16
+ 'opacity-50': account.accountStatus !== AccountStatus.ACTIVE,
17
+ }"
18
+ >
19
+ <UAvatar
20
+ :alt="account.label[0]"
21
+ :ui="{
22
+ root: 'bg-blue-300 rounded-sm',
23
+ fallback: 'text-white font-bold text-xl',
24
+ }"
25
+ />
26
+ <div class="flex w-full flex-col text-left">
27
+ <span class="text-lg font-bold text-neutral-highlighted">
28
+ {{ account.label }}
29
+ </span>
30
+ </div>
31
+ </div>
32
+ <div class="flex w-full flex-col gap-4 sm:w-fit sm:flex-row">
33
+ <div class="my-auto flex gap-2">
34
+ <UBadge
35
+ v-if="account.accountStatus !== AccountStatus.ACTIVE"
36
+ :label="$t('badge.inactiveAccount')"
37
+ class="bg-[#fff7e3] px-3 text-center font-bold text-neutral"
38
+ />
39
+ </div>
40
+
41
+ <UButton
42
+ :label="$t('connect.label.useThisAccount')"
43
+ :aria-label="$t('connect.label.useThisAccountAria', { name: account.label })"
44
+ :icon="'i-mdi-chevron-right'"
45
+ trailing
46
+ :disabled="account.accountStatus !== AccountStatus.ACTIVE"
47
+ size="xl"
48
+ data-testid="choose-existing-account-button"
49
+ class="w-full justify-center sm:w-min sm:justify-normal"
50
+ @click="$emit('select', account.id)"
51
+ />
52
+ </div>
53
+ </li>
54
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ accounts: ConnectAccount[]
4
+ }>()
5
+
6
+ defineEmits<{
7
+ select: [id: number]
8
+ }>()
9
+ </script>
10
+
11
+ <template>
12
+ <section class="space-y-4">
13
+ <ConnectI18nHelper
14
+ as="h2"
15
+ class="font-normal"
16
+ translation-path="connect.label.yourExistingAccounts"
17
+ :count="accounts.length"
18
+ />
19
+ <ul
20
+ class="bg-white flex flex-col divide-y divide-line-muted rounded p-8 max-h-[50dvh] overflow-y-auto"
21
+ >
22
+ <ConnectAccountExistingListItem
23
+ v-for="account in accounts"
24
+ :key="account.id"
25
+ :account
26
+ @select="$emit('select', $event)"
27
+ />
28
+ </ul>
29
+ </section>
30
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ content?: string
4
+ }>()
5
+ </script>
6
+
7
+ <template>
8
+ <div
9
+ v-sanitize="content"
10
+ class="tos-content max-w-full wrap-break-word space-y-6 *:space-y-4"
11
+ />
12
+ </template>
13
+
14
+ <style scoped>
15
+ @reference "#connect-theme";
16
+
17
+ .tos-content :deep(header) {
18
+ @apply font-bold text-neutral-highlighted;
19
+ }
20
+
21
+ .tos-content :deep(ul) {
22
+ @apply space-y-1;
23
+ }
24
+
25
+ .tos-content :deep(ul li span:first-child) {
26
+ @apply mr-1;
27
+ }
28
+ </style>
@@ -0,0 +1,101 @@
1
+ <script setup lang="ts">
2
+ import { z } from 'zod'
3
+ import type { FormErrorEvent, Form } from '@nuxt/ui'
4
+
5
+ const { t } = useI18n()
6
+ const { openDeclineTosModal } = useConnectTosModals()
7
+
8
+ const props = defineProps<{
9
+ hasReachedBottom: boolean
10
+ disableButtons: boolean
11
+ loading: boolean
12
+ }>()
13
+
14
+ const state = reactive({
15
+ agreeToTerms: false
16
+ })
17
+
18
+ const formRef = useTemplateRef<Form<unknown>>('form-ref')
19
+
20
+ const schema = z.object({
21
+ agreeToTerms: z.boolean()
22
+ }).superRefine((val, ctx) => {
23
+ if (!val.agreeToTerms) {
24
+ ctx.addIssue({
25
+ code: 'custom',
26
+ message: t('connect.validation.acceptTermsOfUse'),
27
+ path: ['agreeToTerms']
28
+ })
29
+ } else if (!props.hasReachedBottom) {
30
+ ctx.addIssue({
31
+ code: 'custom',
32
+ message: t('connect.validation.termsOfUseScrollToBottom'),
33
+ path: ['agreeToTerms']
34
+ })
35
+ }
36
+ })
37
+
38
+ function onFormSubmitError(event: FormErrorEvent) {
39
+ if (event?.errors?.[0]?.id) {
40
+ const element = document.getElementById(event.errors[0].id)
41
+ if (element) {
42
+ setTimeout(() => {
43
+ element.focus({ preventScroll: true })
44
+ }, 0)
45
+ }
46
+ }
47
+ }
48
+
49
+ // reset form errors if user reaches bottom of page
50
+ watch(() => props.hasReachedBottom, (newVal) => {
51
+ if (newVal) {
52
+ formRef.value?.clear()
53
+ }
54
+ })
55
+ </script>
56
+
57
+ <template>
58
+ <UForm
59
+ ref="form-ref"
60
+ class="sticky bottom-0 flex w-full flex-col items-start justify-between
61
+ gap-4 border-t border-line bg-shade py-4 sm:flex-row sm:items-center sm:gap-0 sm:pb-8"
62
+ :state
63
+ :schema
64
+ @error="onFormSubmitError"
65
+ >
66
+ <UFormField
67
+ v-slot="{ error }"
68
+ name="agreeToTerms"
69
+ :ui="{
70
+ error: 'text-base',
71
+ }"
72
+ >
73
+ <UCheckbox
74
+ ref="checkboxRef"
75
+ v-model="state.agreeToTerms"
76
+ :label="$t('connect.text.iHaveReadAndAcceptTermsOfUse')"
77
+ :ui="{
78
+ label: error ? 'text-lg text-error' : 'text-lg',
79
+ }"
80
+ />
81
+ </UFormField>
82
+ <div class="flex w-full gap-4 sm:w-fit">
83
+ <UButton
84
+ class="flex-1 sm:flex-none"
85
+ :ui="{ base: 'flex justify-center items-center' }"
86
+ :label="$t('connect.label.accept')"
87
+ :disabled="disableButtons"
88
+ :loading
89
+ type="submit"
90
+ />
91
+ <UButton
92
+ class="flex-1 sm:flex-none"
93
+ :ui="{ base: 'flex justify-center items-center' }"
94
+ :label="$t('connect.label.decline')"
95
+ variant="outline"
96
+ :disabled="disableButtons"
97
+ @click="openDeclineTosModal"
98
+ />
99
+ </div>
100
+ </UForm>
101
+ </template>
@@ -0,0 +1,60 @@
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
+ async function getTermsOfUse() {
17
+ const query = defineQuery({
18
+ key: ['auth-terms-of-use'],
19
+ query: () => $authApi<ConnectTermsOfUse>('/documents/termsofuse'),
20
+ staleTime: 300000
21
+ })
22
+ return query()
23
+ }
24
+
25
+ const usePatchTermsOfUse = defineMutation(() => {
26
+ const { mutateAsync, ...mutation } = useMutation({
27
+ mutation: (vars: { accepted: boolean, version: string, successCb?: () => Promise<unknown> }) => {
28
+ return $authApi<ConnectAuthProfile>('/users/@me', {
29
+ method: 'PATCH',
30
+ body: {
31
+ istermsaccepted: vars.accepted,
32
+ termsversion: vars.version
33
+ }
34
+ })
35
+ },
36
+ onError: (error) => {
37
+ // TODO: FUTURE - add api error message to modal content - remove console.error
38
+ console.error('ERROR: ', error)
39
+ useConnectTosModals().openPatchTosErrorModal()
40
+ },
41
+ onSuccess: async (_, _vars) => {
42
+ await queryCache.invalidateQueries({ key: ['auth-user-profile'], exact: true })
43
+ if (_vars.successCb) {
44
+ await _vars.successCb()
45
+ }
46
+ }
47
+ })
48
+
49
+ return {
50
+ ...mutation,
51
+ patchTermsOfUse: mutateAsync
52
+ }
53
+ })
54
+
55
+ return {
56
+ getAuthUserProfile,
57
+ getTermsOfUse,
58
+ usePatchTermsOfUse
59
+ }
60
+ }
@@ -0,0 +1,39 @@
1
+ import type { RouteLocationNormalizedGeneric } from '#vue-router'
2
+
3
+ export const useConnectAccountFlowRedirect = () => {
4
+ function finalRedirect(route: RouteLocationNormalizedGeneric) {
5
+ const localePath = useLocalePath()
6
+ const ac = useAppConfig().connect.login
7
+ const externalRedirectUrl = route.query.return as string | undefined
8
+ const internalRedirectUrl = ac.redirect
9
+
10
+ const query = { ...route.query }
11
+
12
+ if (query.return) {
13
+ delete query.return
14
+ }
15
+
16
+ if (query.allowedIdps) {
17
+ delete query.allowedIdps
18
+ }
19
+
20
+ if (externalRedirectUrl) {
21
+ return navigateTo(
22
+ {
23
+ path: appendUrlParam(externalRedirectUrl, 'accountid', useConnectAccountStore().currentAccount.id),
24
+ query
25
+ },
26
+ { external: true }
27
+ )
28
+ } else {
29
+ return navigateTo({
30
+ path: localePath(internalRedirectUrl),
31
+ query
32
+ })
33
+ }
34
+ }
35
+
36
+ return {
37
+ finalRedirect
38
+ }
39
+ }
@@ -0,0 +1,46 @@
1
+ export const useConnectTosModals = () => {
2
+ const { baseModal } = useConnectModal()
3
+ const { logout } = useConnectAuth()
4
+ const t = useNuxtApp().$i18n.t
5
+
6
+ function openDeclineTosModal() {
7
+ baseModal.open({
8
+ title: `${t('connect.label.declineTermsOfUse')}?`,
9
+ description: t('connect.text.declineTOSCantAccessService'),
10
+ dismissible: true,
11
+ buttons: [
12
+ {
13
+ label: t('connect.label.declineTermsOfUse'),
14
+ onClick: async () => await logout()
15
+ },
16
+ {
17
+ label: t('connect.label.cancel'),
18
+ shouldClose: true,
19
+ variant: 'outline'
20
+ }
21
+ ]
22
+ })
23
+ }
24
+
25
+ // TODO: better error text
26
+ function openPatchTosErrorModal() {
27
+ baseModal.open({
28
+ title: "Can't update TOS at this time",
29
+ description: 'Please try again later',
30
+ dismissible: true,
31
+ buttons: [
32
+ {
33
+ label: t('connect.label.close'),
34
+ shouldClose: true
35
+ }
36
+ ]
37
+ // contact info ???
38
+ // include api error message?
39
+ })
40
+ }
41
+
42
+ return {
43
+ openDeclineTosModal,
44
+ openPatchTosErrorModal
45
+ }
46
+ }
@@ -0,0 +1,15 @@
1
+ export interface ConnectAuthContact {
2
+ email: string
3
+ phone: string
4
+ phoneExtension: string
5
+ city?: string
6
+ country?: string
7
+ street?: string
8
+ streetAdditional?: string
9
+ postalCode?: string
10
+ region?: string
11
+ }
12
+
13
+ export interface ConnectAuthContacts {
14
+ contacts: ConnectAuthContact[]
15
+ }
@@ -0,0 +1,24 @@
1
+ export interface ConnectAuthProfile {
2
+ contacts: ConnectAuthContact[]
3
+ created: ApiDateTimeUtc
4
+ id: number
5
+ idpUserid: string
6
+ keycloakGuid: string
7
+ lastname: string
8
+ firstname?: string
9
+ loginSource: ConnectLoginSource
10
+ loginTime: ApiDateTimeUtc
11
+ modified: ApiDateTimeUtc
12
+ modifiedBy: string
13
+ userStatus: number
14
+ type: string // PUBLIC_USER - // TODO: get enum?
15
+ userTerms: ConnectUserTerms
16
+ username: string
17
+ verified: boolean
18
+ version: number
19
+ }
20
+
21
+ export interface ConnectUserTerms {
22
+ isTermsOfUseAccepted: boolean
23
+ termsOfUseAcceptedVersion: string
24
+ }
@@ -0,0 +1,6 @@
1
+ export interface ConnectTermsOfUse {
2
+ content: string // html
3
+ contentType: 'text/html'
4
+ type: 'termsofuse'
5
+ versionId: string
6
+ }
@@ -0,0 +1,20 @@
1
+ export default defineNuxtRouteMiddleware((to) => {
2
+ const ac = useAppConfig().connect.login
3
+ const validIdps = getValidIdps()
4
+ const allowedIdps = to.query.allowedIdps as string | undefined
5
+
6
+ if (allowedIdps) {
7
+ const idpArray = allowedIdps
8
+ .split(',')
9
+ .filter(idp =>
10
+ validIdps.includes(idp as ConnectValidIdpOption)
11
+ ) as ConnectValidIdps
12
+
13
+ if (idpArray.length) {
14
+ // updateAppConfig util doesn't seem to be updating correctly
15
+ // https://nuxt.com/docs/4.x/api/utils/update-app-config
16
+ // assign directly
17
+ ac.idps = idpArray
18
+ }
19
+ }
20
+ })
@@ -1,12 +1,27 @@
1
- export default defineNuxtRouteMiddleware((to) => {
1
+ export default defineNuxtRouteMiddleware(async (to) => {
2
2
  const { isAuthenticated } = useConnectAuth()
3
3
  const rtc = useRuntimeConfig().public
4
4
  const localePath = useLocalePath()
5
+ const authApi = useAuthApi()
6
+ const { finalRedirect } = useConnectAccountFlowRedirect()
5
7
 
6
8
  if (!isAuthenticated.value && !rtc.playwright) {
7
9
  return navigateTo(localePath(`/auth/login?return=${rtc.baseUrl}${to.fullPath.slice(1)}`))
8
10
  }
9
11
 
12
+ if (isAuthenticated.value) {
13
+ const { data, refresh } = await authApi.getAuthUserProfile()
14
+ await refresh()
15
+ const hasAccepted = data.value?.userTerms.isTermsOfUseAccepted
16
+ const isTosPage = to.meta.connectTosPage === true
17
+ if (!hasAccepted && !isTosPage) {
18
+ const query = { ...to.query }
19
+ return navigateTo({ path: localePath('/auth/terms-of-use'), query })
20
+ } else if (hasAccepted && isTosPage) {
21
+ return finalRedirect(to)
22
+ }
23
+ }
24
+
10
25
  if (rtc.playwright) {
11
26
  const { $connectAuth } = useNuxtApp()
12
27
  const { currentAccount } = storeToRefs(useConnectAccountStore())
@@ -0,0 +1,26 @@
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
+ })
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ definePageMeta({
3
+ layout: 'connect-auth',
4
+ alias: ['/auth/account/create'],
5
+ middleware: 'connect-auth'
6
+ })
7
+
8
+ const rtc = useRuntimeConfig().public
9
+ const accountStore = useConnectAccountStore()
10
+ const { authUser } = useConnectAuth()
11
+ const { finalRedirect } = useConnectAccountFlowRedirect()
12
+
13
+ const addNew = ref(false)
14
+
15
+ function selectAndRedirect(id: number) {
16
+ accountStore.switchCurrentAccount(id)
17
+ finalRedirect(useRoute())
18
+ }
19
+
20
+ onBeforeMount(() => {
21
+ if (accountStore.userAccounts.length === 0 && authUser.value.loginSource === ConnectLoginSource.BCSC) {
22
+ addNew.value = true
23
+ }
24
+ })
25
+ </script>
26
+
27
+ <template>
28
+ <UContainer class="max-w-6xl">
29
+ <ConnectTransitionFade>
30
+ <div class="space-y-6 sm:space-y-10">
31
+ <h1>{{ !addNew ? $t('connect.label.existingAccountFound') : $t('connect.label.sbcAccountCreation') }}</h1>
32
+ <ConnectAccountExistingAlert v-if="!addNew" />
33
+ </div>
34
+ </ConnectTransitionFade>
35
+
36
+ <ConnectTransitionFade>
37
+ <ConnectAccountExistingList
38
+ v-if="addNew === false"
39
+ :accounts="accountStore.userAccounts"
40
+ @select="selectAndRedirect"
41
+ />
42
+
43
+ <div v-else class="h-[66dvh] bg-white rounded border-2 border-black flex items-center justify-center text-3xl">
44
+ Create Account Form Here
45
+ </div>
46
+ </ConnectTransitionFade>
47
+
48
+ <div class="flex justify-center">
49
+ <UButton
50
+ v-if="authUser.loginSource === ConnectLoginSource.BCSC"
51
+ variant="outline"
52
+ :label="$t('connect.label.createNewAccount')"
53
+ icon="i-mdi-chevron-right"
54
+ trailing
55
+ size="xl"
56
+ class="w-full justify-center sm:w-min sm:justify-normal"
57
+ @click="addNew = !addNew"
58
+ />
59
+ <UButton
60
+ v-else
61
+ variant="outline"
62
+ :label="$t('connect.label.createNewAccount')"
63
+ icon="i-mdi-chevron-right"
64
+ trailing
65
+ size="xl"
66
+ class="w-full justify-center sm:w-min sm:justify-normal"
67
+ external
68
+ :to="rtc.authWebUrl + 'setup-account'"
69
+ />
70
+ </div>
71
+ </UContainer>
72
+ </template>
@@ -1,11 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import loginImage from '#auth/public/img/BCReg_Generic_Login_image.jpg'
3
3
 
4
- const { t, locale } = useI18n()
4
+ const { t } = useI18n()
5
5
  const { login } = useConnectAuth()
6
- const rtc = useRuntimeConfig().public
7
- const ac = useAppConfig().connect
8
- const route = useRoute()
6
+ const ac = useAppConfig().connect.login
9
7
 
10
8
  useHead({
11
9
  title: t('connect.page.login.title')
@@ -14,48 +12,34 @@ useHead({
14
12
  definePageMeta({
15
13
  layout: 'connect-auth',
16
14
  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
- }
15
+ middleware: 'connect-login-page'
27
16
  })
28
17
 
29
18
  const isSessionExpired = sessionStorage.getItem(ConnectAuthStorageKey.CONNECT_SESSION_EXPIRED)
30
19
 
31
20
  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
21
  const loginOptionsMap: Record<
38
- 'bcsc' | 'bceid' | 'idir',
22
+ ConnectValidIdpOption,
39
23
  { label: string, icon: string, onClick: () => Promise<void> }
40
24
  > = {
41
25
  bcsc: {
42
26
  label: t('connect.page.login.loginBCSC'),
43
27
  icon: 'i-mdi-account-card-details-outline',
44
- onClick: () => login(ConnectIdpHint.BCSC, redirectUrl)
28
+ onClick: () => login(ConnectIdpHint.BCSC)
45
29
  },
46
30
  bceid: {
47
31
  label: t('connect.page.login.loginBCEID'),
48
32
  icon: 'i-mdi-two-factor-authentication',
49
- onClick: () => login(ConnectIdpHint.BCEID, redirectUrl)
33
+ onClick: () => login(ConnectIdpHint.BCEID)
50
34
  },
51
35
  idir: {
52
36
  label: t('connect.page.login.loginIDIR'),
53
37
  icon: 'i-mdi-account-group-outline',
54
- onClick: () => login(ConnectIdpHint.IDIR, redirectUrl)
38
+ onClick: () => login(ConnectIdpHint.IDIR)
55
39
  }
56
40
  }
57
41
 
58
- return ac.login.idps.map(key => loginOptionsMap[key as keyof typeof loginOptionsMap])
42
+ return ac.idps.map(key => loginOptionsMap[key as keyof typeof loginOptionsMap])
59
43
  })
60
44
  </script>
61
45
 
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ import { ConnectTermsOfUseContent, ConnectTermsOfUseForm } from '#components'
3
+
4
+ const { t } = useI18n()
5
+ const authApi = useAuthApi()
6
+ const { finalRedirect } = useConnectAccountFlowRedirect()
7
+
8
+ definePageMeta({
9
+ layout: 'connect-auth',
10
+ hideBreadcrumbs: true,
11
+ middleware: 'connect-auth',
12
+ connectTosPage: true
13
+ })
14
+
15
+ useHead({
16
+ title: t('connect.page.termsOfUse.title')
17
+ })
18
+
19
+ const { data, status } = await authApi.getTermsOfUse()
20
+ const { patchTermsOfUse, isLoading } = authApi.usePatchTermsOfUse()
21
+
22
+ const formRef = useTemplateRef<InstanceType<typeof ConnectTermsOfUseForm>>('form-ref')
23
+ const contentRef = useTemplateRef<InstanceType<typeof ConnectTermsOfUseContent>>('content-ref')
24
+
25
+ // track if user has scrolled to bottom of page
26
+ const { bottom: tosBottom } = useElementBounding(contentRef)
27
+ const { top: formTop } = useElementBounding(formRef)
28
+ const hasReachedBottom = computed(() => formTop.value >= tosBottom.value)
29
+
30
+ const disableButtons = computed<boolean>(() => {
31
+ return isLoading.value || status.value === 'pending' || status.value === 'error' || !data.value?.content
32
+ })
33
+
34
+ // TODO: - FUTURE - add help/contact info to alert?
35
+ </script>
36
+
37
+ <template>
38
+ <UContainer
39
+ class="max-w-6xl relative grow flex flex-col"
40
+ >
41
+ <h1 class="sticky top-0 w-full border-b border-line bg-shade pb-2 pt-4 text-center z-10 inset-x-0">
42
+ {{ $t('connect.page.termsOfUse.h1') }}
43
+ </h1>
44
+
45
+ <div class="relative grow flex flex-col justify-center">
46
+ <ConnectSpinner v-if="status === 'pending'" />
47
+
48
+ <UAlert
49
+ v-else-if="status === 'error'"
50
+ icon="i-mdi-alert"
51
+ :title="$t('connect.text.alertUnableToLoadTermsOfUse')"
52
+ color="error"
53
+ variant="subtle"
54
+ :close-button="null"
55
+ :ui="{ title: 'text-base' }"
56
+ />
57
+
58
+ <ConnectTermsOfUseContent
59
+ v-else
60
+ ref="content-ref"
61
+ :content="data?.content"
62
+ />
63
+ </div>
64
+
65
+ <ConnectTermsOfUseForm
66
+ ref="form-ref"
67
+ :disable-buttons="disableButtons"
68
+ :has-reached-bottom="hasReachedBottom"
69
+ :loading="isLoading"
70
+ @submit="patchTermsOfUse({
71
+ accepted: true,
72
+ version: data!.versionId,
73
+ successCb: async () => await finalRedirect(useRoute()),
74
+ })"
75
+ />
76
+ </UContainer>
77
+ </template>
@@ -1,5 +1,6 @@
1
1
  export const useConnectAccountStore = defineStore('connect-auth-account-store', () => {
2
2
  const { $authApi } = useNuxtApp()
3
+ const authApi = useAuthApi()
3
4
  const rtc = useRuntimeConfig().public
4
5
  const { authUser } = useConnectAuth()
5
6
  // selected user account
@@ -39,13 +40,6 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
39
40
  return accountId === currentAccount.value.id
40
41
  }
41
42
 
42
- /** Get user information from AUTH */
43
- async function getAuthUserProfile(identifier: string): Promise<{ firstname: string, lastname: string } | undefined> {
44
- return $authApi<{ firstname: string, lastname: string }>(`/users/${identifier}`, {
45
- parseResponse: JSON.parse
46
- })
47
- }
48
-
49
43
  /** Update user information in AUTH with current token info */
50
44
  async function updateAuthUserInfo(): Promise<void> {
51
45
  await $authApi('/users', {
@@ -56,10 +50,11 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
56
50
 
57
51
  /** Set user name information */
58
52
  async function setUserName() {
59
- const authUserInfo = await getAuthUserProfile('@me')
60
- if (authUserInfo?.firstname && authUserInfo?.lastname) {
61
- userFirstName.value = authUserInfo.firstname
62
- userLastName.value = authUserInfo.lastname
53
+ const { data, refresh } = await authApi.getAuthUserProfile()
54
+ await refresh()
55
+ if (data.value?.firstname && data.value?.lastname) {
56
+ userFirstName.value = data.value.firstname
57
+ userLastName.value = data.value.lastname
63
58
  return
64
59
  }
65
60
  userFirstName.value = user.value?.firstName || '-'
@@ -175,7 +170,6 @@ export const useConnectAccountStore = defineStore('connect-auth-account-store',
175
170
  setUserName,
176
171
  hasRoles,
177
172
  isCurrentAccount,
178
- getAuthUserProfile,
179
173
  setAccountInfo,
180
174
  getUserAccounts,
181
175
  switchCurrentAccount,
@@ -3,7 +3,9 @@ declare module '@nuxt/schema' {
3
3
  connect?: {
4
4
  login?: {
5
5
  redirect?: string
6
- idps?: Array<'bcsc' | 'bceid' | 'idir'>
6
+ idps?: ConnectValidIdps
7
+ skipAccountRedirect?: boolean
8
+ // idpEnforcement: 'strict' | 'none' - future potentially
7
9
  }
8
10
  logout?: {
9
11
  redirect?: string
@@ -0,0 +1,5 @@
1
+ export type ConnectValidIdpOption = ConnectIdpHint.BCSC
2
+ | ConnectIdpHint.BCEID
3
+ | ConnectIdpHint.IDIR
4
+
5
+ export type ConnectValidIdps = ConnectValidIdpOption[]
@@ -0,0 +1 @@
1
+ export * from './valid-idps'
@@ -0,0 +1,7 @@
1
+ export function getValidIdps(): ConnectValidIdps {
2
+ return [
3
+ ConnectIdpHint.BCSC,
4
+ ConnectIdpHint.BCEID,
5
+ ConnectIdpHint.IDIR
6
+ ]
7
+ }
@@ -0,0 +1 @@
1
+ export * from './constants'
@@ -3,23 +3,32 @@ export default {
3
3
  /* Ordering should be alphabetical unless otherwise specified */
4
4
  connect: {
5
5
  label: {
6
+ accept: 'Accept',
6
7
  accountInfo: 'Account Info',
7
8
  accountOptionsMenu: 'Account Options Menu',
8
9
  accountSettings: 'Account Settings',
9
10
  bceid: 'BCeID',
10
11
  bcsc: 'BC Services Card',
11
12
  createAccount: 'Create Account',
13
+ createNewAccount: 'Create New Account',
14
+ decline: 'Decline',
15
+ declineTermsOfUse: 'Decline Terms of Use',
12
16
  editProfile: 'Edit Profile',
17
+ existingAccountFound: 'Existing Account Found',
13
18
  idir: 'IDIR',
14
19
  logout: 'Log out',
15
20
  login: 'Log in',
16
21
  mainMenu: 'Main Menu',
17
22
  notifications: 'Notifications',
18
23
  notificationsAria: 'Notifications, {count} unread',
24
+ sbcAccountCreation: 'Service BC Account Creation',
19
25
  selectLoginMethod: 'Select log in method',
20
26
  switchAccount: 'Switch Account',
21
27
  teamMembers: 'Team Members',
22
- transactions: 'Transactions'
28
+ transactions: 'Transactions',
29
+ useThisAccount: 'Use this Account',
30
+ useThisAccountAria: 'Use this Account, {name}',
31
+ yourExistingAccounts: '{boldStart}Your Existing Accounts{boldEnd} ({count})'
23
32
  },
24
33
  page: {
25
34
  login: {
@@ -32,6 +41,10 @@ export default {
32
41
  title: 'Session Expired',
33
42
  description: 'Your session has expired. Please log in again to continue.'
34
43
  }
44
+ },
45
+ termsOfUse: {
46
+ h1: 'Terms of Use',
47
+ title: 'Terms of Use - Service BC Connect'
35
48
  }
36
49
  },
37
50
  sessionExpiry: {
@@ -44,11 +57,19 @@ export default {
44
57
  }
45
58
  },
46
59
  text: {
60
+ alertExistingAccountFound: '{boldStart}Note:{boldEnd} It looks like you already have an account with Service BC Connect. You can use an existing account to proceed or create a new one.',
61
+ alertUnableToLoadTermsOfUse: 'Unable to load Terms of Use, please try again later.',
62
+ declineTOSCantAccessService: 'By declining the Terms of Use, you won’t be able to access this service. Do you wish to proceed?',
63
+ iHaveReadAndAcceptTermsOfUse: 'I have read and accept the Terms of Use',
47
64
  imageAltGenericLogin: 'Generic Login Image',
48
65
  notifications: {
49
66
  none: 'No Notifications',
50
67
  teamMemberApproval: '{count} team member requires approval to access this account. | {count} team members require approval to access this account.'
51
68
  }
69
+ },
70
+ validation: {
71
+ acceptTermsOfUse: 'Please accept the Terms of Use.',
72
+ termsOfUseScrollToBottom: 'Please scroll to the bottom of the page to accept the Terms of Use.'
52
73
  }
53
74
  }
54
75
  }
package/nuxt.config.ts CHANGED
@@ -18,7 +18,8 @@ export default defineNuxtConfig({
18
18
 
19
19
  modules: [
20
20
  '@pinia/nuxt',
21
- 'pinia-plugin-persistedstate/nuxt'
21
+ 'pinia-plugin-persistedstate/nuxt',
22
+ '@pinia/colada-nuxt'
22
23
  ],
23
24
 
24
25
  alias: {
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.32",
4
+ "version": "0.2.1",
5
5
  "repository": "github:bcgov/connect-nuxt",
6
6
  "license": "BSD-3-Clause",
7
7
  "main": "./nuxt.config.ts",
@@ -17,11 +17,13 @@
17
17
  "@sbc-connect/vitest-config": "0.0.6"
18
18
  },
19
19
  "dependencies": {
20
+ "@pinia/colada": "^0.17.9",
21
+ "@pinia/colada-nuxt": "^0.2.4",
20
22
  "@pinia/nuxt": "^0.11.2",
21
23
  "keycloak-js": "^26.2.1",
22
24
  "pinia": "^3.0.3",
23
25
  "pinia-plugin-persistedstate": "^4.5.0",
24
- "@sbc-connect/nuxt-base": "0.2.0"
26
+ "@sbc-connect/nuxt-base": "0.4.0"
25
27
  },
26
28
  "scripts": {
27
29
  "preinstall": "npx only-allow pnpm",