Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove domain prop in TokenProvider #803

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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 packages/app-elements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"ts:check": "tsc --noEmit"
},
"dependencies": {
"@commercelayer/js-auth": "^6.6.2",
"@commercelayer/sdk": "6.22.0",
"@monaco-editor/react": "4.6.0",
"@types/lodash": "^4.17.10",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { TokenProvider, type TokenProviderProps } from './TokenProvider'
// slug is `giuseppe`
// kind is `integration`
const accessToken =
'eyJhbGciOiJIUzUxMiJ9.eyJvcmdhbml6YXRpb24iOnsiaWQiOiJrUk1lakZXalpSIiwic2x1ZyI6ImdpdXNlcHBlIiwiZW50ZXJwcmlzZSI6ZmFsc2V9LCJhcHBsaWNhdGlvbiI6eyJpZCI6IkFwUGtaaWxWQk0iLCJraW5kIjoiaW50ZWdyYXRpb24iLCJwdWJsaWMiOmZhbHNlfSwidGVzdCI6dHJ1ZSwiZXhwIjoxNjc1Njg0Mzk5LCJyYW5kIjowLjg1ODA5MzgzOTA3OTQ1OTZ9.fake-signature-test'
'eyJhbGciOiJIUzUxMiJ9.eyJvcmdhbml6YXRpb24iOnsiaWQiOiJrUk1lakZXalpSIiwic2x1ZyI6ImdpdXNlcHBlIiwiZW50ZXJwcmlzZSI6ZmFsc2V9LCJhcHBsaWNhdGlvbiI6eyJpZCI6IkFwUGtaaWxWQk0iLCJraW5kIjoiaW50ZWdyYXRpb24iLCJwdWJsaWMiOmZhbHNlfSwidGVzdCI6dHJ1ZSwiZXhwIjoxNjc1Njg0Mzk5LCJyYW5kIjowLjg1ODA5MzgzOTA3OTQ1OTYsImlzcyI6Imh0dHBzOi8vYXV0aC5jb21tZXJjZWxheWVyLmlvIn0.fake-signature-test'
const accessTokenLive =
'eyJhbGciOiJIUzUxMiJ9.eyJvcmdhbml6YXRpb24iOnsiaWQiOiJrUk1lakZXalpSIiwic2x1ZyI6ImdpdXNlcHBlIiwiZW50ZXJwcmlzZSI6ZmFsc2V9LCJhcHBsaWNhdGlvbiI6eyJpZCI6IkFwUGtaaWxWQk0iLCJraW5kIjoiaW50ZWdyYXRpb24iLCJwdWJsaWMiOmZhbHNlfSwidGVzdCI6dHJ1ZSwiZXhwIjoxNjc1Njg0Mzk5LCJyYW5kIjowLjg1ODA5MzgzOTA3OTQ1OTZ9.fake-signature-live'
'eyJhbGciOiJIUzUxMiJ9.eyJvcmdhbml6YXRpb24iOnsiaWQiOiJrUk1lakZXalpSIiwic2x1ZyI6ImdpdXNlcHBlIiwiZW50ZXJwcmlzZSI6ZmFsc2V9LCJhcHBsaWNhdGlvbiI6eyJpZCI6IkFwUGtaaWxWQk0iLCJraW5kIjoiaW50ZWdyYXRpb24iLCJwdWJsaWMiOmZhbHNlfSwidGVzdCI6ZmFsc2UsImV4cCI6MTY3NTY4NDM5OSwicmFuZCI6MC44NTgwOTM4MzkwNzk0NTk2LCJpc3MiOiJodHRwczovL2F1dGguY29tbWVyY2VsYXllci5pbyJ9.fake-signature-live'
const validDateNow = new Date('2023-02-06T10:00:00.000Z')
const expiredDateNow = new Date('2023-02-10T10:00:00.000Z')

Expand Down Expand Up @@ -93,7 +93,7 @@ describe('TokenProvider', () => {
expect(getByText('can access orders: yes')).toBeVisible()
expect(getByText('can access exports: no')).toBeVisible()

expect(onInvalidAuth).toBeCalledTimes(0)
expect(onInvalidAuth).toHaveBeenCalledTimes(0)
})

test('Should return live mode if token comes from live environment', async () => {
Expand Down
23 changes: 12 additions & 11 deletions packages/app-elements/src/providers/TokenProvider/TokenProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { type TokenProviderTokenApplicationKind } from '#providers/TokenProvider'
import { decodeExtras, getExtrasFromUrl } from '#providers/TokenProvider/extras'
import { isProductionHostname } from '#providers/TokenProvider/url'
import { extractDomainFromApiBaseEndpoint } from '#providers/TokenProvider/url'
import { PageError } from '#ui/composite/PageError'
import { PageSkeleton } from '#ui/composite/PageSkeleton'
import { getCoreApiBaseEndpoint } from '@commercelayer/js-auth'
import type { ListableResourceType, Organization } from '@commercelayer/sdk'
import {
createContext,
Expand Down Expand Up @@ -68,10 +69,6 @@ export interface TokenProviderProps {
* and the token will be persisted using a default key ('commercelayer').
*/
organizationSlug?: string
/**
* The base domain to be used for Commerce Layer API requests (e.g. `commercelayer.io`)
*/
domain?: string
/**
* The callback invoked when token is not valid.
* Can be used to manually handle the re-authentication flow when `reauthenticateOnInvalidAuth` is false.
Expand Down Expand Up @@ -129,7 +126,6 @@ export const TokenProvider: React.FC<TokenProviderProps> = ({
devMode,
children,
organizationSlug,
domain = 'commercelayer.io',
onInvalidAuth,
loadingElement,
errorElement,
Expand All @@ -139,7 +135,6 @@ export const TokenProvider: React.FC<TokenProviderProps> = ({
extras: extrasFromProp
}) => {
const [_state, dispatch] = useReducer(reducer, initialTokenProviderState)
domain = isProductionHostname() ? 'commercelayer.io' : domain

const accessToken =
accessTokenFromProp ??
Expand All @@ -151,6 +146,10 @@ export const TokenProvider: React.FC<TokenProviderProps> = ({
getPersistentJWT({ appSlug, organizationSlug, itemType: 'extras' })
const extras = extrasFromProp ?? decodeExtras(encodeExtras)

const apiBaseEndpoint =
accessToken != null ? getCoreApiBaseEndpoint(accessToken) : null
const domain = extractDomainFromApiBaseEndpoint(apiBaseEndpoint)

const dashboardUrl = makeDashboardUrl({
domain,
accessToken
Expand Down Expand Up @@ -183,6 +182,11 @@ export const TokenProvider: React.FC<TokenProviderProps> = ({
useEffect(
function validateAndSetToken() {
void (async (): Promise<void> => {
if (apiBaseEndpoint == null) {
emitInvalidAuth('apiBaseEndpoint is missing')
return
}

if (accessToken == null) {
emitInvalidAuth('accessToken is missing')
return
Expand All @@ -201,7 +205,6 @@ export const TokenProvider: React.FC<TokenProviderProps> = ({
const tokenInfo = await isValidTokenForCurrentApp({
accessToken,
kind,
domain,
isProduction: !devMode,
currentMode: getCurrentMode({ accessToken }),
organizationSlug
Expand All @@ -217,9 +220,7 @@ export const TokenProvider: React.FC<TokenProviderProps> = ({
tokenInfo.permissions?.organizations?.read === true &&
appSlug !== 'dashboard'
? await getOrganization({
accessToken,
domain,
organizationSlug: tokenInfo.organizationSlug
accessToken
})
: null

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { getCoreApiBaseEndpoint } from '@commercelayer/js-auth'
import { type Organization } from '@commercelayer/sdk'
import fetch from 'cross-fetch'

export async function getOrganization({
accessToken,
organizationSlug,
domain
accessToken
}: {
accessToken: string
organizationSlug: string
domain: string
}): Promise<Organization | null> {
try {
const response = await fetch(
`https://${organizationSlug}.${domain}/api/organization`,
{
method: 'GET',
headers: { authorization: `Bearer ${accessToken}` }
}
)
const apiBaseEndpoint = getCoreApiBaseEndpoint(accessToken)
const response = await fetch(`${apiBaseEndpoint}/api/organization`, {
method: 'GET',
headers: { authorization: `Bearer ${accessToken}` }
})
const organization = await response.json()
return organization?.data?.attributes ?? null
} catch {
Expand Down
52 changes: 11 additions & 41 deletions packages/app-elements/src/providers/TokenProvider/url.test.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,15 @@
import { isProductionHostname } from './url'
import { extractDomainFromApiBaseEndpoint } from './url'

describe('isProductionHostname', () => {
const { location } = window
beforeAll(function clearLocation() {
delete (window as any).location
;(window as any).location = {
...location,
hostname: ''
}
})
afterAll(function resetLocation() {
window.location = location
})

test('should return true for production hostnames', () => {
;[
'org.commercelayer.app',
'org-123.commercelayer.app',
'123-456.commercelayer.app',
'dashboard.commercelayer.io',
'_org.commercelayer.app',
'org_.commercelayer.app',
'org-_.commercelayer.app',
'org_-.commercelayer.app',
'123.commercelayer.app',
'org123.commercelayer.app'
].forEach((hostname) => {
window.location.hostname = hostname
expect(isProductionHostname()).toBe(true)
})
})
describe('extractDomainFromApiBaseEndpoint', () => {
test('should return the domain from the apiBaseEndpoint', () => {
expect(
extractDomainFromApiBaseEndpoint('https://demo-store.commercelayer.io')
).toBe('commercelayer.io')

test('should return false for non-production hostnames', () => {
;[
'demo-store.stg.commercelayer.app',
'demo-store.stg.commercelayer.app.test',
'org.dashboard.commercelayer.io',
'dashboard.commercelayer.io.test'
].forEach((hostname) => {
window.location.hostname = hostname
expect(isProductionHostname()).toBe(false)
})
expect(
extractDomainFromApiBaseEndpoint(
'https://demo-store.foo.commercelayer.io'
)
).toBe('foo.commercelayer.io')
})
})
13 changes: 6 additions & 7 deletions packages/app-elements/src/providers/TokenProvider/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ export function makeDashboardUrl({
return `https://dashboard.${domain}/${mode}/${orgSlug}`
}

export function isProductionHostname(): boolean {
if (typeof window !== 'undefined') {
return /^[\w-]+\.commercelayer\.app$|^dashboard\.commercelayer\.io$/.test(
window.location.hostname
)
export function extractDomainFromApiBaseEndpoint(
apiBaseEndpoint?: string | null
): string {
if (apiBaseEndpoint == null) {
return 'commercelayer.io'
}

return false
return apiBaseEndpoint.replace('https://', '').split('.').slice(1).join('.')
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isTokenExpired, isValidTokenForCurrentApp } from './validateToken'

const token =
'eyJhbGciOiJIUzUxMiJ9.eyJvcmdhbml6YXRpb24iOnsiaWQiOiJkWGttWkZNcUdSIiwic2x1ZyI6ImdpdXNlcHBlLWltcG9ydHMiLCJlbnRlcnByaXNlIjpmYWxzZX0sImFwcGxpY2F0aW9uIjp7ImlkIjoiYU1hS21pYW5CTiIsImtpbmQiOiJpbnRlZ3JhdGlvbiIsInB1YmxpYyI6ZmFsc2V9LCJ0ZXN0Ijp0cnVlLCJleHAiOjE2NjE4NjA4NzksInJhbmQiOjAuNDIzNzM0OTczNTE3NzY0OH0.1dJs_MjNl8rEa8KOSFNea921LS-PpVOaM65kNqL-yFYy4NJdpZ_HHTNAWCCX2LXV2RQ5cg241CvxPJz3IhFw2g'
'eyJhbGciOiJIUzUxMiJ9.eyJvcmdhbml6YXRpb24iOnsiaWQiOiJkWGttWkZNcUdSIiwic2x1ZyI6ImdpdXNlcHBlLWltcG9ydHMiLCJlbnRlcnByaXNlIjpmYWxzZX0sImFwcGxpY2F0aW9uIjp7ImlkIjoiYU1hS21pYW5CTiIsImtpbmQiOiJpbnRlZ3JhdGlvbiIsInB1YmxpYyI6ZmFsc2V9LCJ0ZXN0Ijp0cnVlLCJleHAiOjE2NjE4NjA4NzksInJhbmQiOjAuNDIzNzM0OTczNTE3NzY0OCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmNvbW1lcmNlbGF5ZXIuaW8ifQ.yw9TjnpUDUyqeyJ0xv7AS-Suq0TIh7GIAtLyEDvG0yy8t94XM4HojZ6sTU7o963qGOj9Ni7z4wUT4RihWWRpCw'

describe('isTokenExpired', () => {
test('should check expired token', () => {
Expand Down Expand Up @@ -31,7 +31,6 @@ describe('isValidTokenForCurrentApp', () => {
const tokenInfo = await isValidTokenForCurrentApp({
accessToken: token,
kind: 'integration',
domain: 'commercelayer.io',
isProduction: false,
currentMode: 'test'
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type TokenProviderAllowedApp,
type TokenProviderTokenApplicationKind
} from '#providers/TokenProvider'
import { getCoreApiBaseEndpoint } from '@commercelayer/js-auth'
import { type ListableResourceType } from '@commercelayer/sdk'
import fetch from 'cross-fetch'
import isEmpty from 'lodash/isEmpty'
Expand Down Expand Up @@ -49,14 +50,12 @@ interface InvalidToken {
export async function isValidTokenForCurrentApp({
accessToken,
kind,
domain,
isProduction,
currentMode,
organizationSlug
}: {
accessToken: string
kind: TokenProviderTokenApplicationKind
domain: string
isProduction: boolean
currentMode: Mode
/**
Expand All @@ -83,9 +82,9 @@ export async function isValidTokenForCurrentApp({
try {
const tokenInfo = await fetchTokenInfo({
accessToken,
orgSlug: jwtInfo.orgSlug,
domain
orgSlug: jwtInfo.orgSlug
})

const isValidOnCore = Boolean(tokenInfo?.token)
const isValidKind = jwtInfo.appKind === kind
const isValidOrganizationSlug = isEmpty(organizationSlug)
Expand Down Expand Up @@ -152,16 +151,15 @@ export async function isValidTokenForCurrentApp({

async function fetchTokenInfo({
accessToken,
orgSlug,
domain
orgSlug
}: {
accessToken: string
orgSlug: string
domain: string
}): Promise<TokenProviderTokenInfo | null> {
try {
const coreApiBaseEndpoint = getCoreApiBaseEndpoint(accessToken)
const tokenInfoResponse = await fetch(
`https://${orgSlug}.${domain}/oauth/tokeninfo`,
`${coreApiBaseEndpoint}/oauth/tokeninfo`,
{
method: 'GET',
headers: { authorization: `Bearer ${accessToken}` }
Expand Down
8 changes: 1 addition & 7 deletions packages/app-elements/src/providers/createApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ type ClApp = Record<
declare global {
interface Window extends ClApp {
clAppConfig: {
/**
* Specific domain to use for Commerce Layer API requests.
* It must be set as `commercelayer.io`.
*/
domain: string
/**
* Enable Google Tag Manager for the provided GTM ID.
*/
Expand All @@ -35,7 +30,7 @@ declare global {
export interface ClAppProps
extends Pick<
TokenProviderProps,
'organizationSlug' | 'domain' | 'onAppClose' | 'isInDashboard' | 'extras'
'organizationSlug' | 'onAppClose' | 'isInDashboard' | 'extras'
> {
/**
* Base path for internal routing.
Expand Down Expand Up @@ -67,7 +62,6 @@ export function createApp(
root.render(
children({
...props,
domain: props?.domain ?? window.clAppConfig?.domain,
organizationSlug: parseOrganizationSlug(props?.organizationSlug),
routerBase: parseRouterBase(props?.routerBase)
})
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.