diff --git a/packages/developer-portal/src/core/__styles__/nav.ts b/packages/developer-portal/src/core/__styles__/nav.ts new file mode 100644 index 0000000000..019e59822b --- /dev/null +++ b/packages/developer-portal/src/core/__styles__/nav.ts @@ -0,0 +1,31 @@ +import { css } from '@linaria/core' + +export const NavWithOrgPicker = css` + .el-nav-menu-option:first-child { + height: auto; + } + .el-nav-menu-option:first-child:hover { + cursor: default; + color: var(--color-black); + } + .el-nav-item:nth-last-of-type(2) { + margin-right: auto !important; + } + .el-nav-item:last-of-type { + margin-right: 0 !important; + background: none !important; + } + + @media screen and (min-width: 768px) { + .el-nav-item:last-of-type { + margin-right: 0 !important; + } + } + + .el-nav-menu-option:first-of-type { + padding: 0; + margin-top: -0.5rem; + margin-bottom: 0.5rem; + border-bottom: 1px solid var(--color-grey-light); + } +` diff --git a/packages/developer-portal/src/core/connect-session.ts b/packages/developer-portal/src/core/connect-session.ts index eb1e890bfd..ac3b3ca0e2 100644 --- a/packages/developer-portal/src/core/connect-session.ts +++ b/packages/developer-portal/src/core/connect-session.ts @@ -1,7 +1,38 @@ import { ReapitConnectBrowserSession } from '@reapit/connect-session' +import { v4 as uuid } from 'uuid' + +class ReapitConnectBrowserSessionExtended extends ReapitConnectBrowserSession { + constructor(config) { + super(config) + } + + public async changeOrg(chosenOrgId: string, chosenOrgType?: string, redirectUri?: string): Promise { + const authRedirectUri = redirectUri || this['connectLoginRedirectPath'] + const params = new URLSearchParams(window.location.search) + params.delete('code') + const search = params ? `?${params.toString()}` : '' + const internalRedirectPath = encodeURIComponent(`${window.location.pathname}${search}`) + const stateNonce = uuid() + this['refreshTokenStorage'].setItem(stateNonce, internalRedirectPath) + const code_challenge = await this['encryptCodeVerifier'](this['codeVerifier'](stateNonce)) + + const orgChoice = {} + if (chosenOrgId) { + orgChoice['chosenOrgId'] = chosenOrgId + } + if (chosenOrgType) { + orgChoice['chosenOrgType'] = chosenOrgType + } + + let location = `${this['connectOAuthUrl']}/authorize?response_type=code&client_id=${this['connectClientId']}&redirect_uri=${authRedirectUri}&state=${stateNonce}&${new URLSearchParams(orgChoice).toString()}` + if (this['usePKCE']) location += `&code_challenge_method=S256&code_challenge=${code_challenge}` + + window.location.href = location + } +} // Needs to be a singleton as the class is stateful -export const reapitConnectBrowserSession = new ReapitConnectBrowserSession({ +export const reapitConnectBrowserSession = new ReapitConnectBrowserSessionExtended({ connectClientId: process.env.connectClientId, connectOAuthUrl: process.env.connectOAuthUrl, connectLoginRedirectPath: '/apps', diff --git a/packages/developer-portal/src/core/menu.tsx b/packages/developer-portal/src/core/menu.tsx index c56a0353bb..c6cb886e3b 100644 --- a/packages/developer-portal/src/core/menu.tsx +++ b/packages/developer-portal/src/core/menu.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react' import { useNavigate, useLocation } from 'react-router' import Routes from '../constants/routes' -import { Icon, NavResponsive, NavResponsiveAvatarOption, NavResponsiveOption } from '@reapit/elements' +import { ElChipLabel, Icon, NavResponsive, NavResponsiveAvatarOption, NavResponsiveOption } from '@reapit/elements' import { memo } from 'react' import { navigateRoute, openNewPage } from '../utils/navigation' // Comment out after Christmas @@ -18,12 +18,132 @@ import { reapitConnectBrowserSession } from './connect-session' import { validate as isUuid } from 'uuid' import { getAvatarInitials } from '@reapit/utils-react' import { useGlobalState } from './use-global-state' +import { NavWithOrgPicker } from './__styles__/nav' +import { css } from '@linaria/core' const XmasImage = styled.img` height: 2.5rem; width: 2.5rem; ` +const CurrentOrgNameStyled = styled(ElChipLabel)` + margin-right: 0.5rem; +` + +function parseJwt(token: string) { + const base64Url = token.split('.')[1] + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') + const jsonPayload = decodeURIComponent( + window + .atob(base64) + .split('') + .map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + }) + .join(''), + ) + + return JSON.parse(jsonPayload) +} + +const useOrgTypes = () => { + const { connectSession } = useReapitConnect(reapitConnectBrowserSession) + const b = connectSession?.idToken && parseJwt(connectSession?.idToken) + if (!b) { + return null + } + + try { + const orgTypes = JSON.parse(b.organisationTypes) as { + id: string + name: string + types: ('customer' | 'developer' | 'organisation')[] + }[] + + return orgTypes + } catch { + return null + } +} + +const CurrentOrgName = () => { + const orgTypes = useOrgTypes() + const { connectSession } = useReapitConnect(reapitConnectBrowserSession) + const loginIdentity = selectLoginIdentity(connectSession) + + if (orgTypes?.map((o) => o.types).flat().length === 1) { + return null + } + + return {loginIdentity?.orgName} +} + +const ChooseableOrgType = css` + cursor: pointer; + + &:hover { + color: var(--intent-primary); + } +` + +const OrgPicker = () => { + const orgTypes = useOrgTypes() + + const switchOrg = (orgId: string, orgType?: string) => { + reapitConnectBrowserSession.changeOrg(orgId, orgType) + } + + if (!orgTypes || orgTypes.length <= 1) { + return null + } + + return ( +
+

Switch Organisation

+ +
+ ) +} + dayjs.extend(isBetween) export const XmasLogo: React.FC = () => { @@ -211,10 +331,15 @@ export const Menu: FC = () => { }, ], }, + { + itemIndex: 23, + text: , + }, ].filter(Boolean) as NavResponsiveOption[] return ( { avatarText={getAvatarInitials(connectSession)} avatarOptions={ [ + { + text: , + }, { callback: navigateRoute(navigate, Routes.SETTINGS_PROFILE), text: 'Profile',