diff --git a/clientConfig/rollup.config.js b/clientConfig/rollup.config.js index 809145b2..38b5bb68 100644 --- a/clientConfig/rollup.config.js +++ b/clientConfig/rollup.config.js @@ -2,9 +2,10 @@ const packageJson = require('./package.json'); import babel from '@rollup/plugin-babel'; import {terser} from 'rollup-plugin-terser'; +import tsPlugin from '@rollup/plugin-typescript'; export default { - plugins: [babel({babelHelpers: 'bundled'}), terser()], + plugins: [babel({babelHelpers: 'bundled'}), terser(), tsPlugin()], input: './src/index.js', output: [ { diff --git a/componentConfig/rollup.config.js b/componentConfig/rollup.config.js index a308e3f0..f01d7d04 100644 --- a/componentConfig/rollup.config.js +++ b/componentConfig/rollup.config.js @@ -1,8 +1,9 @@ import babel from '@rollup/plugin-babel'; import {terser} from 'rollup-plugin-terser'; +import tsPlugin from '@rollup/plugin-typescript'; export default { - plugins: [babel({babelHelpers: 'bundled'}), terser()], + plugins: [babel({babelHelpers: 'bundled'}), terser(), tsPlugin()], input: 'src/components/index.js', output: [ { diff --git a/middlewareConfig/rollup.config.js b/middlewareConfig/rollup.config.js index b1c83665..5e4fd15d 100644 --- a/middlewareConfig/rollup.config.js +++ b/middlewareConfig/rollup.config.js @@ -1,8 +1,9 @@ import babel from '@rollup/plugin-babel'; import {terser} from 'rollup-plugin-terser'; +import tsPlugin from '@rollup/plugin-typescript'; export default { - plugins: [babel({babelHelpers: 'bundled'}), terser()], + plugins: [babel({babelHelpers: 'bundled'}), terser(), tsPlugin()], input: 'src/middleware/index.js', output: [ { diff --git a/package.json b/package.json index b6a940ff..3c65c94b 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "@babel/preset-react": "^7.17.12", "@babel/preset-typescript": "^7.23.3", "@rollup/plugin-babel": "^5.3.1", - "@rollup/plugin-typescript": "^11.1.5", + "@rollup/plugin-typescript": "^11.1.6", "@testing-library/react": "^14.2.1", "@types/cookie": "^0.5.3", "@types/jest": "^29.5.12", diff --git a/serverConfig/rollup.config.js b/serverConfig/rollup.config.js index f8922858..3ad18f7c 100644 --- a/serverConfig/rollup.config.js +++ b/serverConfig/rollup.config.js @@ -1,8 +1,9 @@ import babel from '@rollup/plugin-babel'; import {terser} from 'rollup-plugin-terser'; +import tsPlugin from '@rollup/plugin-typescript'; export default { - plugins: [babel({babelHelpers: 'bundled'}), terser()], + plugins: [babel({babelHelpers: 'bundled'}), terser(), tsPlugin()], input: 'src/server/index.js', output: [ { diff --git a/src/config/index.js b/src/config/index.js index 89577665..cec9bb2f 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -1,4 +1,3 @@ -import {GrantType} from '@kinde-oss/kinde-typescript-sdk'; import {version} from '../utils/version'; import {removeTrailingSlash} from '../utils/removeTrailingSlash'; @@ -93,5 +92,5 @@ export const config = { frameworkVersion: version, scope: KINDE_SCOPE }, - grantType: GrantType.AUTHORIZATION_CODE + grantType: 'AUTHORIZATION_CODE' }; diff --git a/src/handlers/setup.js b/src/handlers/setup.js index 6a5a2e88..57f63018 100644 --- a/src/handlers/setup.js +++ b/src/handlers/setup.js @@ -1,6 +1,7 @@ import jwtDecode from 'jwt-decode'; import RouterClient from '../routerClients/RouterClient'; import {config} from '../config/index'; +import {generateUserObject} from '../utils/generateUserObject'; /** * @@ -61,6 +62,17 @@ export const setup = async (routerClient) => { 'user_properties' ); + const phone_number = await routerClient.kindeClient.getClaimValue( + routerClient.sessionManager, + 'phone_number', + 'id_token' + ); + const username = await routerClient.kindeClient.getClaimValue( + routerClient.sessionManager, + 'preferred_username', + 'id_token' + ); + const orgNames = await routerClient.kindeClient.getClaimValue( routerClient.sessionManager, 'organizations', @@ -74,24 +86,12 @@ export const setup = async (routerClient) => { idToken, idTokenRaw: idTokenEncoded, idTokenEncoded, - user: { - ...user, - properties: { - city: userProperties?.kp_usr_city?.v, - industry: userProperties?.kp_usr_industry?.v, - job_title: userProperties?.kp_usr_job_title?.v, - middle_name: userProperties?.kp_usr_middle_name?.v, - postcode: userProperties?.kp_usr_postcode?.v, - salutation: userProperties?.kp_usr_salutation?.v, - state_region: userProperties?.kp_usr_state_region?.v, - street_address: userProperties?.kp_usr_street_address?.v, - street_address_2: userProperties?.kp_usr_street_address_2?.v - } - }, + user: generateUserObject(user, userProperties, phone_number, username), permissions: { permissions, orgCode: organization }, + needsRefresh: false, organization: { orgCode: organization, orgName, @@ -115,8 +115,22 @@ export const setup = async (routerClient) => { }); } catch (error) { if (config.isDebugMode) { - console.error(error); + console.debug(error); } + + if (error.code == 'ERR_JWT_EXPIRED') { + return routerClient.json( + { + needsRefresh: true + }, + {status: 200} + ); + } + return routerClient.json( + { + error + }, + {status: 500} + ); } - return routerClient.json({error: 'Log in with Kinde'}, {status: 401}); }; diff --git a/src/server/index.js b/src/server/index.js index 414233e6..6d0ef33c 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -1,4 +1,4 @@ -export {default as getKindeServerSession} from '../session/index'; +export {default as getKindeServerSession} from '../session/index.ts'; export {authMiddleware, withAuth} from '../authMiddleware/authMiddleware'; export { LoginLink, diff --git a/src/session/getUser.js b/src/session/getUser.js deleted file mode 100644 index 9bb1a95a..00000000 --- a/src/session/getUser.js +++ /dev/null @@ -1,55 +0,0 @@ -import {sessionManager} from './sessionManager'; -import {kindeClient} from './kindeServerClient'; -import {config} from '../config/index'; - -/** - * @callback getUser - * @returns {Promise} - */ -/** - * - * @param {import('next').NextApiRequest} [req] - * @param {import('next').NextApiResponse} [res] - * @returns {getUser} - */ -export const getUserFactory = (req, res) => async () => { - try { - // @ts-ignore - const user = await kindeClient.getUser(sessionManager(req, res)); - const userProperties = await kindeClient.getClaimValue( - sessionManager(req, res), - 'user_properties' - ); - const phone_number = await kindeClient.getClaimValue( - sessionManager(req, res), - 'phone_number', - 'id_token' - ); - const username = await kindeClient.getClaimValue( - sessionManager(req, res), - 'preferred_username', - 'id_token' - ); - return { - ...user, - phone_number, - username, - properties: { - city: userProperties?.kp_usr_city?.v, - industry: userProperties?.kp_usr_industry?.v, - job_title: userProperties?.kp_usr_job_title?.v, - middle_name: userProperties?.kp_usr_middle_name?.v, - postcode: userProperties?.kp_usr_postcode?.v, - salutation: userProperties?.kp_usr_salutation?.v, - state_region: userProperties?.kp_usr_state_region?.v, - street_address: userProperties?.kp_usr_street_address?.v, - street_address_2: userProperties?.kp_usr_street_address_2?.v - } - }; - } catch (error) { - if (config.isDebugMode) { - console.debug('getUser', error); - } - return null; - } -}; diff --git a/src/session/getUser.ts b/src/session/getUser.ts new file mode 100644 index 00000000..27d6d7b2 --- /dev/null +++ b/src/session/getUser.ts @@ -0,0 +1,28 @@ +import jwtDecode from 'jwt-decode'; +import {NextApiRequest, NextApiResponse} from 'next'; +import {KindeAccessToken, KindeIdToken, KindeUser} from '../../types'; +import {config} from '../config/index'; +import {generateUserObject} from '../utils/generateUserObject'; +import {sessionManager} from './sessionManager'; + +export const getUserFactory = + (req: NextApiRequest, res: NextApiResponse) => + async >(): Promise> => { + try { + const idToken = jwtDecode( + (await sessionManager(req, res).getSessionItem('id_token')) as string + ) as KindeIdToken; + + const accessToken = jwtDecode( + (await sessionManager(req, res).getSessionItem( + 'access_token' + )) as string + ) as KindeAccessToken; + return generateUserObject(idToken, accessToken) as KindeUser; + } catch (error) { + if (config.isDebugMode) { + console.debug('getUser', error); + } + return null; + } + }; diff --git a/src/session/getUserOrganizations.js b/src/session/getUserOrganizations.js deleted file mode 100644 index 64b41aac..00000000 --- a/src/session/getUserOrganizations.js +++ /dev/null @@ -1,36 +0,0 @@ -import {sessionManager} from './sessionManager'; -import {kindeClient} from './kindeServerClient'; -/** - * @callback getUserOrganizations - * @returns {Promise} - */ - -/** - * - * @param {import('next').NextApiRequest} [req] - * @param {import('next').NextApiResponse} [res] - * @returns {getUserOrganizations} - */ -export const getUserOrganizationsFactory = (req, res) => async () => { - try { - const userOrgs = await kindeClient.getUserOrganizations( - sessionManager(req, res) - ); - const orgNames = await kindeClient.getClaimValue( - sessionManager(req, res), - 'organizations', - 'id_token' - ); - - return { - orgCodes: userOrgs.orgCodes, - orgs: orgNames?.map((org) => ({ - code: org?.id, - name: org?.name - })) - }; - } catch (error) { - console.error('Failed to fetch user organizations:', error); - return null; - } -}; diff --git a/src/session/getUserOrganizations.ts b/src/session/getUserOrganizations.ts new file mode 100644 index 00000000..e54b89fd --- /dev/null +++ b/src/session/getUserOrganizations.ts @@ -0,0 +1,44 @@ +import {sessionManager} from './sessionManager'; +import {kindeClient} from './kindeServerClient'; +import {config} from '../config/index'; +import {NextApiRequest, NextApiResponse} from 'next'; +import {KindeOrganizations} from '../../types'; + +export const getUserOrganizationsFactory = + (req?: NextApiRequest, res?: NextApiResponse) => + async (): Promise => { + try { + const session = sessionManager(req, res); + const userOrgs = await kindeClient.getUserOrganizations(session); + const orgNames = (await kindeClient.getClaimValue( + session, + 'organizations', + 'id_token' + )) as {id: string; name: string}[]; + + const hasuraOrgCodes = (await kindeClient.getClaimValue( + session, + 'x-hasura-org-codes', + 'id_token' + )) as string[]; + + const hasuraOrganizations = (await kindeClient.getClaimValue( + session, + 'x-hasura-organizations', + 'id_token' + )) as {id: string; name: string}[]; + + return { + orgCodes: [...userOrgs.orgCodes, ...hasuraOrgCodes], + orgs: [...orgNames, ...hasuraOrganizations].map((org) => ({ + code: org?.id, + name: org?.name + })) + }; + } catch (error) { + if (config.isDebugMode) { + console.debug('getUserOrganization error:', error); + } + return null; + } + }; diff --git a/src/session/index.js b/src/session/index.ts similarity index 92% rename from src/session/index.js rename to src/session/index.ts index e9308390..1f61d723 100644 --- a/src/session/index.js +++ b/src/session/index.ts @@ -17,17 +17,13 @@ import {sessionManager} from './sessionManager'; import {getRolesFactory} from './getRoles'; import {getClaimFactory} from './getClaim'; import {config} from '../config/index'; +import {NextApiRequest, NextApiResponse} from 'next'; -/** - * - * @param {import('next').NextApiRequest | Request} [req] - * @param {import('next').NextApiResponse | Response} [res] - * @returns - */ -export default function (req, res) { +export default function (req?: NextApiRequest, res?: NextApiResponse) { return { refreshTokens: async () => { try { + // @ts-ignore const response = await kindeClient.refreshTokens( sessionManager(req, res) ); diff --git a/src/session/kindeServerClient.js b/src/session/kindeServerClient.js deleted file mode 100644 index ae14d5fc..00000000 --- a/src/session/kindeServerClient.js +++ /dev/null @@ -1,9 +0,0 @@ -import {createKindeServerClient} from '@kinde-oss/kinde-typescript-sdk'; -import {config} from '../config/index'; - -/** @type {import('../../types').KindeClient} */ -// @ts-ignore -export const kindeClient = createKindeServerClient( - config.grantType, - config.clientOptions -); diff --git a/src/session/kindeServerClient.ts b/src/session/kindeServerClient.ts new file mode 100644 index 00000000..82bd56b3 --- /dev/null +++ b/src/session/kindeServerClient.ts @@ -0,0 +1,10 @@ +import { + createKindeServerClient, + GrantType +} from '@kinde-oss/kinde-typescript-sdk'; +import {config} from '../config/index'; + +export const kindeClient = createKindeServerClient( + GrantType.AUTHORIZATION_CODE, + config.clientOptions +); diff --git a/src/utils/generateUserObject.ts b/src/utils/generateUserObject.ts new file mode 100644 index 00000000..4056cf93 --- /dev/null +++ b/src/utils/generateUserObject.ts @@ -0,0 +1,61 @@ +import {KindeAccessToken, KindeIdToken, KindeUser} from '../../types'; + +type CustomPropertyType = Record; +export const generateUserObject = ( + idToken: KindeIdToken, + accessToken: KindeAccessToken +) => { + const user: KindeUser = { + id: idToken.sub, + email: idToken.email, + family_name: idToken.family_name, + given_name: idToken.given_name, + picture: idToken.picture, + username: idToken.preferred_username, + phone_number: idToken.phone_number + }; + let res = user; + + const properties = idToken.user_properties || accessToken.user_properties; + + if (properties) { + const { + kp_usr_city: cityObj, + kp_usr_industry: industryObj, + kp_usr_is_marketing_opt_in: isMarketingOptInObj, + kp_usr_job_title: jobTitleObj, + kp_usr_middle_name: middleNameObj, + kp_usr_postcode: postcodeObj, + kp_usr_salutation: salutationObj, + kp_usr_state_region: stateRegionObj, + kp_usr_street_address: streetAddressObj, + kp_usr_street_address_2: streetAddress2Obj, + ...rest + } = properties; + + const sanitizedRest: Record = Object.keys(rest).reduce( + (acc, key) => { + acc[key] = rest[key]?.v; + return acc; + }, + {} + ); + res = { + ...user, + properties: { + city: cityObj?.v, + industry: industryObj?.v, + is_marketing_opt_in: isMarketingOptInObj?.v, + job_title: jobTitleObj?.v, + middle_name: middleNameObj?.v, + postcode: postcodeObj?.v, + salutation: salutationObj?.v, + state_region: stateRegionObj?.v, + street_address: streetAddressObj?.v, + street_address_2: streetAddress2Obj?.v, + ...sanitizedRest + } + }; + } + return res; +}; diff --git a/src/utils/version.js b/src/utils/version.js index fc4d3978..fe43f0ae 100644 --- a/src/utils/version.js +++ b/src/utils/version.js @@ -1,2 +1,2 @@ // Generated by genversion. -export const version = '2.3.6'; +export const version = '2.3.6' diff --git a/tests/frontend/utils.test.ts b/tests/frontend/utils.test.ts new file mode 100644 index 00000000..08c63a23 --- /dev/null +++ b/tests/frontend/utils.test.ts @@ -0,0 +1,386 @@ +import {removeTrailingSlash} from '../../src/utils/removeTrailingSlash'; // Assuming the function is exported from utils file +import {generateUserObject} from '../../src/utils/generateUserObject'; // Assuming the function is exported from utils file +import {KindeAccessToken, KindeIdToken} from '../../types'; + +describe('removeTrailingSlash', () => { + test('should remove trailing slash', () => { + const url = 'http://example.com/'; + expect(removeTrailingSlash(url)).toBe('http://example.com'); + }); + + test('should trim whitespace', () => { + const url = ' http://example.com/ '; + expect(removeTrailingSlash(url)).toBe('http://example.com'); + }); + + test('should handle url without trailing slash', () => { + const url = 'http://example.com'; + expect(removeTrailingSlash(url)).toBe('http://example.com'); + }); + + test('should handle empty string', () => { + const url = ''; + expect(removeTrailingSlash(url)).toBe(''); + }); +}); + +describe('generateUserObject', () => { + const accessToken: KindeAccessToken = { + aud: [], + azp: '463db732edf24274970e395735e6d3e2', + email: 'peter@kinde.com', + exp: 1724626727, + feature_flags: { + boolean: { + t: 'b', + v: true + }, + booleanflag: { + t: 'b', + v: true + }, + flag: { + t: 'j', + v: { + message: 'hi' + } + }, + flagtest: { + t: 'j', + v: { + flag: 'yeeee' + } + }, + integerflag: { + t: 'i', + v: 7 + }, + integerflagtest: { + t: 'i', + v: 123 + }, + stringflag: { + t: 's', + v: 'asdfg' + }, + stringflagtest: { + t: 's', + v: 'stringFlagTest' + }, + test: { + t: 'b', + v: true + } + }, + iat: 1724626665, + iss: 'https://peter.kinde.com', + jti: 'a39d1f76-f87d-447b-8d37-ae5bfcd36bd2', + org_code: 'org_95755120efb', + org_name: 'Peter Phanouvong', + organization_properties: { + kp_org_city: {}, + kp_org_industry: {}, + kp_org_postcode: {}, + kp_org_state_region: {}, + kp_org_street_address: {}, + kp_org_street_address_2: {} + }, + permissions: ['test2', 'tester'], + roles: [ + { + id: '018f17f2-48dc-c606-94a7-0f425996ef57', + key: 'admin', + name: 'Admin' + }, + { + id: '018e9bd7-d933-e4f3-dd7d-686e53be8a2c', + key: 'bud', + name: 'Bud' + } + ], + scp: ['openid', 'profile', 'email', 'offline'], + sub: 'kp:3b1b9e1c1a5a46bfae4e46c969065381', + user_properties: { + kp_usr_industry: { + v: 'Software' + }, + kp_usr_job_title: { + v: 'Engineer' + }, + kp_usr_middle_name: { + v: 'Sabichay' + }, + kp_usr_postcode: {}, + kp_usr_salutation: {}, + kp_usr_state_region: {}, + kp_usr_street_address: {}, + kp_usr_street_address_2: {} + } + }; + const accessTokenWithoutProperties: KindeAccessToken = { + aud: [], + azp: '463db732edf24274970e395735e6d3e2', + email: 'peter@kinde.com', + exp: 1724626727, + feature_flags: { + boolean: { + t: 'b', + v: true + }, + booleanflag: { + t: 'b', + v: true + }, + flag: { + t: 'j', + v: { + message: 'hi' + } + }, + flagtest: { + t: 'j', + v: { + flag: 'yeeee' + } + }, + integerflag: { + t: 'i', + v: 7 + }, + integerflagtest: { + t: 'i', + v: 123 + }, + stringflag: { + t: 's', + v: 'asdfg' + }, + stringflagtest: { + t: 's', + v: 'stringFlagTest' + }, + test: { + t: 'b', + v: true + } + }, + iat: 1724626665, + iss: 'https://peter.kinde.com', + jti: 'a39d1f76-f87d-447b-8d37-ae5bfcd36bd2', + org_code: 'org_95755120efb', + org_name: 'Peter Phanouvong', + organization_properties: { + kp_org_city: {}, + kp_org_industry: {}, + kp_org_postcode: {}, + kp_org_state_region: {}, + kp_org_street_address: {}, + kp_org_street_address_2: {} + }, + permissions: ['test2', 'tester'], + roles: [ + { + id: '018f17f2-48dc-c606-94a7-0f425996ef57', + key: 'admin', + name: 'Admin' + }, + { + id: '018e9bd7-d933-e4f3-dd7d-686e53be8a2c', + key: 'bud', + name: 'Bud' + } + ], + scp: ['openid', 'profile', 'email', 'offline'], + sub: 'kp:3b1b9e1c1a5a46bfae4e46c969065381' + }; + const idToken: KindeIdToken = { + at_hash: '07zCRWxQSbvxUYvB2KL6tA', + aud: ['463db732edf24274970e395735e6d3e2'], + auth_time: 1724626003, + azp: '463db732edf24274970e395735e6d3e2', + email: 'peter@kinde.com', + email_verified: true, + exp: 1724626065, + family_name: 'Phanouvong', + given_name: 'Peterzz', + iat: 1724626004, + iss: 'https://peter.kinde.com', + jti: '4b8f85a5-7996-45cf-811b-29ecdcce9c31', + name: 'Peterzz Phanouvong', + org_codes: ['org_19eb76166dee3', 'org_8615151456b42', 'org_95755120efb'], + organization_properties: { + kp_org_city: {}, + kp_org_industry: {}, + kp_org_postcode: {}, + kp_org_state_region: {}, + kp_org_street_address: {}, + kp_org_street_address_2: {} + }, + organizations: [ + { + id: 'org_19eb76166dee3', + name: 'RBAC' + }, + { + id: 'org_8615151456b42', + name: 'awesome' + }, + { + id: 'org_95755120efb', + name: 'Peter Phanouvong' + } + ], + picture: + 'https://lh3.googleusercontent.com/a/ACg8ocJy7qVlRTf6YhuE5u6Z1FK30BvfXNK5OoMydpzct5oXFrUDRQ=s96-c', + preferred_username: 'peteswah', + rat: 1724626003, + sub: 'kp:3b1b9e1c1a5a46bfae4e46c969065381', + updated_at: 1724286328, + user_properties: { + custom_prop: { + v: 'hello world' + }, + kp_usr_city: { + v: 'Sydney' + }, + kp_usr_industry: { + v: 'Software' + }, + kp_usr_is_marketing_opt_in: {}, + kp_usr_job_title: { + v: 'Engineer' + }, + kp_usr_middle_name: { + v: 'Sabichay' + }, + kp_usr_postcode: {}, + kp_usr_salutation: {}, + kp_usr_state_region: {}, + kp_usr_street_address: {}, + kp_usr_street_address_2: {}, + test: { + v: 'fafdsafdsa' + } + } + }; + const idTokenWithoutProperties: KindeIdToken = { + at_hash: '07zCRWxQSbvxUYvB2KL6tA', + aud: ['463db732edf24274970e395735e6d3e2'], + auth_time: 1724626003, + azp: '463db732edf24274970e395735e6d3e2', + email: 'peter@kinde.com', + email_verified: true, + exp: 1724626065, + family_name: 'Phanouvong', + given_name: 'Peterzz', + iat: 1724626004, + iss: 'https://peter.kinde.com', + jti: '4b8f85a5-7996-45cf-811b-29ecdcce9c31', + name: 'Peterzz Phanouvong', + org_codes: ['org_19eb76166dee3', 'org_8615151456b42', 'org_95755120efb'], + organization_properties: { + kp_org_city: {}, + kp_org_industry: {}, + kp_org_postcode: {}, + kp_org_state_region: {}, + kp_org_street_address: {}, + kp_org_street_address_2: {} + }, + organizations: [ + { + id: 'org_19eb76166dee3', + name: 'RBAC' + }, + { + id: 'org_8615151456b42', + name: 'awesome' + }, + { + id: 'org_95755120efb', + name: 'Peter Phanouvong' + } + ], + picture: + 'https://lh3.googleusercontent.com/a/ACg8ocJy7qVlRTf6YhuE5u6Z1FK30BvfXNK5OoMydpzct5oXFrUDRQ=s96-c', + preferred_username: 'peteswah', + rat: 1724626003, + sub: 'kp:3b1b9e1c1a5a46bfae4e46c969065381', + updated_at: 1724286328 + }; + test('should generate user object', () => { + expect(generateUserObject(idToken, accessToken)).toEqual({ + id: 'kp:3b1b9e1c1a5a46bfae4e46c969065381', + email: 'peter@kinde.com', + family_name: 'Phanouvong', + given_name: 'Peterzz', + picture: + 'https://lh3.googleusercontent.com/a/ACg8ocJy7qVlRTf6YhuE5u6Z1FK30BvfXNK5OoMydpzct5oXFrUDRQ=s96-c', + username: 'peteswah', + properties: { + city: 'Sydney', + industry: 'Software', + job_title: 'Engineer', + middle_name: 'Sabichay', + custom_prop: 'hello world', + test: 'fafdsafdsa' + } + }); + }); + test('should generate user object when there are no properties in idtoken', () => { + expect(generateUserObject(idTokenWithoutProperties, accessToken)).toEqual({ + id: 'kp:3b1b9e1c1a5a46bfae4e46c969065381', + email: 'peter@kinde.com', + family_name: 'Phanouvong', + given_name: 'Peterzz', + picture: + 'https://lh3.googleusercontent.com/a/ACg8ocJy7qVlRTf6YhuE5u6Z1FK30BvfXNK5OoMydpzct5oXFrUDRQ=s96-c', + username: 'peteswah', + phone_number: undefined, + properties: { + city: undefined, + industry: 'Software', + is_marketing_opt_in: undefined, + job_title: 'Engineer', + middle_name: 'Sabichay', + postcode: undefined, + salutation: undefined, + state_region: undefined, + street_address: undefined, + street_address_2: undefined + } + }); + }); + test('should generate user object when there are no properties in access token', () => { + expect(generateUserObject(idToken, accessTokenWithoutProperties)).toEqual({ + id: 'kp:3b1b9e1c1a5a46bfae4e46c969065381', + email: 'peter@kinde.com', + family_name: 'Phanouvong', + given_name: 'Peterzz', + picture: + 'https://lh3.googleusercontent.com/a/ACg8ocJy7qVlRTf6YhuE5u6Z1FK30BvfXNK5OoMydpzct5oXFrUDRQ=s96-c', + username: 'peteswah', + properties: { + city: 'Sydney', + industry: 'Software', + job_title: 'Engineer', + middle_name: 'Sabichay', + custom_prop: 'hello world', + test: 'fafdsafdsa' + } + }); + }); + + test('should generate user object with no properties when not properties in access token or id token', () => { + expect( + generateUserObject(idTokenWithoutProperties, accessTokenWithoutProperties) + ).toEqual({ + id: 'kp:3b1b9e1c1a5a46bfae4e46c969065381', + email: 'peter@kinde.com', + family_name: 'Phanouvong', + given_name: 'Peterzz', + picture: + 'https://lh3.googleusercontent.com/a/ACg8ocJy7qVlRTf6YhuE5u6Z1FK30BvfXNK5OoMydpzct5oXFrUDRQ=s96-c', + username: 'peteswah' + }); + }); +}); diff --git a/tests/frontend/utils.test.tsx b/tests/frontend/utils.test.tsx deleted file mode 100644 index cdfd40bc..00000000 --- a/tests/frontend/utils.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import {removeTrailingSlash} from '../../src/utils/removeTrailingSlash'; // Assuming the function is exported from utils file - -describe('removeTrailingSlash', () => { - test('should remove trailing slash', () => { - const url = 'http://example.com/'; - expect(removeTrailingSlash(url)).toBe('http://example.com'); - }); - - test('should trim whitespace', () => { - const url = ' http://example.com/ '; - expect(removeTrailingSlash(url)).toBe('http://example.com'); - }); - - test('should handle url without trailing slash', () => { - const url = 'http://example.com'; - expect(removeTrailingSlash(url)).toBe('http://example.com'); - }); - - test('should handle empty string', () => { - const url = ''; - expect(removeTrailingSlash(url)).toBe(''); - }); -}); diff --git a/types.d.ts b/types.d.ts index 40c5b5d5..004ac4b2 100644 --- a/types.d.ts +++ b/types.d.ts @@ -11,18 +11,79 @@ import { export type KindeAccessToken = { aud: string[]; - azp: number; + azp: string; + email?: string; exp: number; + feature_flags: Record; iat: number; iss: string; jti: string; org_code: string; org_name?: string; - permissions: KindePermissions; + organization_properties?: KindeTokenOrganizationProperties; + permissions: string[]; roles?: KindeRole[]; - email?: string; scp: string[]; sub: string; + user_properties?: KindeTokenUserProperties; +}; + +type KindeTokenOrganizationProperties = { + kp_org_city: { + v?: string; + }; + kp_org_industry: { + v?: string; + }; + kp_org_postcode: { + v?: string; + }; + kp_org_state_region: { + v?: string; + }; + kp_org_street_address: { + v?: string; + }; + kp_org_street_address_2: { + v?: string; + }; +}; + +type KindeTokenUserProperties = { + kp_usr_city?: { + v?: string; + }; + kp_usr_industry?: { + v?: string; + }; + kp_usr_is_marketing_opt_in?: { + v?: string; + }; + kp_usr_job_title?: { + v?: string; + }; + kp_usr_middle_name?: { + v?: string; + }; + kp_usr_postcode?: { + v?: string; + }; + kp_usr_salutation?: { + v?: string; + }; + kp_usr_state_region?: { + v?: string; + }; + kp_usr_street_address?: { + v?: string; + }; + kp_usr_street_address_2?: { + v?: string; + }; +} & { + [key: string]: { + v?: any; + }; }; export type KindeIdToken = { @@ -31,19 +92,62 @@ export type KindeIdToken = { auth_time: number; azp: string; email: string; + email_verified: boolean; exp: number; + ext_provider?: { + claims: { + connection_id: string; + email: string; + family_name: string; + given_name: string; + is_confirmed: boolean; + picture?: string; + profile?: { + email: string; + family_name: string; + given_name: string; + hd: string; + id: string; + name: string; + picture: string; + verified_email: boolean; + }; + }; + connection_id: string; + name: string; + }; family_name: string; given_name: string; iat: number; iss: string; jti: string; name: string; + organization_properties?: KindeTokenOrganizationProperties; org_codes: string[]; + organizations?: {id: string; name: string}[]; + picture: string; + phone_number?: string; + preferred_username?: string; + user_properties?: KindeTokenUserProperties; + rat: number; sub: string; updated_at: number; }; -export type KindeUser = { +type KindeUserProperties> = { + city?: string; + industry?: string; + is_marketing_opt_in?: string; + job_title?: string; + middle_name?: string; + postcode?: string; + salutation?: string; + state_region?: string; + street_address?: string; + street_address_2?: string; +} & T; + +export type KindeUserBase = { id: string; email: string | null; given_name: string | null; @@ -51,19 +155,12 @@ export type KindeUser = { picture: string | null; username?: string | null; phone_number?: string | null; - properties?: { - usr_city?: string; - usr_industry?: string; - usr_job_title?: string; - usr_middle_name?: string; - usr_postcode?: string; - usr_salutation?: string; - usr_state_region?: string; - usr_street_address?: string; - usr_street_address_2?: string; - }; }; +export interface KindeUser extends KindeUserBase { + properties?: KindeUserProperties; +} + export type KindePermissions = { permissions: string[]; orgCode: string | null; @@ -82,12 +179,12 @@ export type KindeRole = { export type KindeFlagRaw = { t: KindeFlagTypeCode; - v: string | number | boolean; + v?: string | number | boolean | object; }; -export type KindeFlagTypeCode = 'b' | 'i' | 's'; +export type KindeFlagTypeCode = 'b' | 'i' | 's' | 'j'; -export type KindeFlagTypeValue = 'boolean' | 'integer' | 'string'; +export type KindeFlagTypeValue = 'boolean' | 'integer' | 'string' | 'json'; export type KindeFlag = { code: string; @@ -192,7 +289,7 @@ export type KindeClient = { defaultValue?: string | number | boolean | undefined, type?: keyof FlagType | undefined ) => Promise; - refreshData: () => Promise; + refreshTokens: (sessionManager: SessionManager) => Promise; }; export type KindeState = { @@ -246,7 +343,7 @@ export type KindeState = { defaultValue: string ) => string | null | undefined; getToken: () => string | null; - getUser: () => KindeUser | null; + getUser: () => KindeUser> | null; getUserOrganizations: () => KindeOrganizations | null; refreshData: () => Promise; }; @@ -255,7 +352,7 @@ export type KindeSetupResponse = { accessToken: KindeAccessToken; accessTokenEncoded: string; idToken: KindeIdToken; - user: KindeUser; + user: KindeUser>; permissions: KindePermissions; organization: KindeOrganization; featureFlags: Record;