diff --git a/packages/sdk/src/controllers/SubscriberController.ts b/packages/sdk/src/controllers/SubscriberController.ts index c2684822..ed5f96c3 100644 --- a/packages/sdk/src/controllers/SubscriberController.ts +++ b/packages/sdk/src/controllers/SubscriberController.ts @@ -1,4 +1,5 @@ import { ConfigType, Id } from '../types/CommonTypes'; +import { AuthRefreshTypeEnum as AuthRefreshTypeEnum } from '../types/ConnectorTypes'; import { MeasurementUnit } from '../types/LayoutTypes'; import { ToolType } from '../utils/enums'; @@ -93,7 +94,7 @@ export class SubscriberController { }; /** - * A listener on the general state of the document, gets triggered every time a change is done on the document. + * Listener on the general state of the document, gets triggered every time a change is done on the document. */ onStateChanged = () => { const callBack = this.config.onStateChanged; @@ -101,7 +102,28 @@ export class SubscriberController { }; /** - * A listener on when the document is fully loaded. + * Listener on authentication expiration. + * The callback should resolve to the refreshed authentication. If the + * listener is not defined, the http requests from the connector will return + * 401 with no refetch of assets. + * + * When this emits it means either: + * - the grafxToken needs to be renewed + * - the 3rd party auth (user impersonation) needs to be renewed. + * + * @param connectorId connector id + * @param type the type of auth renewal needed + */ + onAuthExpired = (connectorId: string, type: AuthRefreshTypeEnum) => { + const callBack = this.config.onAuthExpired; + + return callBack + ? callBack(connectorId, type).then((auth) => JSON.stringify(auth)) + : new Promise((resolve) => resolve(null)); + }; + + /** + * Listener on when the document is fully loaded. */ onDocumentLoaded = () => { const callBack = this.config.onDocumentLoaded; diff --git a/packages/sdk/src/interactions/connector.ts b/packages/sdk/src/interactions/connector.ts index 0b148978..dcb6cc96 100644 --- a/packages/sdk/src/interactions/connector.ts +++ b/packages/sdk/src/interactions/connector.ts @@ -1,6 +1,7 @@ import { Connection, connectToChild } from 'penpal'; import { Id } from '../types/CommonTypes'; import { StudioStyling } from '../types/ConfigurationTypes'; +import { AuthRefreshTypeEnum } from '../types/ConnectorTypes'; export const validateEditorLink = (editorLink: string) => { const linkValidator = new RegExp(/^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w.-]+)+[\w]+\/$/); @@ -52,6 +53,7 @@ export const setupFrame = (iframe: HTMLIFrameElement, editorLink: string, stylin interface ConfigParameterTypes { onActionsChanged: (state: string) => void; onStateChanged: (state: string) => void; + onAuthExpired: (connectorId: string, refreshType: AuthRefreshTypeEnum) => Promise; onDocumentLoaded: () => void; onSelectedFramesContentChanged: (state: string) => void; onSelectedFramesLayoutChanged: (state: string) => void; @@ -119,6 +121,7 @@ const Connect = ( actionsChanged: params.onActionsChanged, stateChanged: params.onStateChanged, documentLoaded: params.onDocumentLoaded, + authExpired: params.onAuthExpired, selectedFramesContent: params.onSelectedFramesContentChanged, selectedFramesLayout: params.onSelectedFramesLayoutChanged, selectedLayoutProperties: params.onSelectedLayoutPropertiesChanged, diff --git a/packages/sdk/src/sdk.ts b/packages/sdk/src/sdk.ts index 87a86804..2c831c0d 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk/src/sdk.ts @@ -128,6 +128,7 @@ export class SDK { { onActionsChanged: this.subscriber.onActionsChanged, onStateChanged: this.subscriber.onStateChanged, + onAuthExpired: this.subscriber.onAuthExpired, onDocumentLoaded: this.subscriber.onDocumentLoaded, onSelectedFramesContentChanged: this.subscriber.onSelectedFramesContentChanged, onSelectedFramesLayoutChanged: this.subscriber.onSelectedFramesLayoutChanged, diff --git a/packages/sdk/src/tests/__mocks__/config.ts b/packages/sdk/src/tests/__mocks__/config.ts index 682be8b9..a6a83d5d 100644 --- a/packages/sdk/src/tests/__mocks__/config.ts +++ b/packages/sdk/src/tests/__mocks__/config.ts @@ -6,6 +6,7 @@ export const defaultMockReturn = jest.fn().mockResolvedValue({ success: true, st const mockConfig: ConfigType = { onActionsChanged: defaultMockReturn, onStateChanged: defaultMockReturn, + onAuthExpired: defaultMockReturn, onDocumentLoaded: defaultMockReturn, onSelectedFrameLayoutChanged: defaultMockReturn, onSelectedFramesLayoutChanged: defaultMockReturn, diff --git a/packages/sdk/src/tests/controllers/SubscriberContoller.test.ts b/packages/sdk/src/tests/controllers/SubscriberContoller.test.ts index e504223b..33265f77 100644 --- a/packages/sdk/src/tests/controllers/SubscriberContoller.test.ts +++ b/packages/sdk/src/tests/controllers/SubscriberContoller.test.ts @@ -6,7 +6,14 @@ import { FrameAnimationType } from '../../types/AnimationTypes'; import { VariableType } from '../../types/VariableTypes'; import { ToolType } from '../../utils/enums'; -import { ConnectorStateType } from '../../types/ConnectorTypes'; +import { + AuthCredentials, + ConnectorStateType, + RefreshedAuthCredendentials, + GrafxTokenAuthCredentials, + AuthCredentialsTypeEnum, + AuthRefreshTypeEnum, +} from '../../types/ConnectorTypes'; import type { PageSize } from '../../types/PageTypes'; import { CornerRadiusUpdateModel } from '../../types/ShapeTypes'; import { AsyncError, EditorAPI } from '../../types/CommonTypes'; @@ -265,4 +272,64 @@ describe('SubscriberController', () => { expect(mockEditorApi.onAsyncError).toHaveBeenCalledTimes(1); expect(mockEditorApi.onAsyncError).toHaveBeenCalledWith(asyncError); }); + + describe('onAuthExpired', () => { + const connectorId = 'connectorId'; + + it('returns the token defined by the callback', async () => { + const refreshedToken = 'newToken'; + + const mockConfig = { + onAuthExpired() { + return new Promise((resolve) => + resolve(new GrafxTokenAuthCredentials(refreshedToken)), + ); + }, + }; + + jest.spyOn(mockConfig, 'onAuthExpired'); + const mockedSubscriberController = new SubscriberController(mockConfig); + + const resultJsonString = await mockedSubscriberController.onAuthExpired( + connectorId, + AuthRefreshTypeEnum.grafxToken, + ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const resultAuth: GrafxTokenAuthCredentials = JSON.parse(resultJsonString!); + + expect(resultAuth.token).toBe(refreshedToken); + expect(mockConfig.onAuthExpired).toHaveBeenCalledWith(connectorId, AuthRefreshTypeEnum.grafxToken); + expect(mockConfig.onAuthExpired).toHaveBeenCalledTimes(1); + }); + + it('returns the notification defined by the callback', async () => { + const mockConfig = { + onAuthExpired() { + return new Promise((resolve) => resolve(new RefreshedAuthCredendentials())); + }, + }; + + jest.spyOn(mockConfig, 'onAuthExpired'); + const mockedSubscriberController = new SubscriberController(mockConfig); + + const resultJsonString = await mockedSubscriberController.onAuthExpired( + connectorId, + AuthRefreshTypeEnum.user, + ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const resultAuth = JSON.parse(resultJsonString!); + + expect(resultAuth.type).toBe(AuthCredentialsTypeEnum.refreshed); + expect(mockConfig.onAuthExpired).toHaveBeenCalledWith(connectorId, AuthRefreshTypeEnum.user); + expect(mockConfig.onAuthExpired).toHaveBeenCalledTimes(1); + }); + + it('returns a null token if the listener is not defined', async () => { + const mockedSubscriberController = new SubscriberController({}); + + const result = await mockedSubscriberController.onAuthExpired(connectorId, AuthRefreshTypeEnum.grafxToken); + + expect(result).toBe(null); + }); + }); }); diff --git a/packages/sdk/src/types/CommonTypes.ts b/packages/sdk/src/types/CommonTypes.ts index e5a4b32d..d57f2c65 100644 --- a/packages/sdk/src/types/CommonTypes.ts +++ b/packages/sdk/src/types/CommonTypes.ts @@ -9,7 +9,7 @@ import { DocumentType, UndoState } from './DocumentTypes'; import { DocumentColor } from './ColorStyleTypes'; import { ParagraphStyle } from './ParagraphStyleTypes'; import { CharacterStyle } from './CharacterStyleTypes'; -import { ConnectorEvent } from './ConnectorTypes'; +import { AuthCredentials, ConnectorEvent, AuthRefreshTypeEnum } from './ConnectorTypes'; import { PageSize } from './PageTypes'; import { SelectedTextStyle } from './TextStyleTypes'; import { CornerRadiusUpdateModel } from './ShapeTypes'; @@ -20,13 +20,14 @@ export type Id = string; export type ConfigType = { onActionsChanged?: (state: DocumentAction[]) => void; onStateChanged?: () => void; + onAuthExpired?: (connectorId: string, type: AuthRefreshTypeEnum) => Promise; onDocumentLoaded?: () => void; /** * @deprecated use `onSelectedFramesLayoutChanged` instead * */ onSelectedFrameLayoutChanged?: (state: FrameLayoutType) => void; - onSelectedFramesLayoutChanged?: (state: FrameLayoutType[]) => void; + onSelectedFramesLayoutChanged?: (states: FrameLayoutType[]) => void; /** * @deprecated use `onSelectedFramesContentChanged` instead */ diff --git a/packages/sdk/src/types/ConnectorTypes.ts b/packages/sdk/src/types/ConnectorTypes.ts index 691eb0d7..fea2098a 100644 --- a/packages/sdk/src/types/ConnectorTypes.ts +++ b/packages/sdk/src/types/ConnectorTypes.ts @@ -212,3 +212,35 @@ export const grafxMediaConnectorRegistration: ConnectorLocalRegistration = { url: 'grafx-media.json', source: ConnectorRegistrationSource.local, }; + +/** + * Grafx token to return to the engine when it expires. + */ +export class GrafxTokenAuthCredentials { + token: string; + type = AuthCredentialsTypeEnum.grafxToken; + + constructor(token: string) { + this.token = token; + } +} + +/** + * Notification to return to the engine whenever a 3rd party auth (user impersonation) + * has been renewed by the integration. + */ +export class RefreshedAuthCredendentials { + type = AuthCredentialsTypeEnum.refreshed; +} + +export enum AuthCredentialsTypeEnum { + grafxToken, + refreshed, +} + +export type AuthCredentials = GrafxTokenAuthCredentials | RefreshedAuthCredendentials; + +export enum AuthRefreshTypeEnum { + grafxToken = 'grafxToken', + user = 'user', +}