From 73839c50b83c8aeaa2b256dde5309933da88163e Mon Sep 17 00:00:00 2001 From: John Sandoval Date: Wed, 4 Feb 2026 16:18:58 -0700 Subject: [PATCH 01/14] WIP: Cosmetology multi-backend scaffolding - Added the concept of an app-mode to track jcc vs. cosmo switching - @todo: Get actual .env values for API & cognito & begin smoke testing - @todo: Implement interceptors to evaluate which API to call - @todo: Update mock data with cosmo data - @todo: Update ChangePassword component (if needed) - @todo: Add debug component to nav that displays app-mode --- webroot/.env.example | 10 +++- webroot/src/app.config.ts | 36 +++++++++--- webroot/src/components/App/App.ts | 18 ++++++ .../CompactSelector/CompactSelector.ts | 7 ++- webroot/src/locales/en.json | 17 ++++++ webroot/src/locales/es.json | 17 ++++++ webroot/src/models/Compact/Compact.model.ts | 3 +- .../src/pages/AuthCallback/AuthCallback.ts | 58 +++++++++++++++---- webroot/src/pages/Logout/Logout.ts | 27 ++++++++- .../MfaResetConfirmLicensee.ts | 7 ++- .../MfaResetStartLicensee.ts | 7 ++- .../PublicDashboard/PublicDashboard.spec.ts | 22 +++++-- .../pages/PublicDashboard/PublicDashboard.ts | 9 ++- .../src/plugins/EnvConfig/envConfig.plugin.ts | 12 ++++ webroot/src/store/global/global.actions.ts | 3 + webroot/src/store/global/global.mutations.ts | 6 +- webroot/src/store/global/global.spec.ts | 10 +++- webroot/src/store/global/global.state.ts | 4 +- webroot/src/store/user/user.actions.ts | 13 ++++- webroot/src/store/user/user.spec.ts | 7 ++- 20 files changed, 250 insertions(+), 43 deletions(-) diff --git a/webroot/.env.example b/webroot/.env.example index 856b3de93..cb10af5c8 100644 --- a/webroot/.env.example +++ b/webroot/.env.example @@ -7,11 +7,17 @@ VUE_APP_API_STATE_ROOT=https://api.test.jcc.iaapi.io VUE_APP_API_LICENSE_ROOT=https://api.test.jcc.iaapi.io VUE_APP_API_SEARCH_ROOT=https://search.test.jcc.iaapi.io VUE_APP_API_USER_ROOT=https://api.test.jcc.iaapi.io +VUE_APP_API_STATE_ROOT_COSMO=https://api.test.cosmo.iaapi.io +VUE_APP_API_LICENSE_ROOT_COSMO=https://api.test.cosmo.iaapi.io +VUE_APP_API_SEARCH_ROOT_COSMO=https://search.test.cosmo.iaapi.io +VUE_APP_API_USER_ROOT_COSMO=https://api.test.cosmo.iaapi.io VUE_APP_COGNITO_REGION=us-east-1 VUE_APP_COGNITO_AUTH_DOMAIN_STAFF=https://staff-auth.test.jcc.iaapi.io -VUE_APP_COGNITO_CLIENT_ID_STAFF=4s5iil9aut9lo0du76p37o8m7h +VUE_APP_COGNITO_CLIENT_ID_STAFF=15mh24ea4af3of8jcnv8h2ic10 VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE=https://licensee-auth.test.jcc.iaapi.io -VUE_APP_COGNITO_CLIENT_ID_LICENSEE=topd4vhftng5cfm3ccgkb6ejd +VUE_APP_COGNITO_CLIENT_ID_LICENSEE=2uv8d9iom5tq8onicri9akot3r +VUE_APP_COGNITO_AUTH_DOMAIN_STAFF_COSMO=https://staff-auth.test.cosmo.iaapi.io +VUE_APP_COGNITO_CLIENT_ID_STAFF_COSMO=TODO VUE_APP_RECAPTCHA_KEY=6Le-3bgqAAAAAILDVUKkRnAF9SSzb8o9uv5lY7Ih VUE_APP_STATSIG_KEY=TODO VUE_APP_STATSIG_DISABLED=false diff --git a/webroot/src/app.config.ts b/webroot/src/app.config.ts index b37752936..e3d4a750d 100644 --- a/webroot/src/app.config.ts +++ b/webroot/src/app.config.ts @@ -8,6 +8,14 @@ import { config as envConfig } from '@plugins/EnvConfig/envConfig.plugin'; import localStorage from '@store/local.storage'; import moment from 'moment'; +// =============== +// = App Modes = +// =============== +export enum AppModes { + JCC = 'jcc', + COSMETOLOGY = 'cosmo', +} + // ========================= // = Authorization Types = // ========================= @@ -25,7 +33,7 @@ export type CognitoConfig = { clientId: string; authDomain: string; }; -export const getCognitoConfig = (authType: AuthTypes): CognitoConfig => { +export const getCognitoConfig = (appMode: AppModes, authType: AuthTypes): CognitoConfig => { const config: CognitoConfig = { scopes: '', clientId: '', @@ -35,12 +43,23 @@ export const getCognitoConfig = (authType: AuthTypes): CognitoConfig => { switch (authType) { case AuthTypes.STAFF: config.scopes = staffLoginScopes; - if (envConfig.cognitoClientIdStaff) { - config.clientId = envConfig.cognitoClientIdStaff; - } - if (envConfig.cognitoAuthDomainStaff) { - config.authDomain = envConfig.cognitoAuthDomainStaff; + + if (appMode === AppModes.JCC) { + if (envConfig.cognitoClientIdStaff) { + config.clientId = envConfig.cognitoClientIdStaff; + } + if (envConfig.cognitoAuthDomainStaff) { + config.authDomain = envConfig.cognitoAuthDomainStaff; + } + } else if (appMode === AppModes.COSMETOLOGY) { + if (envConfig.cognitoClientIdStaffCosmo) { + config.clientId = envConfig.cognitoClientIdStaffCosmo; + } + if (envConfig.cognitoAuthDomainStaffCosmo) { + config.authDomain = envConfig.cognitoAuthDomainStaffCosmo; + } } + break; case AuthTypes.LICENSEE: config.scopes = licenseeLoginScopes; @@ -58,9 +77,9 @@ export const getCognitoConfig = (authType: AuthTypes): CognitoConfig => { return config; }; -export const getHostedLoginUri = (authType: AuthTypes, hostedIdpPath = '/login'): string => { +export const getHostedLoginUri = (appMode: AppModes, authType: AuthTypes, hostedIdpPath = '/login'): string => { const { domain } = envConfig; - const { scopes, clientId, authDomain } = getCognitoConfig(authType); + const { scopes, clientId, authDomain } = getCognitoConfig(appMode, authType); const loginUriQuery = [ `?client_id=${clientId}`, `&response_type=code`, @@ -308,6 +327,7 @@ export const compacts = { aslp: {}, octp: {}, coun: {}, + cosm: {}, }; // ============================= diff --git a/webroot/src/components/App/App.ts b/webroot/src/components/App/App.ts index a082ac900..e0a1bf983 100644 --- a/webroot/src/components/App/App.ts +++ b/webroot/src/components/App/App.ts @@ -14,6 +14,7 @@ import { import { RouteRecordName } from 'vue-router'; import { authStorage, + AppModes, AuthTypes, relativeTimeFormats, AUTH_TYPE @@ -47,6 +48,8 @@ class App extends Vue { // Lifecycle // async created() { + this.setAppModeFromCompact(this.routeCompactType); + if (this.userStore.isLoggedIn) { await this.handleAuth(); } @@ -103,6 +106,18 @@ class App extends Vue { } } + setAppModeFromCompact(compact: CompactType | null): void { + let appMode = AppModes.JCC; + + if (compact === CompactType.COSMETOLOGY) { + appMode = AppModes.COSMETOLOGY; + } + + if (this.globalStore.appMode !== appMode) { + this.$store.dispatch('setAppMode', appMode); + } + } + setAuthType() { let authType: AuthTypes; @@ -192,6 +207,9 @@ class App extends Vue { }); } } + + // Set the app mode based on the current compact + this.setAppModeFromCompact(userDefaultCompact?.type || null); } setRelativeTimeFormats() { diff --git a/webroot/src/components/CompactSelector/CompactSelector.ts b/webroot/src/components/CompactSelector/CompactSelector.ts index 8db49ff50..95f95b5f0 100644 --- a/webroot/src/components/CompactSelector/CompactSelector.ts +++ b/webroot/src/components/CompactSelector/CompactSelector.ts @@ -5,7 +5,7 @@ // Created by InspiringApps on 10/2/2024. // -import { compacts as compactsConfig } from '@/app.config'; +import { AppModes, compacts as compactsConfig } from '@/app.config'; import { Component, mixins, @@ -146,6 +146,11 @@ class CompactSelector extends mixins(MixinForm) { } else { // Refresh the compact type on the store await this.$store.dispatch('user/setCurrentCompact', CompactSerializer.fromServer({ type: selectedCompactType })); + if (selectedCompactType === CompactType.COSMETOLOGY) { + this.$store.dispatch('setAppMode', AppModes.COSMETOLOGY); + } else { + this.$store.dispatch('setAppMode', AppModes.JCC); + } } } diff --git a/webroot/src/locales/en.json b/webroot/src/locales/en.json index 247d75bcf..013deaeaf 100644 --- a/webroot/src/locales/en.json +++ b/webroot/src/locales/en.json @@ -500,6 +500,11 @@ "name": "Counseling", "abbrev": "Counseling", "key": "coun" + }, + { + "name": "Cosmetology and Esthetics", + "abbrev": "COSM", + "key": "cosm" } ], "compact": { @@ -812,6 +817,18 @@ "key": "licensed professional counselor", "compactKey": "coun", "abbrev": "lpc" + }, + { + "name": "Cosmetologist", + "key": "cosmetologist", + "compactKey": "cosm", + "abbrev": "cos" + }, + { + "name": "Esthetician", + "key": "esthetician", + "compactKey": "cosm", + "abbrev": "esth" } ], "disciplineTypes": [ diff --git a/webroot/src/locales/es.json b/webroot/src/locales/es.json index a8dc9db2b..b2cbcab45 100644 --- a/webroot/src/locales/es.json +++ b/webroot/src/locales/es.json @@ -484,6 +484,11 @@ "name": "Asesoramiento", "abbrev": "Counseling", "key": "coun" + }, + { + "name": "Cosmetología y Estética", + "abbrev": "COSM", + "key": "cosm" } ], "compact": { @@ -796,6 +801,18 @@ "key": "licensed professional counselor", "compactKey": "coun", "abbrev": "lpc" + }, + { + "name": "Cosmetólogo", + "key": "cosmetologist", + "compactKey": "cosm", + "abbrev": "cos" + }, + { + "name": "Esteticista", + "key": "esthetician", + "compactKey": "cosm", + "abbrev": "esth" } ], "disciplineTypes": [ diff --git a/webroot/src/models/Compact/Compact.model.ts b/webroot/src/models/Compact/Compact.model.ts index 409253c04..9c5cca37b 100644 --- a/webroot/src/models/Compact/Compact.model.ts +++ b/webroot/src/models/Compact/Compact.model.ts @@ -13,10 +13,11 @@ import { CompactFeeConfig } from '@models/CompactFeeConfig/CompactFeeConfig.mode // ======================================================== // = Interface = // ======================================================== -export enum CompactType { // Temp server definition until server returns via endpoint +export enum CompactType { ASLP = 'aslp', OT = 'octp', COUNSELING = 'coun', + COSMETOLOGY = 'cosm', } export interface PaymentProcessorConfig { diff --git a/webroot/src/pages/AuthCallback/AuthCallback.ts b/webroot/src/pages/AuthCallback/AuthCallback.ts index 49e20d762..863de1963 100644 --- a/webroot/src/pages/AuthCallback/AuthCallback.ts +++ b/webroot/src/pages/AuthCallback/AuthCallback.ts @@ -11,6 +11,7 @@ import Section from '@components/Section/Section.vue'; import Card from '@components/Card/Card.vue'; import { authStorage, + AppModes, AuthTypes, AUTH_TYPE, AUTH_LOGIN_GOTO_PATH, @@ -18,6 +19,12 @@ import { } from '@/app.config'; import axios from 'axios'; +export enum CognitoStateTypes { + STAFF_JCC = 'staff', + STAFF_COSMETOLOGY = 'staff-cosmo', + LICENSEE_JCC = 'licensee', +} + @Component({ name: 'AuthCallback', components: { @@ -59,34 +66,48 @@ export default class AuthCallback extends Vue { // async getTokens(): Promise { this.$store.dispatch('startLoading'); + const { userType } = this; - if (this.userType === AuthTypes.STAFF) { - await this.getTokensStaff().catch(() => { + if (userType === CognitoStateTypes.STAFF_JCC) { + this.$store.dispatch('setAppMode', AppModes.JCC); + await this.getTokensStaffJcc().catch(() => { this.isError = true; }); - } else if (this.userType === AuthTypes.LICENSEE) { - await this.getTokensLicensee().catch(() => { + } else if (userType === CognitoStateTypes.LICENSEE_JCC) { + this.$store.dispatch('setAppMode', AppModes.JCC); + await this.getTokensLicenseeJcc().catch(() => { + this.isError = true; + }); + } else if (userType === CognitoStateTypes.STAFF_COSMETOLOGY) { + this.$store.dispatch('setAppMode', AppModes.COSMETOLOGY); + await this.getTokensStaffCosmo().catch(() => { this.isError = true; }); } else { // If the state query param is absent or not matching we will - // still try to get tokens, if the user just logged in one of the two - // user pools will successfully return tokens. If neither do we enter - // the error state + // still try to get tokens, if the user just logged in one of the + // user pools will successfully return tokens. If none then we enter + // the error state. let errorCount = 0; - await this.getTokensStaff().catch(() => { + await this.getTokensStaffJcc().catch(() => { errorCount += 1; }); if (errorCount > 0) { - await this.getTokensLicensee().catch(() => { + await this.getTokensStaffCosmo().catch(() => { errorCount += 1; }); } if (errorCount > 1) { + await this.getTokensLicenseeJcc().catch(() => { + errorCount += 1; + }); + } + + if (errorCount > 2) { this.isError = true; } } @@ -98,7 +119,7 @@ export default class AuthCallback extends Vue { } } - async getTokensStaff(): Promise { + async getTokensStaffJcc(): Promise { const { domain, cognitoAuthDomainStaff, cognitoClientIdStaff } = this.$envConfig; const params = new URLSearchParams(); @@ -113,7 +134,22 @@ export default class AuthCallback extends Vue { await this.$store.dispatch('user/loginSuccess', AuthTypes.STAFF); } - async getTokensLicensee(): Promise { + async getTokensStaffCosmo(): Promise { + const { domain, cognitoAuthDomainStaffCosmo, cognitoClientIdStaffCosmo } = this.$envConfig; + const params = new URLSearchParams(); + + params.append('grant_type', 'authorization_code'); + params.append('client_id', cognitoClientIdStaffCosmo || ''); + params.append('redirect_uri', `${domain}${this.$route.path}`); + params.append('code', this.authorizationCode); + + const { data } = await axios.post(`${cognitoAuthDomainStaffCosmo}/oauth2/token`, params); + + await this.$store.dispatch('user/updateAuthTokens', { tokenResponse: data, authType: AuthTypes.STAFF }); + await this.$store.dispatch('user/loginSuccess', AuthTypes.STAFF); + } + + async getTokensLicenseeJcc(): Promise { const { domain, cognitoAuthDomainLicensee, cognitoClientIdLicensee } = this.$envConfig; const params = new URLSearchParams(); diff --git a/webroot/src/pages/Logout/Logout.ts b/webroot/src/pages/Logout/Logout.ts index 43b4af619..562296fab 100644 --- a/webroot/src/pages/Logout/Logout.ts +++ b/webroot/src/pages/Logout/Logout.ts @@ -8,6 +8,7 @@ import { Component, Vue } from 'vue-facing-decorator'; import { authStorage, + AppModes, AuthTypes, AUTH_TYPE, AUTH_LOGIN_GOTO_PATH, @@ -30,6 +31,10 @@ export default class Logout extends Vue { // // Computed // + get appMode(): AppModes { + return this.$store.state.appMode; + } + get userStore() { return this.$store.state.user; } @@ -39,14 +44,30 @@ export default class Logout extends Vue { } get hostedLogoutUriStaff(): string { - const { domain, cognitoAuthDomainStaff, cognitoClientIdStaff } = this.$envConfig; + const { + domain, + cognitoAuthDomainStaff, + cognitoClientIdStaff, + cognitoAuthDomainStaffCosmo, + cognitoClientIdStaffCosmo + } = this.$envConfig; + let cognitoAuthDomain = cognitoAuthDomainStaff; + let cognitoClientId = cognitoClientIdStaff; + + // Adjust cognito params based on app mode + if (this.appMode === AppModes.COSMETOLOGY) { + cognitoAuthDomain = cognitoAuthDomainStaffCosmo; + cognitoClientId = cognitoClientIdStaffCosmo; + } + + // Create the logout URI const logoutLink = encodeURIComponent(`${(domain as string)}/Logout`); const logoutUriQuery = [ - `?client_id=${cognitoClientIdStaff}`, + `?client_id=${cognitoClientId}`, `&logout_uri=${logoutLink}` ].join(''); const idpPath = '/logout'; - const logoutUri = `${cognitoAuthDomainStaff}${idpPath}${logoutUriQuery}`; + const logoutUri = `${cognitoAuthDomain}${idpPath}${logoutUriQuery}`; return logoutUri; } diff --git a/webroot/src/pages/MfaResetConfirmLicensee/MfaResetConfirmLicensee.ts b/webroot/src/pages/MfaResetConfirmLicensee/MfaResetConfirmLicensee.ts index 4317f78a7..0f328a841 100644 --- a/webroot/src/pages/MfaResetConfirmLicensee/MfaResetConfirmLicensee.ts +++ b/webroot/src/pages/MfaResetConfirmLicensee/MfaResetConfirmLicensee.ts @@ -13,6 +13,7 @@ import { } from 'vue-facing-decorator'; import { authStorage, + AppModes, AuthTypes, getHostedLoginUri, AUTH_LOGIN_GOTO_PATH, @@ -60,6 +61,10 @@ class MfaResetConfirmLicensee extends Vue { // // Computed // + get appMode(): AppModes { + return this.$store.state.appMode; + } + get compactQuery(): string { const compact: string = (this.$route.query?.compact as string) || ''; @@ -75,7 +80,7 @@ class MfaResetConfirmLicensee extends Vue { } get hostedLoginUriLicensee(): string { - return getHostedLoginUri(AuthTypes.LICENSEE, '/login'); + return getHostedLoginUri(this.appMode, AuthTypes.LICENSEE, '/login'); } get isUsingMockApi(): boolean { diff --git a/webroot/src/pages/MfaResetStartLicensee/MfaResetStartLicensee.ts b/webroot/src/pages/MfaResetStartLicensee/MfaResetStartLicensee.ts index bf329d1e6..834e3c7cd 100644 --- a/webroot/src/pages/MfaResetStartLicensee/MfaResetStartLicensee.ts +++ b/webroot/src/pages/MfaResetStartLicensee/MfaResetStartLicensee.ts @@ -20,6 +20,7 @@ import { import { stateList, dateFormatPatterns, + AppModes, AuthTypes, getHostedLoginUri } from '@/app.config'; @@ -85,6 +86,10 @@ class MfaResetStartLicensee extends mixins(MixinForm) { // // Computed // + get appMode(): AppModes { + return this.$store.state.appMode; + } + get stateOptions(): Array { const options = [{ value: '', name: `- ${this.$t('common.select')} -`, isDisabled: true }]; @@ -165,7 +170,7 @@ class MfaResetStartLicensee extends mixins(MixinForm) { } get hostedForgotPasswordUriLicensee(): string { - return getHostedLoginUri(AuthTypes.LICENSEE, '/forgotPassword'); + return getHostedLoginUri(this.appMode, AuthTypes.LICENSEE, '/forgotPassword'); } get isUsingMockApi(): boolean { diff --git a/webroot/src/pages/PublicDashboard/PublicDashboard.spec.ts b/webroot/src/pages/PublicDashboard/PublicDashboard.spec.ts index e086ed908..e52de7dc3 100644 --- a/webroot/src/pages/PublicDashboard/PublicDashboard.spec.ts +++ b/webroot/src/pages/PublicDashboard/PublicDashboard.spec.ts @@ -7,7 +7,12 @@ import { mountShallow } from '@tests/helpers/setup'; import PublicDashboard from '@pages/PublicDashboard/PublicDashboard.vue'; -import { AuthTypes, getCognitoConfig, getHostedLoginUri } from '@/app.config'; +import { + AppModes, + AuthTypes, + getCognitoConfig, + getHostedLoginUri +} from '@/app.config'; import { config as envConfig } from '@plugins/EnvConfig/envConfig.plugin'; const chaiMatchPattern = require('chai-match-pattern'); @@ -22,15 +27,22 @@ describe('PublicDashboard page', async () => { expect(wrapper.exists()).to.equal(true); expect(wrapper.findComponent(PublicDashboard).exists()).to.equal(true); }); - it('should use staff cognito config in app.config', async () => { - const cognitoConfig = getCognitoConfig(AuthTypes.STAFF); + it('should use staff cognito config in app.config (jcc)', async () => { + const cognitoConfig = getCognitoConfig(AppModes.JCC, AuthTypes.STAFF); expect(cognitoConfig.scopes).to.equal('email openid phone profile aws.cognito.signin.user.admin'); expect(cognitoConfig.clientId).to.equal(envConfig.cognitoClientIdStaff); expect(cognitoConfig.authDomain).to.equal(envConfig.cognitoAuthDomainStaff); }); - it('should use licensee cognito config in app.config', async () => { - const cognitoConfig = getCognitoConfig(AuthTypes.LICENSEE); + it('should use staff cognito config in app.config (cosmetology)', async () => { + const cognitoConfig = getCognitoConfig(AppModes.COSMETOLOGY, AuthTypes.STAFF); + + expect(cognitoConfig.scopes).to.equal('email openid phone profile aws.cognito.signin.user.admin'); + expect(cognitoConfig.clientId).to.equal(envConfig.cognitoClientIdStaffCosmo); + expect(cognitoConfig.authDomain).to.equal(envConfig.cognitoAuthDomainStaffCosmo); + }); + it('should use licensee cognito config in app.config (jcc)', async () => { + const cognitoConfig = getCognitoConfig(AppModes.JCC, AuthTypes.LICENSEE); expect(cognitoConfig.scopes).to.equal('email openid phone profile aws.cognito.signin.user.admin'); expect(cognitoConfig.clientId).to.equal(envConfig.cognitoClientIdLicensee); diff --git a/webroot/src/pages/PublicDashboard/PublicDashboard.ts b/webroot/src/pages/PublicDashboard/PublicDashboard.ts index 9c123d2cc..812104823 100644 --- a/webroot/src/pages/PublicDashboard/PublicDashboard.ts +++ b/webroot/src/pages/PublicDashboard/PublicDashboard.ts @@ -8,6 +8,7 @@ import { Component, Vue } from 'vue-facing-decorator'; import { authStorage, + AppModes, AuthTypes, getHostedLoginUri, AUTH_LOGIN_GOTO_PATH, @@ -42,6 +43,10 @@ export default class DashboardPublic extends Vue { // // Computed // + get appMode(): AppModes { + return this.$store.state.appMode; + } + get bypassQuery(): string { const bypass: string = (this.$route.query?.bypass as string) || ''; @@ -59,11 +64,11 @@ export default class DashboardPublic extends Vue { } get hostedLoginUriStaff(): string { - return getHostedLoginUri(AuthTypes.STAFF, this.hostedLoginUriPath); + return getHostedLoginUri(this.appMode, AuthTypes.STAFF, this.hostedLoginUriPath); } get hostedLoginUriLicensee(): string { - return getHostedLoginUri(AuthTypes.LICENSEE, this.hostedLoginUriPath); + return getHostedLoginUri(this.appMode, AuthTypes.LICENSEE, this.hostedLoginUriPath); } get isUsingMockApi(): boolean { diff --git a/webroot/src/plugins/EnvConfig/envConfig.plugin.ts b/webroot/src/plugins/EnvConfig/envConfig.plugin.ts index 820a686bb..d945e5895 100644 --- a/webroot/src/plugins/EnvConfig/envConfig.plugin.ts +++ b/webroot/src/plugins/EnvConfig/envConfig.plugin.ts @@ -45,6 +45,10 @@ export interface EnvConfig { apiUrlLicense?: string; apiUrlSearch?: string; apiUrlUser?: string; + apiUrlStateCosmo?: string; + apiUrlLicenseCosmo?: string; + apiUrlSearchCosmo?: string; + apiUrlUserCosmo?: string; apiUrlExample?: string; apiKeyExample?: string; cognitoRegion?: string; @@ -52,6 +56,8 @@ export interface EnvConfig { cognitoClientIdStaff?: string; cognitoAuthDomainLicensee?: string; cognitoClientIdLicensee?: string; + cognitoAuthDomainStaffCosmo?: string; + cognitoClientIdStaffCosmo?: string; recaptchaKey?: string; statsigKey?: string; isStatsigDisabled?: boolean; @@ -78,6 +84,10 @@ export const config: EnvConfig = { apiUrlLicense: context.VUE_APP_API_LICENSE_ROOT, apiUrlSearch: context.VUE_APP_API_SEARCH_ROOT, apiUrlUser: context.VUE_APP_API_USER_ROOT, + apiUrlStateCosmo: context.VUE_APP_API_STATE_ROOT_COSMO, + apiUrlLicenseCosmo: context.VUE_APP_API_LICENSE_ROOT_COSMO, + apiUrlSearchCosmo: context.VUE_APP_API_SEARCH_ROOT_COSMO, + apiUrlUserCosmo: context.VUE_APP_API_USER_ROOT_COSMO, apiUrlExample: '/api', apiKeyExample: 'example', cognitoRegion: context.VUE_APP_COGNITO_REGION, @@ -85,6 +95,8 @@ export const config: EnvConfig = { cognitoClientIdStaff: context.VUE_APP_COGNITO_CLIENT_ID_STAFF, cognitoAuthDomainLicensee: context.VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE, cognitoClientIdLicensee: context.VUE_APP_COGNITO_CLIENT_ID_LICENSEE, + cognitoAuthDomainStaffCosmo: context.VUE_APP_COGNITO_AUTH_DOMAIN_STAFF_COSMO, + cognitoClientIdStaffCosmo: context.VUE_APP_COGNITO_CLIENT_ID_STAFF_COSMO, recaptchaKey: context.VUE_APP_RECAPTCHA_KEY, statsigKey: context.VUE_APP_STATSIG_KEY, isStatsigDisabled: (context.VUE_APP_STATSIG_DISABLED === 'true'), diff --git a/webroot/src/store/global/global.actions.ts b/webroot/src/store/global/global.actions.ts index d82f29be7..1d07e6b8c 100644 --- a/webroot/src/store/global/global.actions.ts +++ b/webroot/src/store/global/global.actions.ts @@ -30,6 +30,9 @@ export default { setModalIsLogoutOnly: ({ commit }, isLogoutOnly) => { commit(MutationTypes.SET_MODAL_LOGOUT_ONLY, isLogoutOnly); }, + setAppMode: ({ commit }, mode) => { + commit(MutationTypes.SET_APP_MODE, mode); + }, setAuthType: ({ commit }, type) => { commit(MutationTypes.SET_AUTH_TYPE, type); }, diff --git a/webroot/src/store/global/global.mutations.ts b/webroot/src/store/global/global.mutations.ts index 9576b065b..04a7fb7d9 100644 --- a/webroot/src/store/global/global.mutations.ts +++ b/webroot/src/store/global/global.mutations.ts @@ -5,7 +5,7 @@ // Created by InspiringApps on 4/12/20. // -import { AuthTypes } from '@/app.config'; +import { AuthTypes, AppModes } from '@/app.config'; import { AppMessage } from '@/models/AppMessage/AppMessage.model'; import { State } from './global.state'; @@ -19,6 +19,7 @@ export enum MutationTypes { STORE_RESET_GLOBAL = '[Global] Store reset', SET_MODAL_OPEN = '[Global] Modal isOpen set', SET_MODAL_LOGOUT_ONLY = '[Global] Modal isLogoutOnly set', + SET_APP_MODE = '[Global] App mode set', SET_AUTH_TYPE = '[Global] Auth type set', EXPAND_NAV_MENU = '[Global] Expand nav menu', COLLAPSE_NAV_MENU = '[Global] Collapse nav menu', @@ -54,6 +55,9 @@ export default { state.isModalOpen = false; state.isModalLogoutOnly = false; }, + [MutationTypes.SET_APP_MODE]: (state: State, mode: AppModes) => { + state.appMode = mode; + }, [MutationTypes.SET_AUTH_TYPE]: (state: State, type: AuthTypes) => { state.authType = type; }, diff --git a/webroot/src/store/global/global.spec.ts b/webroot/src/store/global/global.spec.ts index f3fdd758b..43d6921ec 100644 --- a/webroot/src/store/global/global.spec.ts +++ b/webroot/src/store/global/global.spec.ts @@ -5,7 +5,7 @@ // Created by InspiringApps on 4/12/20. // -import { AuthTypes } from '@/app.config'; +import { AuthTypes, AppModes } from '@/app.config'; import mutations, { MutationTypes } from './global.mutations'; import actions from './global.actions'; @@ -99,6 +99,14 @@ describe('Global Store Mutations', () => { expect(state.isModalLogoutOnly).to.equal(isModalLogoutOnly); }); + it('should successfully set app mode', () => { + const state = {}; + const appMode = AppModes.JCC; + + mutations[MutationTypes.SET_APP_MODE](state, appMode); + + expect(state.appMode).to.equal(appMode); + }); it('should successfully set auth type', () => { const state = {}; const authType = AuthTypes.LICENSEE; diff --git a/webroot/src/store/global/global.state.ts b/webroot/src/store/global/global.state.ts index a100e9f4c..e6dea49bf 100644 --- a/webroot/src/store/global/global.state.ts +++ b/webroot/src/store/global/global.state.ts @@ -4,7 +4,7 @@ // // Created by InspiringApps on 4/12/20. // -import { AuthTypes } from '@/app.config'; +import { AuthTypes, AppModes } from '@/app.config'; import { AppMessage } from '@/models/AppMessage/AppMessage.model'; export interface State { @@ -13,6 +13,7 @@ export interface State { messages: Array; isModalOpen: boolean; isModalLogoutOnly: boolean; + appMode: AppModes; authType: AuthTypes; isNavExpanded: boolean; } @@ -23,6 +24,7 @@ export const state: State = { messages: [], isModalOpen: false, isModalLogoutOnly: false, + appMode: AppModes.JCC, authType: AuthTypes.PUBLIC, isNavExpanded: false, }; diff --git a/webroot/src/store/user/user.actions.ts b/webroot/src/store/user/user.actions.ts index 081e60dbc..840e71034 100644 --- a/webroot/src/store/user/user.actions.ts +++ b/webroot/src/store/user/user.actions.ts @@ -9,6 +9,7 @@ import { dataApi } from '@network/data.api'; import { config } from '@plugins/EnvConfig/envConfig.plugin'; import { authStorage, + AppModes, AuthTypes, tokens, AUTH_TYPE, @@ -235,13 +236,19 @@ export default { dispatch('setRefreshTokenTimeout', { refreshToken, expiresIn, authType }); } }, - setRefreshTokenTimeout: async ({ commit, dispatch }, { refreshToken, expiresIn, authType }) => { + setRefreshTokenTimeout: async ({ rootState, commit, dispatch }, { refreshToken, expiresIn, authType }) => { + const { appMode } = rootState; let cognitoClientId; let cognitoAuthDomain; if (authType === AuthTypes.STAFF) { - cognitoClientId = config.cognitoClientIdStaff; - cognitoAuthDomain = config.cognitoAuthDomainStaff; + if (appMode === AppModes.JCC) { + cognitoClientId = config.cognitoClientIdStaff; + cognitoAuthDomain = config.cognitoAuthDomainStaff; + } else if (appMode === AppModes.COSMETOLOGY) { + cognitoClientId = config.cognitoClientIdStaffCosmo; + cognitoAuthDomain = config.cognitoAuthDomainStaffCosmo; + } } else if (authType === AuthTypes.LICENSEE) { cognitoClientId = config.cognitoClientIdLicensee; cognitoAuthDomain = config.cognitoAuthDomainLicensee; diff --git a/webroot/src/store/user/user.spec.ts b/webroot/src/store/user/user.spec.ts index a5169e01c..fce3be555 100644 --- a/webroot/src/store/user/user.spec.ts +++ b/webroot/src/store/user/user.spec.ts @@ -9,6 +9,7 @@ import { authStorage, tokens, FeeTypes, + AppModes, AuthTypes } from '@/app.config'; import chaiMatchPattern from 'chai-match-pattern'; @@ -742,25 +743,27 @@ describe('User Store Actions', async () => { expect(dispatch.calledOnce).to.equal(false); }); it('should successfully set staff refresh token timeout', () => { + const rootState = { appMode: AppModes.JCC }; const commit = sinon.spy(); const dispatch = sinon.spy(); const refreshToken = 'test_refresh_token'; const expiresIn = 7200; const authType = 'staff'; - actions.setRefreshTokenTimeout({ commit, dispatch }, { refreshToken, expiresIn, authType }); + actions.setRefreshTokenTimeout({ rootState, commit, dispatch }, { refreshToken, expiresIn, authType }); expect(commit.calledOnce).to.equal(true); expect(dispatch.calledOnce).to.equal(false); }); it('should successfully set licensee refresh token timeout', () => { + const rootState = { appMode: AppModes.JCC }; const commit = sinon.spy(); const dispatch = sinon.spy(); const refreshToken = 'test_refresh_token'; const expiresIn = 7200; const authType = 'licensee'; - actions.setRefreshTokenTimeout({ commit, dispatch }, { refreshToken, expiresIn, authType }); + actions.setRefreshTokenTimeout({ rootState, commit, dispatch }, { refreshToken, expiresIn, authType }); expect(commit.calledOnce).to.equal(true); expect(dispatch.calledOnce).to.equal(false); From d36628002b6a2b04dba42d5a0dda40862825faa3 Mon Sep 17 00:00:00 2001 From: John Sandoval Date: Thu, 5 Feb 2026 12:59:50 -0700 Subject: [PATCH 02/14] WIP: Cosmetology multi-backend scaffolding - Add debug component to nav that displays app-mode - @todo: Get actual .env values for API & cognito & begin smoke testing - @todo: Implement interceptors to evaluate which API to call - @todo: Update mock data with cosmo data & to handle app mode - @todo: Update ChangePassword component (if needed) --- webroot/.env.example | 4 ++-- webroot/src/components/App/App.ts | 15 ++++++++++++ .../Page/PageMainNav/PageMainNav.less | 7 ++++++ .../Page/PageMainNav/PageMainNav.vue | 3 +++ .../pages/PublicDashboard/PublicDashboard.ts | 23 +++++++++++++++--- .../pages/PublicDashboard/PublicDashboard.vue | 22 +++++++++++++++++ webroot/src/store/global/global.actions.ts | 3 +++ webroot/src/store/global/global.mutations.ts | 4 ++++ webroot/src/store/global/global.spec.ts | 24 +++++++++++++++++++ webroot/src/store/global/global.state.ts | 2 ++ 10 files changed, 102 insertions(+), 5 deletions(-) diff --git a/webroot/.env.example b/webroot/.env.example index cb10af5c8..6dc53a14c 100644 --- a/webroot/.env.example +++ b/webroot/.env.example @@ -16,8 +16,8 @@ VUE_APP_COGNITO_AUTH_DOMAIN_STAFF=https://staff-auth.test.jcc.iaapi.io VUE_APP_COGNITO_CLIENT_ID_STAFF=15mh24ea4af3of8jcnv8h2ic10 VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE=https://licensee-auth.test.jcc.iaapi.io VUE_APP_COGNITO_CLIENT_ID_LICENSEE=2uv8d9iom5tq8onicri9akot3r -VUE_APP_COGNITO_AUTH_DOMAIN_STAFF_COSMO=https://staff-auth.test.cosmo.iaapi.io -VUE_APP_COGNITO_CLIENT_ID_STAFF_COSMO=TODO +VUE_APP_COGNITO_AUTH_DOMAIN_STAFF_COSMO=https://staff-auth.test.cosmetology.jcc.iaapi.io +VUE_APP_COGNITO_CLIENT_ID_STAFF_COSMO=42km9ho786d28dp812j88kvscq VUE_APP_RECAPTCHA_KEY=6Le-3bgqAAAAAILDVUKkRnAF9SSzb8o9uv5lY7Ih VUE_APP_STATSIG_KEY=TODO VUE_APP_STATSIG_DISABLED=false diff --git a/webroot/src/components/App/App.ts b/webroot/src/components/App/App.ts index e0a1bf983..2a90f6c8a 100644 --- a/webroot/src/components/App/App.ts +++ b/webroot/src/components/App/App.ts @@ -26,6 +26,8 @@ import AutoLogout from '@components/AutoLogout/AutoLogout.vue'; import { StatsigUser } from '@statsig/js-client'; import moment from 'moment'; +const appWindow = window as any; + @Component({ name: 'App', components: { @@ -56,6 +58,7 @@ class App extends Vue { this.setRelativeTimeFormats(); this.setFeatureGateRefetchInterval(); + this.addAppModeDebugger(); } async beforeUnmount() { @@ -223,6 +226,18 @@ class App extends Vue { this.$store.dispatch('clearMessages'); } + addAppModeDebugger(): void { + appWindow.ccModeToggle = () => { + if (this.globalStore.isAppModeDisplayed) { + this.$store.dispatch('setAppModeDisplay', false); + console.log('CompactConnect app mode display: DISABLED'); + } else { + this.$store.dispatch('setAppModeDisplay', true); + console.log('CompactConnect app mode display: ENABLED'); + } + }; + } + // // Watchers // diff --git a/webroot/src/components/Page/PageMainNav/PageMainNav.less b/webroot/src/components/Page/PageMainNav/PageMainNav.less index 5a80e8ada..53a44a93f 100644 --- a/webroot/src/components/Page/PageMainNav/PageMainNav.less +++ b/webroot/src/components/Page/PageMainNav/PageMainNav.less @@ -157,4 +157,11 @@ background-color: fade(@white, 50%); transition: width @navAnimation; } + + .app-mode { + display: flex; + width: 100%; + padding: 0.4rem; + background-color: @midGrey; + } } diff --git a/webroot/src/components/Page/PageMainNav/PageMainNav.vue b/webroot/src/components/Page/PageMainNav/PageMainNav.vue index 7e1b4024e..9f3121388 100644 --- a/webroot/src/components/Page/PageMainNav/PageMainNav.vue +++ b/webroot/src/components/Page/PageMainNav/PageMainNav.vue @@ -71,6 +71,9 @@ :hideIfNotMultiple="true" /> +
+ App mode: {{ $store.state.appMode }} +