Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down
1 change: 1 addition & 0 deletions app/devops/vaults.env
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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())
}
}

Expand Down
37 changes: 35 additions & 2 deletions app/src/main.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<void> {
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
Expand Down
10 changes: 4 additions & 6 deletions app/src/mixins/nr-affiliation-mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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())
}
}

Expand Down
9 changes: 9 additions & 0 deletions app/src/plugins/authHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
const jwt = getJWT()
Expand Down
3 changes: 3 additions & 0 deletions app/src/plugins/getConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
11 changes: 11 additions & 0 deletions app/src/plugins/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
13 changes: 13 additions & 0 deletions app/src/services/auth-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ export default class AuthServices {
})
}

/**
* Fetches current user's settings (including their accounts).
*/
static async fetchUserSettings (keycloakGuid: string): Promise<any[]> {
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.
Expand Down
47 changes: 15 additions & 32 deletions app/src/views/auth/Signin.vue
Original file line number Diff line number Diff line change
@@ -1,45 +1,28 @@
<template>
<SbcSignin
:idpHint="idpHint"
:redirectUrlLoginFail="redirectUrlLoginFail"
:inAuth="false"
@sync-user-profile-ready="onReady()"
/>
<!-- this page immediately redirects to the Business Home login page -->
<div />
</template>

<script lang="ts">
import { Component, Prop, Mixins } from 'vue-property-decorator'
import SbcSignin from 'sbc-common-components/src/components/SbcSignin.vue'
import { LoadKeycloakRolesMixin, NrAffiliationMixin, UpdateUserMixin } from '@/mixins'
import { Component, Prop, Vue } from 'vue-property-decorator'
import { getBusinessHomeLoginUrl, Navigate } from '@/plugins'

/**
* When the user clicks "Log in", they are are redirected to THIS page, which
* renders the SbcSignin component that actually performs the signin process.
* Note that the current state is NOT saved and restored - the user will lose
* their current session data if they sign in mid-session.
* When the user clicks "Log in":
* - they are redirected to THIS page
* - this forwards them to the Business Home login page, which triggers the chosen
* login method immediately (or offers all login options if the method is unknown)
* - after login/account selection and/or creation, Business Home redirects back to the main app page
* - then the Keycloak information is picked up on app init (see syncSession() in main.ts)
* and roles / LaunchDarkly are loaded (see App.vue created()).
*/
@Component({
components: { SbcSignin }
})
export default class Signin extends Mixins(LoadKeycloakRolesMixin, NrAffiliationMixin, UpdateUserMixin) {
@Component({})
export default class Signin extends Vue {
/** The login method, which is passed in the signin route by the SBC Header. */
@Prop({ default: 'bcsc' }) readonly idpHint!: string

/** The URL to redirect to if signin failed: the NR URL. */
get redirectUrlLoginFail (): string {
return `${window.location.origin}${import.meta.env.VUE_APP_PATH}`
}

/** Called after successful signin. */
async onReady () {
console.info('Keycloak session is ready') // eslint-disable-line no-console

// now that the user is logged in, load Keycloak roles and update LaunchDarkly
this.loadKeycloakRoles()
await this.updateLaunchDarkly()

// go to main app page
await this.$router.push('/')
created (): void {
Navigate(getBusinessHomeLoginUrl(`${window.location.origin}${import.meta.env.VUE_APP_PATH}`, this.idpHint))
}
}
</script>
7 changes: 4 additions & 3 deletions app/tests/unit/dialogs/numbered-company-help.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
Expand Down Expand Up @@ -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()
})
Expand Down
Loading