diff --git a/src/common/__tests__/env.test.js b/src/common/__tests__/env.test.js new file mode 100644 index 00000000..44933825 --- /dev/null +++ b/src/common/__tests__/env.test.js @@ -0,0 +1,44 @@ +/* global globalThis */ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { getEnv, toAppOrigin } from '../env.js' + +const envAt = origin => { + globalThis.window = {location: {origin}} + return [getEnv(), getEnv(true)] +} + +test('app.openconceptlab.org is production', () => { + assert.deepEqual(envAt('https://app.openconceptlab.org'), ['production', '']) +}) + +test('app..openconceptlab.org is that environment', () => { + assert.deepEqual(envAt('https://app.staging.openconceptlab.org'), ['staging', 'staging']) + assert.deepEqual(envAt('https://app.qa.openconceptlab.org'), ['qa', 'qa']) + assert.deepEqual(envAt('https://app.demo.openconceptlab.org'), ['demo', 'demo']) + assert.deepEqual(envAt('https://app.dev.openconceptlab.org'), ['dev', 'dev']) + assert.deepEqual(envAt('https://app.staging.who.openconceptlab.org'), ['staging-who', 'staging.who']) +}) + +test('app.v2.openconceptlab.org, where TBv2 stays once TBv3 is the default Term Browser, is production', () => { + assert.deepEqual(envAt('https://app.v2.openconceptlab.org'), ['production', '']) +}) + +test('app.v2..openconceptlab.org is that environment', () => { + assert.deepEqual(envAt('https://app.v2.staging.openconceptlab.org'), ['staging', 'staging']) + assert.deepEqual(envAt('https://app.v2.qa.openconceptlab.org'), ['qa', 'qa']) + assert.deepEqual(envAt('https://app.v2.demo.openconceptlab.org'), ['demo', 'demo']) + assert.deepEqual(envAt('https://app.v2.dev.openconceptlab.org'), ['dev', 'dev']) +}) + +test('any other host is development', () => { + assert.deepEqual(envAt('http://localhost:4000'), ['development', 'development']) +}) + +test('toAppOrigin maps app.v2. to app. and leaves other origins alone', () => { + assert.equal(toAppOrigin('https://app.v2.openconceptlab.org'), 'https://app.openconceptlab.org') + assert.equal(toAppOrigin('https://app.v2.staging.openconceptlab.org'), 'https://app.staging.openconceptlab.org') + assert.equal(toAppOrigin('https://app.qa.openconceptlab.org'), 'https://app.qa.openconceptlab.org') + assert.equal(toAppOrigin('http://localhost:4000'), 'http://localhost:4000') +}) diff --git a/src/common/env.js b/src/common/env.js new file mode 100644 index 00000000..98affecb --- /dev/null +++ b/src/common/env.js @@ -0,0 +1,22 @@ +// TBv2 is served from app[.].openconceptlab.org and from app.v2[.].openconceptlab.org, where it stays +// once TBv3 becomes the default Term Browser (OpenConceptLab/ocl_online#236); either host names the same environment. +export const toAppOrigin = origin => origin.replace('://app.v2.', '://app.') + +export const getEnv = forURL => { + const fqdn = toAppOrigin(window.location.origin); + + if(fqdn.match('app.staging.openconceptlab')) + return 'staging'; + if(fqdn.match('app.qa.openconceptlab')) + return 'qa'; + if(fqdn.match('app.demo.openconceptlab')) + return 'demo'; + if(fqdn.match('app.dev.openconceptlab')) + return 'dev'; + if(fqdn.match('app.staging.who.openconceptlab')) + return forURL ? 'staging.who' : 'staging-who'; + if(fqdn.match('app.openconceptlab')) + return forURL ? '' : 'production'; + + return 'development'; +} diff --git a/src/common/utils.js b/src/common/utils.js index 959cfc3d..78d442a0 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -20,6 +20,7 @@ import GAService from '../services/GAService'; import { ensureIdToken } from './ssoSession'; import { SERVER_CONFIGS } from './serverConfigs'; import { windowConfig } from './windowConfig'; +import { getEnv, toAppOrigin } from './env'; export const currentPath = () => window.location.hash.split('?')[0]; @@ -688,24 +689,7 @@ export const arrayToSentence = (arr, separator, lastSeparator=' and ') => { export const generateRandomString = () => Math.random().toString(36).substring(7); -export const getEnv = forURL => { - const fqdn = window.location.origin; - - if(fqdn.match('app.staging.openconceptlab')) - return 'staging'; - if(fqdn.match('app.qa.openconceptlab')) - return 'qa'; - if(fqdn.match('app.demo.openconceptlab')) - return 'demo'; - if(fqdn.match('app.dev.openconceptlab')) - return 'dev'; - if(fqdn.match('app.staging.who.openconceptlab')) - return forURL ? 'staging.who' : 'staging-who'; - if(fqdn.match('app.openconceptlab')) - return forURL ? '' : 'production'; - - return 'development'; -} +export { getEnv }; export const getOpenMRSURL = () => { let env = getEnv(true); @@ -988,8 +972,10 @@ export const consumeAndValidateOAuthState = returnedState => { return !returnedState || returnedState === storedState } +// Sign-in, sign-up and sign-out come back to the host they started on (app. or app.v2.), since the +// PKCE verifier and OAuth state are in that origin's sessionStorage. LOGIN_REDIRECT_URL only turns SSO on. export const getLoginURL = async returnTo => { - let redirectURL = windowConfig('LOGIN_REDIRECT_URL') || process.env.LOGIN_REDIRECT_URL + let redirectURL = window.location.origin const oidClientID = windowConfig('OIDC_RP_CLIENT_ID') || process.env.OIDC_RP_CLIENT_ID redirectURL = redirectURL.replace(/([^:]\/)\/+/g, "$1"); @@ -1014,7 +1000,7 @@ export const getLoginURL = async returnTo => { } export const getRegisterURL = async returnTo => { - let redirectURL = returnTo || windowConfig('LOGIN_REDIRECT_URL') || process.env.LOGIN_REDIRECT_URL + let redirectURL = returnTo || window.location.origin const oidClientID = windowConfig('OIDC_RP_CLIENT_ID') || process.env.OIDC_RP_CLIENT_ID redirectURL = redirectURL.replace(/([^:]\/)\/+/g, "$1"); @@ -1036,9 +1022,9 @@ export const getRegisterURL = async returnTo => { } export const getSSOLogoutURL = returnTo => { - const redirectURL = returnTo || windowConfig('LOGIN_REDIRECT_URL') || process.env.LOGIN_REDIRECT_URL + const redirectURL = returnTo || window.location.origin const idToken = localStorage.id_token - if(redirectURL && idToken) + if(isSSOEnabled() && idToken) return `${getAPIURL()}/users/logout/?id_token_hint=${idToken}&post_logout_redirect_uri=${redirectURL}` } @@ -1193,7 +1179,7 @@ export const toMapperURL = path => { if(['app.qa.openconceptlab.org', 'app.demo.openconceptlab.org'].includes(window.location.host)) url = 'https://map.qa.openconceptlab.org' if(window.location.host.match('app.*.openconceptlab.org')) - url = window.location.origin.replace('//app.', '//map.') + url = toAppOrigin(window.location.origin).replace('//app.', '//map.') // The Mapper reads auth only from the segment after the first '?' in the referrer's hash, so drop this page's own query let referrerParams = `referrer=${window.location.href.split('?')[0]}` @@ -1271,7 +1257,7 @@ export const toV3URL = path => { if(window.location.host?.includes('localhost')) url = 'http://localhost:4002' if(window.location.host.match('app.*.openconceptlab.org')) - url = window.location.origin.replace('//app.', '//app.v3.') + url = toAppOrigin(window.location.origin).replace('//app.', '//app.v3.') let referrerParams = `referrer=${window.location.href}` if(isLoggedIn()) diff --git a/src/components/users/OIDLoginCallback.jsx b/src/components/users/OIDLoginCallback.jsx index ed4706a1..d4bd65e5 100644 --- a/src/components/users/OIDLoginCallback.jsx +++ b/src/components/users/OIDLoginCallback.jsx @@ -36,7 +36,7 @@ class OIDLoginCallback extends React.Component { return } this.setState({next: next && next !== '/' ? next : null }, () => { - const redirectURL = this.state.next ? window.location.origin + this.state.next : (windowConfig('LOGIN_REDIRECT_URL') || process.env.LOGIN_REDIRECT_URL) + const redirectURL = this.state.next ? window.location.origin + this.state.next : window.location.origin const clientId = windowConfig('OIDC_RP_CLIENT_ID') || process.env.OIDC_RP_CLIENT_ID APIService.users().appendToUrl('oidc/code-exchange/').post({code: code, redirect_uri: redirectURL, client_id: clientId, code_verifier: codeVerifier}).then(res => {