Skip to content
Merged
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
9 changes: 8 additions & 1 deletion src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from './utils/authService';
import { isActiveService } from './utils/authService/tokenizedAuthService';
import { logger } from './utils/logger';
import { getSessionInfo } from './utils/sessionInfoService';
import { getSessionInfo, getPreauthInfo } from './utils/sessionInfoService';
import { ERROR_MESSAGE } from './errors';

// eslint-disable-next-line import/no-mutable-exports
Expand Down Expand Up @@ -55,6 +55,11 @@ export enum AuthStatus {
* Emits when the SDK authenticates successfully
*/
SDK_SUCCESS = 'SDK_SUCCESS',
/**
* @hidden
* Emits when iframe is loaded and session info is available
*/
SESSION_INFO_SUCCESS = 'SESSION_INFO_SUCCESS',
/**
* Emits when the app sends an authentication success message
*/
Expand Down Expand Up @@ -168,6 +173,7 @@ export async function notifyAuthSuccess(): Promise<void> {
return;
}
try {
getPreauthInfo();
const sessionInfo = await getSessionInfo();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we replace this getSessionInfo with the getPreauthInfo here? Better to avoid multiple api calls.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or update the getSessionInfo to call getPreauthInfo internally and replace the dependency on old /info call

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is decided with TSE to keep both APIs as older customers won't have preauth info API. Check more detail here - https://thoughtspot.slack.com/archives/C082G97N8BZ/p1732810938126219

Screenshot 2025-02-13 at 9 17 39 AM

authEE.emit(AuthStatus.SUCCESS, sessionInfo);
} catch (e) {
Expand Down Expand Up @@ -224,6 +230,7 @@ async function isLoggedIn(thoughtSpotHost: string): Promise<boolean> {
*/
export async function postLoginService(): Promise<void> {
try {
getPreauthInfo();
const sessionInfo = await getSessionInfo();
releaseVersion = sessionInfo.releaseVersion;
const embedConfig = getEmbedConfig();
Expand Down
96 changes: 95 additions & 1 deletion src/embed/ts-embed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ import * as mixpanelInstance from '../mixpanel-service';
import * as authInstance from '../auth';
import * as baseInstance from './base';
import { MIXPANEL_EVENT } from '../mixpanel-service';
import * as authService from '../utils/authService/authService';
import * as authService from '../utils/authService';
import { logger } from '../utils/logger';
import { version } from '../../package.json';
import { HiddenActionItemByDefaultForSearchEmbed } from './search';
import { processTrigger } from '../utils/processTrigger';
import { UIPassthroughEvent } from './hostEventClient/contracts';
import * as sessionInfoService from '../utils/sessionInfoService';

jest.mock('../utils/processTrigger');

Expand Down Expand Up @@ -1147,6 +1148,99 @@ describe('Unit test case for ts embed', () => {
});
});

describe('Trigger infoSuccess event on iframe load', () => {
beforeAll(() => {
jest.clearAllMocks();
init({
thoughtSpotHost,
authType: AuthType.None,
loginFailedMessage: 'Failed to Login',
});
});

const setup = async (isLoggedIn = false, overrideOrgId: number | undefined = undefined) => {
jest.spyOn(window, 'addEventListener').mockImplementationOnce(
(event, handler, options) => {
handler({
data: {
type: 'xyz',
},
ports: [3000],
source: null,
});
},
);
mockProcessTrigger.mockResolvedValueOnce({ session: 'test' });
// resetCachedPreauthInfo();
let mockGetPreauthInfo = null;

if (overrideOrgId) {
mockGetPreauthInfo = jest.spyOn(sessionInfoService, 'getPreauthInfo').mockImplementation(jest.fn());
}

const mockPreauthInfoFetch = jest.spyOn(authService, 'fetchPreauthInfoService').mockResolvedValueOnce({
ok: true,
headers: new Headers({ 'content-type': 'application/json' }), // Mock headers correctly
json: async () => ({
info: {
configInfo: {
mixpanelConfig: {
devSdkKey: 'devSdkKey',
},
},
userGUID: 'userGUID',
},
}), // Mock JSON response
});
const iFrame: any = document.createElement('div');
jest.spyOn(baseInstance, 'getAuthPromise').mockResolvedValueOnce(isLoggedIn);
const tsEmbed = new SearchEmbed(getRootEl(), {
overrideOrgId,
});
iFrame.contentWindow = {
postMessage: jest.fn(),
};
tsEmbed.on(EmbedEvent.CustomAction, jest.fn());
jest.spyOn(iFrame, 'addEventListener').mockImplementationOnce(
(event, handler, options) => {
handler({});
},
);
jest.spyOn(document, 'createElement').mockReturnValueOnce(iFrame);
await tsEmbed.render();

return {
mockPreauthInfoFetch,
mockGetPreauthInfo,
iFrame,
};
};

test('should call InfoSuccess Event on preauth call success', async () => {
const {
mockPreauthInfoFetch,
iFrame,
} = await setup(true);
expect(mockPreauthInfoFetch).toHaveBeenCalledTimes(1);

await executeAfterWait(() => {
expect(mockProcessTrigger).toHaveBeenCalledWith(
iFrame,
HostEvent.InfoSuccess,
'http://tshost',
expect.objectContaining({ info: expect.any(Object) }),
);
});
});

test('should not call InfoSuccess Event if overrideOrgId is true', async () => {
const {
mockGetPreauthInfo,
} = await setup(true, 123);
expect(mockGetPreauthInfo).toHaveBeenCalledTimes(0);
});
});

describe('when thoughtSpotHost have value and authPromise return error', () => {
beforeAll(() => {
init({
Expand Down
31 changes: 31 additions & 0 deletions src/embed/ts-embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
import { AuthFailureType } from '../auth';
import { getEmbedConfig } from './embedConfig';
import { ERROR_MESSAGE } from '../errors';
import { getPreauthInfo } from '../utils/sessionInfoService';
import { HostEventClient } from './hostEventClient/host-event-client';

const { version } = pkgInfo;
Expand Down Expand Up @@ -244,6 +245,24 @@ export class TsEmbed {
return null;
}

/**
* Checks if preauth cache is enabled
* from the view config and embed config
* @returns boolean
*/
private isPreAuthCacheEnabled() {
// Disable preauth cache when:
// 1. overrideOrgId is present since:
// - cached auth info would be for wrong org
// - info call response changes for each different overrideOrgId
// 2. disablePreauthCache is explicitly set to true
const isDisabled = (
this.viewConfig.overrideOrgId !== undefined
|| this.embedConfig.disablePreauthCache === true
);
return !isDisabled;
}

/**
* fix for ts7.sep.cl
* will be removed for ts7.oct.cl
Expand Down Expand Up @@ -586,6 +605,10 @@ export class TsEmbed {
queryParams[Param.OverrideOrgId] = overrideOrgId;
}

if (this.isPreAuthCacheEnabled()) {
queryParams[Param.preAuthCache] = true;
}

queryParams[Param.OverrideNativeConsole] = true;
queryParams[Param.ClientLogLevel] = this.embedConfig.logLevel;

Expand Down Expand Up @@ -722,6 +745,14 @@ export class TsEmbed {
elHeight: this.iFrame.clientHeight,
timeTookToLoad: loadTimestamp - initTimestamp,
});
// Send info event if preauth cache is enabled
if (this.isPreAuthCacheEnabled()) {
getPreauthInfo().then((data) => {
if (data?.info) {
this.trigger(HostEvent.InfoSuccess, data);
}
});
}
});
this.iFrame.addEventListener('error', () => {
nextInQueue();
Expand Down
4 changes: 2 additions & 2 deletions src/react/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('React Components', () => {
),
).toBe(true);
expect(getIFrameSrc(container)).toBe(
`http://${thoughtSpotHost}/?embedApp=true&hostAppUrl=local-host&viewPortHeight=768&viewPortWidth=1024&sdkVersion=${version}&authType=None&blockNonEmbedFullAppAccess=true&hideAction=[%22${Action.ReportError}%22,%22editACopy%22,%22saveAsView%22,%22updateTSL%22,%22editTSL%22,%22onDeleteAnswer%22]&overrideConsoleLogs=true&clientLogLevel=ERROR&enableDataPanelV2=false&dataSourceMode=hide&useLastSelectedSources=false&isSearchEmbed=true&collapseSearchBarInitially=true&enableCustomColumnGroups=false&dataPanelCustomGroupsAccordionInitialState=EXPAND_ALL#/embed/answer`,
`http://${thoughtSpotHost}/?embedApp=true&hostAppUrl=local-host&viewPortHeight=768&viewPortWidth=1024&sdkVersion=${version}&authType=None&blockNonEmbedFullAppAccess=true&hideAction=[%22${Action.ReportError}%22,%22editACopy%22,%22saveAsView%22,%22updateTSL%22,%22editTSL%22,%22onDeleteAnswer%22]&preAuthCache=true&overrideConsoleLogs=true&clientLogLevel=ERROR&enableDataPanelV2=false&dataSourceMode=hide&useLastSelectedSources=false&isSearchEmbed=true&collapseSearchBarInitially=true&enableCustomColumnGroups=false&dataPanelCustomGroupsAccordionInitialState=EXPAND_ALL#/embed/answer`,
);
});

Expand Down Expand Up @@ -230,7 +230,7 @@ describe('React Components', () => {
),
).toBe(true);
expect(getIFrameSrc(container)).toBe(
`http://${thoughtSpotHost}/?embedApp=true&hostAppUrl=local-host&viewPortHeight=768&viewPortWidth=1024&sdkVersion=${version}&authType=None&blockNonEmbedFullAppAccess=true&hideAction=[%22${Action.ReportError}%22]&overrideConsoleLogs=true&clientLogLevel=ERROR&dataSources=[%22test%22]&searchTokenString=%5Brevenue%5D&executeSearch=true&useLastSelectedSources=false&isSearchEmbed=true#/embed/search-bar-embed`,
`http://${thoughtSpotHost}/?embedApp=true&hostAppUrl=local-host&viewPortHeight=768&viewPortWidth=1024&sdkVersion=${version}&authType=None&blockNonEmbedFullAppAccess=true&hideAction=[%22${Action.ReportError}%22]&preAuthCache=true&overrideConsoleLogs=true&clientLogLevel=ERROR&dataSources=[%22test%22]&searchTokenString=%5Brevenue%5D&executeSearch=true&useLastSelectedSources=false&isSearchEmbed=true#/embed/search-bar-embed`,
);
});
});
Expand Down
16 changes: 14 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,8 @@ export interface EmbedConfig {
* @version SDK 1.37.0 | ThoughtSpot: 10.7.0.cl
*/
customVariablesForThirdPartyTools?: Record< string, any >;

disablePreauthCache?: boolean;
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
Expand Down Expand Up @@ -3269,8 +3271,17 @@ export enum HostEvent {
*/
UpdatePersonalisedView = 'UpdatePersonalisedView',
/**
* Triggers the action to get the current view of the Liveboard.
* @version SDK: 1.36.0 | ThoughtSpot: 10.6.0.cl
* @hidden
* Notify when info call is completed successfully
* ```js
* liveboardEmbed.trigger(HostEvent.InfoSuccess, data);
*```
* @version SDK: 1.36.0 | Thoughtspot: 10.6.0.cl
*/
InfoSuccess = 'InfoSuccess',
/**
* Triggers the action to get the current view of the liveboard
* @version SDK: 1.36.0 | Thoughtspot: 10.6.0.cl
*/
SaveAnswer = 'saveAnswer',
/**
Expand Down Expand Up @@ -3427,6 +3438,7 @@ export enum Param {
OauthPollingInterval = 'oAuthPollingInterval',
IsForceRedirect = 'isForceRedirect',
DataSourceId = 'dataSourceId',
preAuthCache = 'preAuthCache',
ShowSpotterLimitations = 'showSpotterLimitations',
}

Expand Down
1 change: 1 addition & 0 deletions src/utils/authService/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { logger } from '../logger';
export const EndPoints = {
AUTH_VERIFICATION: '/callosum/v1/session/info',
SESSION_INFO: '/callosum/v1/session/info',
PREAUTH_INFO: '/prism/preauth/info',
SAML_LOGIN_TEMPLATE: (targetUrl: string) => `/callosum/v1/saml/login?targetURLPath=${targetUrl}`,
OIDC_LOGIN_TEMPLATE: (targetUrl: string) => `/callosum/v1/oidc/login?targetURLPath=${targetUrl}`,
TOKEN_LOGIN: '/callosum/v1/session/login/token',
Expand Down
6 changes: 5 additions & 1 deletion src/utils/authService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ export {
fetchBasicAuthService,
verifyTokenService,
} from './authService';
export { fetchLogoutService, fetchSessionInfoService } from './tokenizedAuthService';
export {
fetchLogoutService,
fetchSessionInfoService,
fetchPreauthInfoService,
} from './tokenizedAuthService';
68 changes: 66 additions & 2 deletions src/utils/authService/tokenizedAuthService.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import * as tokenizedFetchModule from '../../tokenizedFetch';
import { isActiveService } from './tokenizedAuthService';
import { isActiveService, fetchSessionInfoService, fetchPreauthInfoService } from './tokenizedAuthService';
import { logger } from '../logger';
import { EndPoints } from './authService';

const thoughtspotHost = 'http://thoughtspotHost';

describe('tokenizedAuthService', () => {
test('isActiveService is fetch returns ok', async () => {
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
test('isActiveService if fetch returns ok', async () => {
jest.spyOn(tokenizedFetchModule, 'tokenizedFetch').mockResolvedValueOnce({
ok: true,
});
Expand Down Expand Up @@ -34,3 +41,60 @@ describe('tokenizedAuthService', () => {
expect(logger.warn).toHaveBeenCalled();
});
});

describe('fetchPreauthInfoService', () => {
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});

test('fetchPreauthInfoService if fetch returns ok', async () => {
const mockFetch = jest.spyOn(tokenizedFetchModule, 'tokenizedFetch');

// Mock for fetchPreauthInfoService
mockFetch
.mockResolvedValueOnce({
ok: true,
headers: new Headers({ 'content-type': 'application/json' }), // Mock headers correctly
status: 200,
statusText: 'Ok',
json: jest.fn().mockResolvedValue({
info: {
configInfo: {
mixpanelConfig: {
devSdkKey: 'devSdkKey',
},
},
userGUID: 'userGUID',
},
}),
});

const result = await fetchPreauthInfoService(thoughtspotHost);
const response = await result.json();

expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenNthCalledWith(1, `${thoughtspotHost}${EndPoints.PREAUTH_INFO}`, {});
expect(response).toHaveProperty('info');
});
it('fetchPreauthInfoService if fetch fails', async () => {
const mockFetch = jest.spyOn(tokenizedFetchModule, 'tokenizedFetch');

// Mock for fetchPreauthInfoService
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
json: jest.fn().mockResolvedValue({}),
text: jest.fn().mockResolvedValue('Internal Server Error'),
});

try {
await fetchPreauthInfoService(thoughtspotHost);
} catch (e) {
expect(e.message).toContain(`Failed to fetch ${thoughtspotHost}${EndPoints.PREAUTH_INFO}`);
}
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith(`${thoughtspotHost}${EndPoints.PREAUTH_INFO}`, {});
});
});
26 changes: 26 additions & 0 deletions src/utils/authService/tokenizedAuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,32 @@ function tokenizedFailureLoggedFetch(url: string, options: RequestInit = {}): Pr
});
}

/**
* Fetches the session info from the ThoughtSpot server.
* @param thoughtspotHost
* @returns {Promise<any>}
* @example
* ```js
* const response = await sessionInfoService();
* ```
*/
export async function fetchPreauthInfoService(thoughtspotHost: string): Promise<any> {
const sessionInfoPath = `${thoughtspotHost}${EndPoints.PREAUTH_INFO}`;
const handleError = (e: any) => {
const error: any = new Error(`Failed to fetch auth info: ${e.message || e.statusText}`);
error.status = e.status; // Attach the status code to the error object
throw error;
};

try {
const response = await tokenizedFailureLoggedFetch(sessionInfoPath);
return response;
} catch (e) {
handleError(e);
return null;
}
}

/**
* Fetches the session info from the ThoughtSpot server.
* @param thoughtspotHost
Expand Down
Loading