From b4ba2904213ca526cb94ea996e7bfa415d5e84c6 Mon Sep 17 00:00:00 2001 From: Kial Jinnah Date: Thu, 3 Sep 2026 10:39:20 -0400 Subject: [PATCH] 34765 login enhancement Signed-off-by: Kial Jinnah --- app/.env.example | 1 + app/devops/vaults.env | 1 + .../existing-request-display.vue | 10 ++-- app/src/main.ts | 37 ++++++++++++++- app/src/mixins/nr-affiliation-mixin.ts | 10 ++-- app/src/plugins/authHelper.ts | 9 ++++ app/src/plugins/getConfig.ts | 3 ++ app/src/plugins/utilities.ts | 11 +++++ app/src/services/auth-services.ts | 13 +++++ app/src/views/auth/Signin.vue | 47 ++++++------------- .../dialogs/numbered-company-help.spec.ts | 7 +-- 11 files changed, 100 insertions(+), 49 deletions(-) diff --git a/app/.env.example b/app/.env.example index 6fc434521..7f69ce4c9 100644 --- a/app/.env.example +++ b/app/.env.example @@ -5,6 +5,7 @@ VUE_APP_REGISTRY_HOME_URL="https://dev.bcregistry.gov.bc.ca/" VUE_APP_AUTH_WEB_URL="https://dev.account.bcregistry.gov.bc.ca/" VUE_APP_CORPORATE_ONLINE_URL="https://www.corporateonline.gov.bc.ca" VUE_APP_BUSINESS_DASH_URL="https://dev.business-dashboard.bcregistry.gov.bc.ca/" +VUE_APP_BUSINESS_HOME_URL="https://dev.home.business.bcregistry.gov.bc.ca/" VUE_APP_BUSINESS_REGISTRY_URL="https://dev.business-registry-dashboard.bcregistry.gov.bc.ca/" VUE_APP_ENTITY_SELECTOR_URL="https://entity-selection-dev.apps.silver.devops.gov.bc.ca/" VUE_APP_PAYMENT_PORTAL_URL="https://dev.account.bcregistry.gov.bc.ca/makepayment/" diff --git a/app/devops/vaults.env b/app/devops/vaults.env index 93771b4c6..9b3e12093 100644 --- a/app/devops/vaults.env +++ b/app/devops/vaults.env @@ -6,6 +6,7 @@ VUE_APP_REGISTRY_HOME_URL="op://web-url/$APP_ENV/registry/REGISTRY_HOME_URL" VUE_APP_AUTH_WEB_URL="op://web-url/$APP_ENV/auth-web/AUTH_WEB_URL" VUE_APP_CORPORATE_ONLINE_URL="op://web-url/$APP_ENV/bcregistry/COLIN_URL" VUE_APP_BUSINESS_DASH_URL="op://web-url/$APP_ENV/business-dash/BUSINESS_DASH_URL" +VUE_APP_BUSINESS_HOME_URL="op://web-url/$APP_ENV/business/REGISTRY_HOME_URL" VUE_APP_BUSINESS_REGISTRY_URL="op://web-url/$APP_ENV/business-registry-ui/BUSINESS_REGISTRY_URL" VUE_APP_ENTITY_SELECTOR_URL="op://web-url/$APP_ENV/entity-selector/ENTITY_SELECTOR_URL" VUE_APP_PAYMENT_PORTAL_URL="op://web-url/$APP_ENV/pay/PAYMENT_PORTAL_URL" diff --git a/app/src/components/existing-request/existing-request-display.vue b/app/src/components/existing-request/existing-request-display.vue index ba2bac2a1..903a2a449 100644 --- a/app/src/components/existing-request/existing-request-display.vue +++ b/app/src/components/existing-request/existing-request-display.vue @@ -342,7 +342,7 @@ import NrApprovedGrayBox from './nr-approved-gray-box.vue' import NrNotApprovedGrayBox from './nr-not-approved-gray-box.vue' import { NameState, NrAction, NrState, PaymentStatus, SbcPaymentStatus, PaymentAction, Furnished } from '@/enums' -import { Sleep, GetFeatureFlag, Navigate } from '@/plugins' +import { Sleep, GetFeatureFlag, getBusinessHomeLoginUrl, Navigate } from '@/plugins' import NamexServices from '@/services/namex-services' import ContactInfo from '@/components/common/contact-info.vue' import { ActionBindingIF } from '@/interfaces/store-interfaces' @@ -761,12 +761,10 @@ export default class ExistingRequestDisplay extends Mixins( // Use the new "magic link routes" in the BRD to perform the affiliations and draft creations. Navigate(this.magicLink(this.nr)) } else { - // persist NR in session for affiliation upon authentication via Signin component + // persist NR in session for affiliation in App.vue after authentication sessionStorage.setItem('NR_DATA', JSON.stringify(this.nr)) - // navigate to BC Registry login page with return parameter - const registryHomeUrl = sessionStorage.getItem('REGISTRY_HOME_URL') - const nameRequestUrl = `${window.location.origin}` - Navigate(`${registryHomeUrl}login?return=${nameRequestUrl}`) + // navigate to Business Home login page, returning to the current page after login + Navigate(getBusinessHomeLoginUrl()) } } diff --git a/app/src/main.ts b/app/src/main.ts index 3797a89ce..120e3afd5 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -1,8 +1,9 @@ import Vue from 'vue' import App from './App.vue' import { getVueRouter } from '@/router' -import { getConfig, getVuetify, InitLdClient, isSigningIn, isSigningOut, getPiniaStore, getVuexStore } - from '@/plugins' +import { getConfig, getKeycloakGuid, getVuetify, InitLdClient, isSigningIn, isSigningOut, getPiniaStore, + getVuexStore } from '@/plugins' +import AuthServices from '@/services/auth-services' import KeycloakService from 'sbc-common-components/src/services/keycloak.services' import { SessionStorageKeys } from 'sbc-common-components/src/util/constants' import ConfigHelper from 'sbc-common-components/src/util/config-helper' @@ -43,6 +44,9 @@ async function startVue () { // Initialize Keycloak / sync SSO await syncSession() + // Seed the current account before the app mounts + await syncCurrentAccount() + // Initialize Launch Darkly if (window['ldClientId']) { console.info('Initializing Launch Darkly...') // eslint-disable-line no-console @@ -87,6 +91,35 @@ async function syncSession () { } } +/** + * Seeds the current account in session storage before the app mounts, so that code + * which reads CURRENT_ACCOUNT on startup (eg, the NR replay logic in App.vue) doesn't + * race SbcHeader's asynchronous account sync. Honours the "accountid" query param + * appended to the return URL by the Business Home login page. + */ +async function syncCurrentAccount (): Promise { + const token = ConfigHelper.getFromSession(SessionStorageKeys.KeyCloakToken) + if (!token) return + + const urlAccountId = new URLSearchParams(window.location.search).get('accountid') + const storedAccountId = JSON.parse( + ConfigHelper.getFromSession(SessionStorageKeys.CurrentAccount) || '{}' + )?.id + // nothing to do if an account is already stored and the URL doesn't specify a different one + if (storedAccountId && (!urlAccountId || String(storedAccountId) === urlAccountId)) return + + await AuthServices.fetchUserSettings(getKeycloakGuid()).then(settings => { + const accounts = settings?.filter(setting => setting.type === 'ACCOUNT') || [] + const account = accounts.find(acct => String(acct.id) === urlAccountId) || accounts[0] + if (account) { + ConfigHelper.addToSession(SessionStorageKeys.CurrentAccount, JSON.stringify(account)) + } + }).catch(error => { + // don't block app startup - SbcHeader will sync the account when it mounts + console.error('syncCurrentAccount =', error) // eslint-disable-line no-console + }) +} + // NB: the .then() makes sure linter doesn't pick up on an un-awaited promise startVue().then().catch(error => { console.error('main =', error) // eslint-disable-line no-console diff --git a/app/src/mixins/nr-affiliation-mixin.ts b/app/src/mixins/nr-affiliation-mixin.ts index 17685cc05..5bf345fda 100644 --- a/app/src/mixins/nr-affiliation-mixin.ts +++ b/app/src/mixins/nr-affiliation-mixin.ts @@ -5,7 +5,7 @@ import AuthServices from '@/services/auth-services' import BusinessServices from '@/services/business-services' import { BusinessRequest, NameRequestI } from '@/interfaces' import { ActionBindingIF } from '@/interfaces/store-interfaces' -import { Navigate } from '@/plugins' +import { getBusinessHomeLoginUrl, Navigate } from '@/plugins' import { CommonMixin } from '@/mixins' import { EntityTypes, NrAffiliationErrors } from '@/enums' import { CREATED, BAD_REQUEST } from 'http-status-codes' @@ -235,13 +235,11 @@ export class NrAffiliationMixin extends Mixins(CommonMixin) { await this.actionNumberedEntity(legalType) } } else { - // persist legal type and request type of the action in session upon authentication via Signin component + // persist legal type and request type of the action in session, replayed in App.vue after authentication sessionStorage.setItem('LEGAL_TYPE', legalType) sessionStorage.setItem('REQUEST_ACTION_CD', this.getRequestActionCd) - // navigate to BC Registry login page with return parameter - const registryHomeUrl = sessionStorage.getItem('REGISTRY_HOME_URL') - const nameRequestUrl = `${window.location.origin}` - Navigate(`${registryHomeUrl}login?return=${nameRequestUrl}`) + // navigate to Business Home login page, returning to the current page after login + Navigate(getBusinessHomeLoginUrl()) } } diff --git a/app/src/plugins/authHelper.ts b/app/src/plugins/authHelper.ts index d8d409c95..525f4c063 100644 --- a/app/src/plugins/authHelper.ts +++ b/app/src/plugins/authHelper.ts @@ -22,6 +22,15 @@ function parseToken (token: string): any { } } +/** Gets Keycloak GUID (the "sub" claim) from JWT. */ +export function getKeycloakGuid (): string { + const jwt = getJWT() + if (jwt.sub) { + return jwt.sub + } + throw new Error('Error getting Keycloak GUID') +} + /** Gets Keycloak roles from JWT. */ export function getKeycloakRoles (): Array { const jwt = getJWT() diff --git a/app/src/plugins/getConfig.ts b/app/src/plugins/getConfig.ts index 0bf1b0002..e482adfd6 100644 --- a/app/src/plugins/getConfig.ts +++ b/app/src/plugins/getConfig.ts @@ -26,6 +26,9 @@ export function getConfig (): void { const businessDashUrl: string = import.meta.env.VUE_APP_BUSINESS_DASH_URL sessionStorage.setItem('BUSINESS_DASH_URL', businessDashUrl) + const businessHomeUrl: string = import.meta.env.VUE_APP_BUSINESS_HOME_URL + sessionStorage.setItem('BUSINESS_HOME_URL', businessHomeUrl) + const businessRegistryUrl: string = import.meta.env.VUE_APP_BUSINESS_REGISTRY_URL sessionStorage.setItem('BUSINESS_REGISTRY_URL', businessRegistryUrl) diff --git a/app/src/plugins/utilities.ts b/app/src/plugins/utilities.ts index 80fd86881..6f8c71048 100644 --- a/app/src/plugins/utilities.ts +++ b/app/src/plugins/utilities.ts @@ -109,6 +109,17 @@ export function containsLongAndShortDesignation (name: string, word: string): bo return false } +/** + * Builds the URL of the Business Home (new Registry Home) login page. + * If a valid idp is specified, that login is triggered immediately instead of + * showing the login options. + */ +export function getBusinessHomeLoginUrl (returnUrl: string = window.location.href, idp?: string): string { + const businessHomeUrl = sessionStorage.getItem('BUSINESS_HOME_URL') + const idpParam = ['bcsc', 'bceid', 'idir'].includes(idp) ? `&idp=${idp}` : '' + return `${businessHomeUrl}en-CA/auth/login?return=${encodeURIComponent(encodeURIComponent(returnUrl))}${idpParam}` +} + export function isSigningIn (): boolean { const path = window.location.pathname return path.includes('/signin') || path.includes('/signin-redirect') || path.includes('/signin-redirect-full') diff --git a/app/src/services/auth-services.ts b/app/src/services/auth-services.ts index 52872feee..dd605dfee 100644 --- a/app/src/services/auth-services.ts +++ b/app/src/services/auth-services.ts @@ -73,6 +73,19 @@ export default class AuthServices { }) } + /** + * Fetches current user's settings (including their accounts). + */ + static async fetchUserSettings (keycloakGuid: string): Promise { + const url = `${this.authApiUrl}/users/${keycloakGuid}/settings` + + return axios.get(url) + .then(response => { + if (response?.data) return response.data + throw new Error('Invalid user settings') + }) + } + /** * Fetches specified org's info. * Throws on error. diff --git a/app/src/views/auth/Signin.vue b/app/src/views/auth/Signin.vue index 14b8b3d1d..ea5ed461c 100644 --- a/app/src/views/auth/Signin.vue +++ b/app/src/views/auth/Signin.vue @@ -1,45 +1,28 @@ diff --git a/app/tests/unit/dialogs/numbered-company-help.spec.ts b/app/tests/unit/dialogs/numbered-company-help.spec.ts index fcaa6013a..217a2dd04 100644 --- a/app/tests/unit/dialogs/numbered-company-help.spec.ts +++ b/app/tests/unit/dialogs/numbered-company-help.spec.ts @@ -32,7 +32,7 @@ const store = useStore() describe('NumberedCompanyHelpDialog', () => { beforeEach(() => { sessionStorage.setItem('CORPORATE_ONLINE_URL', 'https://corporate-online-url/') - sessionStorage.setItem('REGISTRY_HOME_URL', 'https://registry-home-url/') + sessionStorage.setItem('BUSINESS_HOME_URL', 'https://business-home-url/') mockFlags.value = { 'supported-incorporation-registration-entities': [EntityTypes.CR] } @@ -119,8 +119,9 @@ describe('NumberedCompanyHelpDialog', () => { await wrapper.find('#help-business-registry-btn').trigger('click') expect(store.getNumberedCompanyHelpModalVisible).toBe(false) - // unauthenticated user is redirected to login with a return parameter - expect(mockNavigate).toHaveBeenCalledWith(`https://registry-home-url/login?return=${window.location.origin}`) + // unauthenticated user is redirected to login with a double-encoded return parameter + const returnParam = encodeURIComponent(encodeURIComponent(window.location.href)) + expect(mockNavigate).toHaveBeenCalledWith(`https://business-home-url/en-CA/auth/login?return=${returnParam}`) wrapper.destroy() })