Skip to content

Commit

Permalink
[Feature] Add onAuthExpired listener (#396)
Browse files Browse the repository at this point in the history
* [Feature] Add onTokenExpired listener

* [Feature] OnTokenExpired > onAuthExpired

* [Feature] Pass connector id to the listener

* [Feature] Support for 3rd party auth (#400)

* [Feature] Support for 3rd party auth

* [Feature] Updated tests

* [Feature] isGrafxToken > type (enum)
  • Loading branch information
Dvergar authored Jan 4, 2024
1 parent 9779d16 commit a4e31e7
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 5 deletions.
26 changes: 24 additions & 2 deletions packages/sdk/src/controllers/SubscriberController.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -93,15 +94,36 @@ 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;
callBack && callBack();
};

/**
* 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<string | null>((resolve) => resolve(null));
};

/**
* Listener on when the document is fully loaded.
*/
onDocumentLoaded = () => {
const callBack = this.config.onDocumentLoaded;
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/src/interactions/connector.ts
Original file line number Diff line number Diff line change
@@ -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]+\/$/);
Expand Down Expand Up @@ -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<string | null>;
onDocumentLoaded: () => void;
onSelectedFramesContentChanged: (state: string) => void;
onSelectedFramesLayoutChanged: (state: string) => void;
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/tests/__mocks__/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
69 changes: 68 additions & 1 deletion packages/sdk/src/tests/controllers/SubscriberContoller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<AuthCredentials | null>((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<AuthCredentials | null>((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);
});
});
});
5 changes: 3 additions & 2 deletions packages/sdk/src/types/CommonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -20,13 +20,14 @@ export type Id = string;
export type ConfigType = {
onActionsChanged?: (state: DocumentAction[]) => void;
onStateChanged?: () => void;
onAuthExpired?: (connectorId: string, type: AuthRefreshTypeEnum) => Promise<AuthCredentials | null>;
onDocumentLoaded?: () => void;
/**
* @deprecated use `onSelectedFramesLayoutChanged` instead
*
*/
onSelectedFrameLayoutChanged?: (state: FrameLayoutType) => void;
onSelectedFramesLayoutChanged?: (state: FrameLayoutType[]) => void;
onSelectedFramesLayoutChanged?: (states: FrameLayoutType[]) => void;
/**
* @deprecated use `onSelectedFramesContentChanged` instead
*/
Expand Down
32 changes: 32 additions & 0 deletions packages/sdk/src/types/ConnectorTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}

0 comments on commit a4e31e7

Please sign in to comment.