From 6ce29c4297a603ecb3afa2e3c5d1a40c2f27f300 Mon Sep 17 00:00:00 2001 From: ryanbas21 Date: Tue, 28 Jan 2025 15:19:50 -0700 Subject: [PATCH] feat: add-optional-query-params-to-start adds ability to add query parameters to the start call --- .changeset/gorgeous-cats-love.md | 5 +++ e2e/davinci-app/main.ts | 21 ++++++++-- e2e/davinci-app/tsconfig.spec.json | 1 + e2e/davinci-suites/src/basic.test.ts | 42 +++++++++++++++++++ .../davinci-client/src/lib/client.store.ts | 13 ++++-- .../davinci-client/src/lib/davinci.api.ts | 28 ++++++++++--- .../davinci-client/src/lib/davinci.types.ts | 8 ++++ .../davinci-client/src/lib/davinci.utils.ts | 1 + packages/davinci-client/src/lib/node.slice.ts | 6 ++- 9 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 .changeset/gorgeous-cats-love.md diff --git a/.changeset/gorgeous-cats-love.md b/.changeset/gorgeous-cats-love.md new file mode 100644 index 00000000..e89a9771 --- /dev/null +++ b/.changeset/gorgeous-cats-love.md @@ -0,0 +1,5 @@ +--- +'@forgerock/davinci-client': minor +--- + +adds the ability to call start with query parameters which are appended to the /authorize call diff --git a/e2e/davinci-app/main.ts b/e2e/davinci-app/main.ts index 38bb594c..2805320a 100644 --- a/e2e/davinci-app/main.ts +++ b/e2e/davinci-app/main.ts @@ -2,6 +2,7 @@ import './style.css'; import { Config, FRUser, TokenManager } from '@forgerock/javascript-sdk'; import { davinci } from '@forgerock/davinci-client'; +import type { DaVinciConfig } from '@forgerock/davinci-client/types'; import usernameComponent from './components/text.js'; import passwordComponent from './components/password.js'; @@ -10,9 +11,9 @@ import protect from './components/protect.js'; import flowLinkComponent from './components/flow-link.js'; import socialLoginButtonComponent from './components/social-login-button.js'; -const config = { +const config: DaVinciConfig = { clientId: '724ec718-c41c-4d51-98b0-84a583f450f9', - redirectUri: window.location.href, + redirectUri: window.location.origin + '/', scope: 'openid profile email name revoke', serverConfig: { wellknown: @@ -178,7 +179,21 @@ const config = { console.log('Event emitted from store:', node); }); - const node = await davinciClient.start(); + const qs = window.location.search; + const searchParams = new URLSearchParams(qs); + + const query: Record = {}; + + // Get all unique keys from the searchParams + const uniqueKeys = new Set(searchParams.keys()); + + // Iterate over the unique keys + for (const key of uniqueKeys) { + const values = searchParams.getAll(key); + query[key] = values.length > 1 ? values : values[0]; + } + console.log('query', query); + const node = await davinciClient.start({ query }); formEl.addEventListener('submit', async (event) => { event.preventDefault(); diff --git a/e2e/davinci-app/tsconfig.spec.json b/e2e/davinci-app/tsconfig.spec.json index 9d6a34eb..89677859 100644 --- a/e2e/davinci-app/tsconfig.spec.json +++ b/e2e/davinci-app/tsconfig.spec.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "moduleResolution": "NodeNext", "composite": true, "outDir": "../../dist/out-tsc", "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"] diff --git a/e2e/davinci-suites/src/basic.test.ts b/e2e/davinci-suites/src/basic.test.ts index 454260cf..04358a15 100644 --- a/e2e/davinci-suites/src/basic.test.ts +++ b/e2e/davinci-suites/src/basic.test.ts @@ -26,3 +26,45 @@ test('Test happy paths on test page', async ({ page }) => { const accessToken = await page.locator('#accessTokenValue').innerText(); await expect(accessToken).toBeTruthy(); }); +test('ensure query params passed to start are sent off in authorize call', async ({ page }) => { + const { navigate } = asyncEvents(page); + // Wait for the request to a URL containing '/authorize' + const requestPromise = page.waitForRequest((request) => { + return request + .url() + .includes('https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authorize'); + }); + await navigate('/?testParam=123'); + + // Wait for the request to be made to authorize + const request = await requestPromise; + + // Extract and verify the query parameters from authorize + const url = new URL(request.url()); + const queryParams = Object.fromEntries(url.searchParams.entries()); + + expect(queryParams['testParam']).toBe('123'); + expect(queryParams['client_id']).toBe('724ec718-c41c-4d51-98b0-84a583f450f9'); + expect(queryParams['response_mode']).toBe('pi.flow'); + + expect(page.url()).toBe('http://localhost:5829/?testParam=123'); + + await expect(page.getByText('Username/Password Form')).toBeVisible(); + + await page.getByLabel('Username').fill('demouser'); + await page.getByLabel('Password').fill('U.CDmhGLK*nrQPDWEN47ZMyJh'); + + await page.getByText('Sign On').click(); + + await expect(page.getByText('Complete')).toBeVisible(); + + const sessionToken = await page.locator('#sessionToken').innerText(); + const authCode = await page.locator('#authCode').innerText(); + expect(sessionToken).toBeTruthy(); + expect(authCode).toBeTruthy(); + + await page.getByText('Get Tokens').click(); + + const accessToken = await page.locator('#accessTokenValue').innerText(); + expect(accessToken).toBeTruthy(); +}); diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index 881d190d..9218d634 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -11,7 +11,12 @@ import { wellknownApi } from './wellknown.api.js'; * Import the DaVinciRequest types */ import type { DaVinciConfig } from './config.types.js'; -import type { DaVinciAction, DaVinciRequest } from './davinci.types.js'; +import type { + DaVinciAction, + DaVinciRequest, + OutgoingQueryParams, + StartOptions, +} from './davinci.types.js'; import type { SingleValueCollectors } from './collector.types.js'; import type { InitFlow, Updater } from './client.types.js'; @@ -94,8 +99,10 @@ export async function davinci({ config }: { config: DaVinciConfig }) { * @method start - Method for initiating a DaVinci flow * @returns {Promise} - a promise that initiates a DaVinci flow and returns a node */ - start: async () => { - await store.dispatch(davinciApi.endpoints.start.initiate()); + start: async ( + options?: StartOptions | undefined, + ) => { + await store.dispatch(davinciApi.endpoints.start.initiate(options)); return store.getState().node; }, diff --git a/packages/davinci-client/src/lib/davinci.api.ts b/packages/davinci-client/src/lib/davinci.api.ts index 472cc467..47a19d50 100644 --- a/packages/davinci-client/src/lib/davinci.api.ts +++ b/packages/davinci-client/src/lib/davinci.api.ts @@ -14,7 +14,12 @@ import { handleResponse, transformActionRequest, transformSubmitRequest } from ' * Import the DaVinci types */ import type { RootStateWithNode } from './client.store.utils.js'; -import type { DaVinciCacheEntry, ThrownQueryError } from './davinci.types.js'; +import type { + DaVinciCacheEntry, + OutgoingQueryParams, + StartOptions, + ThrownQueryError, +} from './davinci.types'; import type { ContinueNode } from './node.types.js'; import type { StartNode } from '../types.js'; @@ -186,11 +191,11 @@ export const davinciApi = createApi({ * @method start - method for initiating a DaVinci flow * @param - needs no arguments, but need to declare types to make it explicit */ - start: builder.mutation({ + start: builder.mutation | undefined>({ /** * @method queryFn - This is just a wrapper around the fetch call */ - async queryFn(_, api, __, baseQuery) { + async queryFn(options, api, __, baseQuery) { const state = api.getState() as RootStateWithNode; if (!state) { @@ -216,9 +221,23 @@ export const davinciApi = createApi({ responseType: state?.config?.responseType, scope: state?.config?.scope, }); + const url = new URL(authorizeUrl); + const existingParams = url.searchParams; + + if (options?.query) { + Object.entries(options.query).forEach(([key, value]) => { + /** + * We use set here because if we have existing params, we want + * to make sure we override them and not add duplicates + */ + existingParams.set(key, String(value)); + }); + + url.search = existingParams.toString(); + } const response = await baseQuery({ - url: authorizeUrl, + url: url.toString(), credentials: 'include', method: 'GET', headers: { @@ -268,7 +287,6 @@ export const davinciApi = createApi({ } const cacheEntry: DaVinciCacheEntry = api.getCacheEntry(); - handleResponse(cacheEntry, api.dispatch, response?.status || 0); }, }), diff --git a/packages/davinci-client/src/lib/davinci.types.ts b/packages/davinci-client/src/lib/davinci.types.ts index 1cd1815c..6c2ea663 100644 --- a/packages/davinci-client/src/lib/davinci.types.ts +++ b/packages/davinci-client/src/lib/davinci.types.ts @@ -255,3 +255,11 @@ export interface ThrownQueryError { isHandledError: boolean; meta: FetchBaseQueryMeta; } + +export interface StartOptions { + query: Query; +} +// Outgoing query parameters (sent in the request) +export interface OutgoingQueryParams { + [key: string]: string | string[]; +} diff --git a/packages/davinci-client/src/lib/davinci.utils.ts b/packages/davinci-client/src/lib/davinci.utils.ts index 208a4596..7fe8104a 100644 --- a/packages/davinci-client/src/lib/davinci.utils.ts +++ b/packages/davinci-client/src/lib/davinci.utils.ts @@ -12,6 +12,7 @@ import type { DaVinciNextResponse, DaVinciRequest, DaVinciSuccessResponse, + StartOptions, } from './davinci.types'; import type { ContinueNode } from './node.types'; diff --git a/packages/davinci-client/src/lib/node.slice.ts b/packages/davinci-client/src/lib/node.slice.ts index fd71d81a..5133bf05 100644 --- a/packages/davinci-client/src/lib/node.slice.ts +++ b/packages/davinci-client/src/lib/node.slice.ts @@ -165,7 +165,11 @@ export const nodeSlice = createSlice({ */ next( state, - action: PayloadAction<{ data: DaVinciNextResponse; requestId: string; httpStatus: number }>, + action: PayloadAction<{ + data: DaVinciNextResponse; + requestId: string; + httpStatus: number; + }>, ) { const newState = state as Draft;