From d4506736c6506c7a2d2ec9ebd6b19138aa51fc07 Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Tue, 15 Oct 2024 14:00:38 +1100 Subject: [PATCH 01/13] chore: move generate user object tests to its own file --- tests/frontend/generate-user-object.test.ts | 363 ++++++++++++++++++++ tests/frontend/utils.test.ts | 363 -------------------- 2 files changed, 363 insertions(+), 363 deletions(-) create mode 100644 tests/frontend/generate-user-object.test.ts diff --git a/tests/frontend/generate-user-object.test.ts b/tests/frontend/generate-user-object.test.ts new file mode 100644 index 00000000..5c4a2217 --- /dev/null +++ b/tests/frontend/generate-user-object.test.ts @@ -0,0 +1,363 @@ +import {generateUserObject} from '../../src/utils/generateUserObject'; // Assuming the function is exported from utils file +import {KindeAccessToken, KindeIdToken} from '../../types'; + +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.ts b/tests/frontend/utils.test.ts index 08c63a23..cdfd40bc 100644 --- a/tests/frontend/utils.test.ts +++ b/tests/frontend/utils.test.ts @@ -1,6 +1,4 @@ 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', () => { @@ -23,364 +21,3 @@ describe('removeTrailingSlash', () => { 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' - }); - }); -}); From e724b4730837b2570233d67e6a988da57e7c3d88 Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Tue, 15 Oct 2024 14:01:05 +1100 Subject: [PATCH 02/13] feat: generateOrg object to grab from access token and id token --- src/session/getOrganization.js | 45 ------------------------- src/session/getOrganization.ts | 34 +++++++++++++++++++ src/utils/generateOrganizationObject.ts | 31 +++++++++++++++++ 3 files changed, 65 insertions(+), 45 deletions(-) delete mode 100644 src/session/getOrganization.js create mode 100644 src/session/getOrganization.ts create mode 100644 src/utils/generateOrganizationObject.ts diff --git a/src/session/getOrganization.js b/src/session/getOrganization.js deleted file mode 100644 index cd50756b..00000000 --- a/src/session/getOrganization.js +++ /dev/null @@ -1,45 +0,0 @@ -import {sessionManager} from './sessionManager'; -import {kindeClient} from './kindeServerClient'; -import {config} from '../config/index'; -/** - * @callback getOrganization - * @returns {Promise} - */ - -/** - * - * @param {import('next').NextApiRequest} [req] - * @param {import('next').NextApiResponse} [res] - * @returns {getOrganization} - */ -export const getOrganizationFactory = (req, res) => async () => { - try { - const org = await kindeClient.getOrganization(sessionManager(req, res)); - const orgName = await kindeClient.getClaimValue( - sessionManager(req, res), - 'org_name' - ); - const orgProperties = await kindeClient.getClaimValue( - sessionManager(req, res), - 'organization_properties' - ); - - return { - orgCode: org.orgCode, - orgName: orgName, - properties: { - city: orgProperties?.kp_org_city?.v, - industry: orgProperties?.kp_org_industry?.v, - postcode: orgProperties?.kp_org_postcode?.v, - state_region: orgProperties?.kp_org_state_region?.v, - street_address: orgProperties?.kp_org_street_address?.v, - street_address_2: orgProperties?.kp_org_street_address_2?.v - } - }; - } catch (error) { - if (config.isDebugMode) { - console.error(error); - } - return null; - } -}; diff --git a/src/session/getOrganization.ts b/src/session/getOrganization.ts new file mode 100644 index 00000000..5d0f3bf1 --- /dev/null +++ b/src/session/getOrganization.ts @@ -0,0 +1,34 @@ +import jwtDecode from 'jwt-decode'; +import {KindeAccessToken, KindeIdToken} from '../../types'; +import {config} from '../config/index'; +import {generateOrganizationObject} from '../utils/generateOrganizationObject'; +import {sessionManager} from './sessionManager'; +/** + * @callback getOrganization + * @returns {Promise} + */ + +/** + * + * @param {import('next').NextApiRequest} [req] + * @param {import('next').NextApiResponse} [res] + * @returns {getOrganization} + */ +export const getOrganizationFactory = (req, res) => async () => { + 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 generateOrganizationObject(idToken, accessToken); + } catch (error) { + if (config.isDebugMode) { + console.error(error); + } + return null; + } +}; diff --git a/src/utils/generateOrganizationObject.ts b/src/utils/generateOrganizationObject.ts new file mode 100644 index 00000000..1e1f8782 --- /dev/null +++ b/src/utils/generateOrganizationObject.ts @@ -0,0 +1,31 @@ +import {KindeAccessToken, KindeIdToken} from '../../types'; + +export const generateOrganizationObject = ( + idToken: KindeIdToken, + accessToken: KindeAccessToken +) => { + return { + orgCode: accessToken.org_code, + orgName: accessToken.org_name, + properties: { + city: + idToken.organization_properties?.kp_org_city?.v || + accessToken.organization_properties?.kp_org_city?.v, + industry: + idToken.organization_properties?.kp_org_industry?.v || + accessToken.organization_properties?.kp_org_industry?.v, + postcode: + idToken.organization_properties?.kp_org_postcode?.v || + accessToken.organization_properties?.kp_org_postcode?.v, + state_region: + idToken.organization_properties?.kp_org_state_region?.v || + accessToken.organization_properties?.kp_org_state_region?.v, + street_address: + idToken.organization_properties?.kp_org_street_address?.v || + accessToken.organization_properties?.kp_org_street_address?.v, + street_address_2: + idToken.organization_properties?.kp_org_street_address_2?.v || + accessToken.organization_properties?.kp_org_street_address_2?.v + } + }; +}; From cfbd6d39e7855c371d52cfddaebcfdfb6a7ffd32 Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Tue, 15 Oct 2024 14:01:22 +1100 Subject: [PATCH 03/13] tests: init for generateOrganizationObject --- .../generate-organization-object.test.ts | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 tests/frontend/generate-organization-object.test.ts diff --git a/tests/frontend/generate-organization-object.test.ts b/tests/frontend/generate-organization-object.test.ts new file mode 100644 index 00000000..d80185c4 --- /dev/null +++ b/tests/frontend/generate-organization-object.test.ts @@ -0,0 +1,198 @@ +import {generateOrganizationObject} from '../../src/utils/generateOrganizationObject'; // Assuming the function is exported from utils file +import {KindeAccessToken, KindeIdToken} from '../../types'; + +describe('generateOrganizationObject', () => { + const orgProperties = { + kp_org_city: { + v: 'Sydney' + }, + kp_org_industry: { + v: 'Software' + }, + kp_org_postcode: { + v: '2165' + }, + kp_org_state_region: { + v: 'NSW' + }, + kp_org_street_address: {}, + kp_org_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', + 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 accessToken: KindeAccessToken = { + ...accessTokenWithoutProperties, + organization_properties: orgProperties + }; + + 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'], + 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 + }; + + const idToken: KindeIdToken = { + ...idTokenWithoutProperties, + organization_properties: orgProperties + }; + + test('should generate org object', () => { + expect(generateOrganizationObject(idToken, accessToken)).toEqual({ + orgCode: 'org_95755120efb', + orgName: 'Peter Phanouvong', + properties: { + city: 'Sydney', + industry: 'Software', + postcode: '2165', + state_region: 'NSW' + } + }); + }); + test('should generate org object when there are no properties in idtoken', () => { + expect( + generateOrganizationObject(idTokenWithoutProperties, accessToken) + ).toEqual({ + orgCode: 'org_95755120efb', + orgName: 'Peter Phanouvong', + properties: { + city: 'Sydney', + industry: 'Software', + postcode: '2165', + state_region: 'NSW' + } + }); + }); + test('should generate org object when there are no properties in access token', () => { + expect( + generateOrganizationObject(idToken, accessTokenWithoutProperties) + ).toEqual({ + orgCode: 'org_95755120efb', + orgName: 'Peter Phanouvong', + properties: { + city: 'Sydney', + industry: 'Software', + postcode: '2165', + state_region: 'NSW' + } + }); + }); + + test('should generate org object with no properties when not properties in access token or id token', () => { + expect( + generateOrganizationObject( + idTokenWithoutProperties, + accessTokenWithoutProperties + ) + ).toEqual({ + orgCode: 'org_95755120efb', + orgName: 'Peter Phanouvong', + properties: { + city: undefined, + industry: undefined, + postcode: undefined, + state_region: undefined, + street_address: undefined, + street_address_2: undefined + } + }); + }); +}); From e57a4627495e66385e8b8912a5d1621511d0b56d Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Fri, 18 Oct 2024 09:11:49 +1100 Subject: [PATCH 04/13] chore: error handling --- src/handlers/setup.ts | 28 +++++++------------------ src/session/getOrganization.ts | 18 ++++++++++++---- src/utils/generateOrganizationObject.ts | 10 +++++++++ 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/handlers/setup.ts b/src/handlers/setup.ts index 21d5770e..8e8e68ff 100644 --- a/src/handlers/setup.ts +++ b/src/handlers/setup.ts @@ -2,6 +2,7 @@ import jwtDecode from 'jwt-decode'; import {KindeAccessToken, KindeIdToken} from '../../types'; import {config} from '../config/index'; import {generateUserObject} from '../utils/generateUserObject'; +import {generateOrganizationObject} from '../utils/generateOrganizationObject'; /** * @@ -14,17 +15,15 @@ export const setup = async (routerClient) => { routerClient.sessionManager ); - const accessTokenEncoded = await routerClient.sessionManager.getSessionItem( - 'access_token' - ); + const accessTokenEncoded = + await routerClient.sessionManager.getSessionItem('access_token'); - const idTokenEncoded = await routerClient.sessionManager.getSessionItem( - 'id_token' - ); + const idTokenEncoded = + await routerClient.sessionManager.getSessionItem('id_token'); - const accessToken = jwtDecode(accessTokenEncoded); + const accessToken: KindeAccessToken = jwtDecode(accessTokenEncoded); - const idToken = jwtDecode(idTokenEncoded); + const idToken: KindeIdToken = jwtDecode(idTokenEncoded); const permissions = await routerClient.kindeClient.getClaimValue( routerClient.sessionManager, @@ -79,18 +78,7 @@ export const setup = async (routerClient) => { orgCode: organization }, needsRefresh: false, - organization: { - orgCode: organization, - orgName, - properties: { - city: orgProperties?.kp_org_city?.v, - industry: orgProperties?.kp_org_industry?.v, - postcode: orgProperties?.kp_org_postcode?.v, - state_region: orgProperties?.kp_org_state_region?.v, - street_address: orgProperties?.kp_org_street_address?.v, - street_address_2: orgProperties?.kp_org_street_address_2?.v - } - }, + organization: generateOrganizationObject(idToken, accessToken), featureFlags, userOrganizations: { orgCodes: userOrganizations, diff --git a/src/session/getOrganization.ts b/src/session/getOrganization.ts index 5d0f3bf1..b24e0397 100644 --- a/src/session/getOrganization.ts +++ b/src/session/getOrganization.ts @@ -16,12 +16,22 @@ import {sessionManager} from './sessionManager'; */ export const getOrganizationFactory = (req, res) => async () => { try { - const idToken = jwtDecode( - (await sessionManager(req, res).getSessionItem('id_token')) as string - ) as KindeIdToken; + const idTokenString = await sessionManager(req, res).getSessionItem( + 'id_token' + ); + if (!idTokenString) { + throw new Error('ID token is missing'); + } + const idToken = jwtDecode(idTokenString as string) as KindeIdToken; + const accessTokenString = await sessionManager(req, res).getSessionItem( + 'access_token' + ); + if (!accessTokenString) { + throw new Error('Access token is missing'); + } const accessToken = jwtDecode( - (await sessionManager(req, res).getSessionItem('access_token')) as string + accessTokenString as string ) as KindeAccessToken; return generateOrganizationObject(idToken, accessToken); diff --git a/src/utils/generateOrganizationObject.ts b/src/utils/generateOrganizationObject.ts index 1e1f8782..245f92d1 100644 --- a/src/utils/generateOrganizationObject.ts +++ b/src/utils/generateOrganizationObject.ts @@ -4,6 +4,16 @@ export const generateOrganizationObject = ( idToken: KindeIdToken, accessToken: KindeAccessToken ) => { + if (!idToken || !accessToken) { + throw new Error('Both idToken and accessToken must be provided'); + } + + if ( + typeof accessToken.org_code !== 'string' || + typeof accessToken.org_name !== 'string' + ) { + throw new Error('Invalid accessToken structure'); + } return { orgCode: accessToken.org_code, orgName: accessToken.org_name, From caaea253ed910291c75ab542f498e27a5dbbbed6 Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Mon, 28 Oct 2024 11:36:35 +1100 Subject: [PATCH 05/13] types: fix permission types and userOrg default value --- src/config/{index.js => index.ts} | 16 ++++++++++------ types.d.ts | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) rename src/config/{index.js => index.ts} (90%) diff --git a/src/config/index.js b/src/config/index.ts similarity index 90% rename from src/config/index.js rename to src/config/index.ts index cec9bb2f..8bd9d4cf 100644 --- a/src/config/index.js +++ b/src/config/index.ts @@ -1,10 +1,8 @@ import {version} from '../utils/version'; import {removeTrailingSlash} from '../utils/removeTrailingSlash'; +import {KindeState} from '../../types'; -/** - * @type {import('../../types').KindeState} - */ -const initialState = { +const initialState: KindeState = { accessToken: null, idToken: null, isAuthenticated: false, @@ -12,7 +10,7 @@ const initialState = { organization: null, permissions: [], user: null, - userOrganizations: [], + userOrganizations: null, getAccessToken: () => null, getBooleanFlag: () => null, getClaim: () => null, @@ -26,7 +24,13 @@ const initialState = { getToken: () => null, getUser: () => null, getUserOrganizations: () => null, - refreshData: () => null + refreshData: () => null, + accessTokenEncoded: null, + accessTokenRaw: null, + idTokenRaw: null, + idTokenEncoded: null, + getAccessTokenRaw: () => null, + getIdTokenRaw: () => null }; const SESSION_PREFIX = 'pkce-verifier'; diff --git a/types.d.ts b/types.d.ts index 004ac4b2..ca82e245 100644 --- a/types.d.ts +++ b/types.d.ts @@ -303,7 +303,7 @@ export type KindeState = { isAuthenticated: boolean | null; isLoading: boolean | null; organization: KindeOrganization; - permissions: KindePermissions; + permissions: KindePermissions | []; user: { id: string; email: string | null; @@ -337,7 +337,7 @@ export type KindeState = { getPermission: ( key: string ) => {isGranted: boolean; orgCode: string | null} | null; - getPermissions: () => KindePermissions; + getPermissions: () => KindePermissions | []; getStringFlag: ( code: string, defaultValue: string From a29e63c160242f977b22774866933ec215555bcb Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Wed, 13 Nov 2024 10:47:38 +1100 Subject: [PATCH 06/13] chore:specific union type for permissions --- types.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types.d.ts b/types.d.ts index ca82e245..60bb6a3b 100644 --- a/types.d.ts +++ b/types.d.ts @@ -303,7 +303,7 @@ export type KindeState = { isAuthenticated: boolean | null; isLoading: boolean | null; organization: KindeOrganization; - permissions: KindePermissions | []; + permissions: KindePermissions | {permissions: []; orgCode: null}; user: { id: string; email: string | null; @@ -337,7 +337,7 @@ export type KindeState = { getPermission: ( key: string ) => {isGranted: boolean; orgCode: string | null} | null; - getPermissions: () => KindePermissions | []; + getPermissions: () => KindePermissions | {permissions: []; orgCode: null}; getStringFlag: ( code: string, defaultValue: string From 25a25efbfeca4d873aa2d3ee7e13e6cffcf7622f Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Thu, 14 Nov 2024 10:32:48 +1100 Subject: [PATCH 07/13] types: fix permission types --- src/config/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/index.ts b/src/config/index.ts index 8bd9d4cf..256bc5ac 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -8,7 +8,7 @@ const initialState: KindeState = { isAuthenticated: false, isLoading: true, organization: null, - permissions: [], + permissions: null, user: null, userOrganizations: null, getAccessToken: () => null, @@ -19,7 +19,7 @@ const initialState: KindeState = { getIntegerFlag: () => null, getOrganization: () => null, getPermission: () => null, - getPermissions: () => [], + getPermissions: () => null, getStringFlag: () => null, getToken: () => null, getUser: () => null, From 29b47cc05c983ea51cf4a3577ca458997c73562d Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Thu, 14 Nov 2024 10:32:57 +1100 Subject: [PATCH 08/13] fix: idtoken from session --- src/session/getIdToken.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/session/getIdToken.js b/src/session/getIdToken.js index 539405fc..72c15cd9 100644 --- a/src/session/getIdToken.js +++ b/src/session/getIdToken.js @@ -18,7 +18,7 @@ import {jwtDecoder} from '@kinde/jwt-decoder'; export const getIdTokenFactory = (req, res) => async () => { try { return jwtDecoder( - await sessionManager(req, res).getSessionItem('id_token') + await (await sessionManager(req, res)).getSessionItem('id_token') ); } catch (err) { if (config.isDebugMode) { From f595719d2983c4433facd84de9b5687dff585d5e Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Thu, 14 Nov 2024 10:33:13 +1100 Subject: [PATCH 09/13] chore: cleanup --- src/session/getOrganization.ts | 66 ++++++++++++++++------------------ 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/src/session/getOrganization.ts b/src/session/getOrganization.ts index c6a34dd6..7d2ac950 100644 --- a/src/session/getOrganization.ts +++ b/src/session/getOrganization.ts @@ -1,44 +1,38 @@ import {jwtDecoder} from '@kinde/jwt-decoder'; -import {KindeAccessToken, KindeIdToken} from '../../types'; +import {KindeAccessToken, KindeIdToken, KindeOrganization} from '../../types'; import {config} from '../config/index'; import {generateOrganizationObject} from '../utils/generateOrganizationObject'; import {sessionManager} from './sessionManager'; -/** - * @callback getOrganization - * @returns {Promise} - */ +import {NextApiRequest, NextApiResponse} from 'next'; -/** - * - * @param {import('next').NextApiRequest} [req] - * @param {import('next').NextApiResponse} [res] - * @returns {getOrganization} - */ -export const getOrganizationFactory = (req, res) => async () => { - try { - const idTokenString = await sessionManager(req, res).getSessionItem( - 'id_token' - ); - if (!idTokenString) { - throw new Error('ID token is missing'); - } - const idToken = jwtDecoder(idTokenString as string); +export const getOrganizationFactory = + ( + req?: NextApiRequest, + res?: NextApiResponse + ): (() => Promise) => + async () => { + try { + const session = await sessionManager(req, res); - const accessTokenString = await sessionManager(req, res).getSessionItem( - 'access_token' - ); - if (!accessTokenString) { - throw new Error('Access token is missing'); - } - const accessToken = jwtDecoder( - accessTokenString as string - ); + const idTokenString = await session.getSessionItem('id_token'); + if (!idTokenString) { + throw new Error('ID token is missing'); + } + const idToken = jwtDecoder(idTokenString as string); + + const accessTokenString = await session.getSessionItem('access_token'); + if (!accessTokenString) { + throw new Error('Access token is missing'); + } + const accessToken = jwtDecoder( + accessTokenString as string + ); - return generateOrganizationObject(idToken, accessToken); - } catch (error) { - if (config.isDebugMode) { - console.error(error); + return generateOrganizationObject(idToken, accessToken); + } catch (error) { + if (config.isDebugMode) { + console.error(error); + } + return null; } - return null; - } -}; + }; From 26b62f369cc4221bb1af25d7ae6145c89cd1204b Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Thu, 14 Nov 2024 10:33:40 +1100 Subject: [PATCH 10/13] fix: hasura mapping support --- src/session/getUserOrganizations.ts | 48 ++++++++------------ src/utils/generateOrganizationObject.ts | 46 +++++++++---------- src/utils/generateUserOrganizationsObject.ts | 29 ++++++++++++ src/utils/getClaim.ts | 38 ++++++++++++++++ 4 files changed, 107 insertions(+), 54 deletions(-) create mode 100644 src/utils/generateUserOrganizationsObject.ts create mode 100644 src/utils/getClaim.ts diff --git a/src/session/getUserOrganizations.ts b/src/session/getUserOrganizations.ts index 2c46bb2c..64cd05fa 100644 --- a/src/session/getUserOrganizations.ts +++ b/src/session/getUserOrganizations.ts @@ -1,42 +1,30 @@ -import {sessionManager} from './sessionManager'; -import {kindeClient} from './kindeServerClient'; -import {config} from '../config/index'; +import {jwtDecoder} from '@kinde/jwt-decoder'; import {NextApiRequest, NextApiResponse} from 'next'; -import {KindeOrganizations} from '../../types'; +import {KindeAccessToken, KindeIdToken, KindeOrganizations} from '../../types'; +import {config} from '../config/index'; +import {generateUserOrganizationsObject} from '../utils/generateUserOrganizationsObject'; +import {sessionManager} from './sessionManager'; export const getUserOrganizationsFactory = (req?: NextApiRequest, res?: NextApiResponse) => async (): Promise => { try { const session = await 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 idTokenString = await session.getSessionItem('id_token'); + if (!idTokenString) { + throw new Error('ID token is missing'); + } + const idToken = jwtDecoder(idTokenString as string); - const hasuraOrganizations = - ((await kindeClient.getClaimValue( - session, - 'x-hasura-organizations', - 'id_token' - )) as {id: string; name: string}[]) ?? []; + const accessTokenString = await session.getSessionItem('access_token'); + if (!accessTokenString) { + throw new Error('Access token is missing'); + } + const accessToken = jwtDecoder( + accessTokenString as string + ); - return { - orgCodes: [...userOrgs.orgCodes, ...hasuraOrgCodes], - orgs: [...orgNames, ...hasuraOrganizations].map((org) => ({ - code: org?.id, - name: org?.name - })) - }; + return generateUserOrganizationsObject(idToken, accessToken); } catch (error) { if (config.isDebugMode) { console.debug('getUserOrganization error:', error); diff --git a/src/utils/generateOrganizationObject.ts b/src/utils/generateOrganizationObject.ts index 245f92d1..ccc9a53d 100644 --- a/src/utils/generateOrganizationObject.ts +++ b/src/utils/generateOrganizationObject.ts @@ -1,41 +1,39 @@ -import {KindeAccessToken, KindeIdToken} from '../../types'; +import {KindeAccessToken, KindeIdToken, KindeOrganization} from '../../types'; +import {getClaim} from './getClaim'; export const generateOrganizationObject = ( idToken: KindeIdToken, accessToken: KindeAccessToken -) => { +): KindeOrganization | null => { if (!idToken || !accessToken) { throw new Error('Both idToken and accessToken must be provided'); } if ( - typeof accessToken.org_code !== 'string' || - typeof accessToken.org_name !== 'string' + (typeof accessToken.org_code !== 'string' || + typeof accessToken.org_name !== 'string') && + (typeof accessToken['x-hasura-org-code'] !== 'string' || + typeof accessToken['x-hasura-org-name'] !== 'string') ) { throw new Error('Invalid accessToken structure'); } + + const orgProperties = getClaim({ + accessToken, + idToken, + claim: 'organization_properties' + }); + return { - orgCode: accessToken.org_code, - orgName: accessToken.org_name, + orgCode: getClaim({accessToken, idToken, claim: 'org_code'}) as string, + orgName: getClaim({accessToken, idToken, claim: 'org_name'}) as string, properties: { - city: - idToken.organization_properties?.kp_org_city?.v || - accessToken.organization_properties?.kp_org_city?.v, - industry: - idToken.organization_properties?.kp_org_industry?.v || - accessToken.organization_properties?.kp_org_industry?.v, - postcode: - idToken.organization_properties?.kp_org_postcode?.v || - accessToken.organization_properties?.kp_org_postcode?.v, - state_region: - idToken.organization_properties?.kp_org_state_region?.v || - accessToken.organization_properties?.kp_org_state_region?.v, - street_address: - idToken.organization_properties?.kp_org_street_address?.v || - accessToken.organization_properties?.kp_org_street_address?.v, - street_address_2: - idToken.organization_properties?.kp_org_street_address_2?.v || - accessToken.organization_properties?.kp_org_street_address_2?.v + city: orgProperties?.kp_org_city?.v, + industry: orgProperties?.kp_org_industry?.v, + postcode: orgProperties?.kp_org_postcode?.v, + state_region: orgProperties?.state_region?.v, + street_address: orgProperties?.street_address?.v, + street_address_2: orgProperties?.street_address_2?.v } }; }; diff --git a/src/utils/generateUserOrganizationsObject.ts b/src/utils/generateUserOrganizationsObject.ts new file mode 100644 index 00000000..88b170f0 --- /dev/null +++ b/src/utils/generateUserOrganizationsObject.ts @@ -0,0 +1,29 @@ +import {KindeAccessToken, KindeIdToken, KindeOrganizations} from '../../types'; +import {getClaim} from './getClaim'; + +export const generateUserOrganizationsObject = ( + idToken: KindeIdToken, + accessToken: KindeAccessToken +): KindeOrganizations | null => { + if (!idToken || !accessToken) { + throw new Error('Both idToken and accessToken must be provided'); + } + const orgCodes = getClaim({ + accessToken, + idToken, + claim: 'org_codes' + }) as string[]; + + const orgs = getClaim({accessToken, idToken, claim: 'organizations'}) as { + id: string; + name: string; + }[]; + + return { + orgCodes, + orgs: orgs.map((org) => ({ + code: org?.id, + name: org?.name + })) + }; +}; diff --git a/src/utils/getClaim.ts b/src/utils/getClaim.ts new file mode 100644 index 00000000..07fe1276 --- /dev/null +++ b/src/utils/getClaim.ts @@ -0,0 +1,38 @@ +import {KindeAccessToken, KindeIdToken} from '../../types'; + +interface GetClaimParams { + accessToken: KindeAccessToken; + idToken: KindeIdToken; + claim: string; +} + +export const getClaim = ({accessToken, claim, idToken}: GetClaimParams) => { + let hasuraClaim = 'x-hasura-' + claim; + if (claim === 'org_name') { + hasuraClaim = 'x-hasura-org-name'; + } + if (claim === 'org_code') { + hasuraClaim = 'x-hasura-org-code'; + } + if (claim === 'org_codes') { + hasuraClaim = 'x-hasura-org-codes'; + } + + if (idToken[claim]) { + return idToken[claim]; + } + + if (idToken[hasuraClaim]) { + return idToken[hasuraClaim]; + } + + if (accessToken[claim]) { + return accessToken[claim]; + } + + if (accessToken[hasuraClaim]) { + return accessToken[hasuraClaim]; + } + + return null; +}; From ed1f8ee608439e2594d8d4647f71600bbcd1754c Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Thu, 14 Nov 2024 10:56:29 +1100 Subject: [PATCH 11/13] chore: user generateUserOrganizations fn --- src/handlers/setup.ts | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/src/handlers/setup.ts b/src/handlers/setup.ts index 1e56d306..abd1b5d1 100644 --- a/src/handlers/setup.ts +++ b/src/handlers/setup.ts @@ -3,6 +3,7 @@ import {KindeAccessToken, KindeIdToken} from '../../types'; import {config} from '../config/index'; import {generateUserObject} from '../utils/generateUserObject'; import {generateOrganizationObject} from '../utils/generateOrganizationObject'; +import {generateUserOrganizationsObject} from '../utils/generateUserOrganizationsObject'; /** * @@ -40,28 +41,6 @@ export const setup = async (routerClient) => { 'feature_flags' ); - const userOrganizations = await routerClient.kindeClient.getClaimValue( - routerClient.sessionManager, - 'org_codes', - 'id_token' - ); - - const orgName = await routerClient.kindeClient.getClaimValue( - routerClient.sessionManager, - 'org_name' - ); - - const orgProperties = await routerClient.kindeClient.getClaimValue( - routerClient.sessionManager, - 'organization_properties' - ); - - const orgNames = await routerClient.kindeClient.getClaimValue( - routerClient.sessionManager, - 'organizations', - 'id_token' - ); - return routerClient.json({ accessToken, accessTokenEncoded, @@ -80,13 +59,7 @@ export const setup = async (routerClient) => { needsRefresh: false, organization: generateOrganizationObject(idToken, accessToken), featureFlags, - userOrganizations: { - orgCodes: userOrganizations, - orgs: orgNames?.map((org) => ({ - code: org?.id, - name: org?.name - })) - } + userOrganizations: generateUserOrganizationsObject(idToken, accessToken) }); } catch (error) { if (config.isDebugMode) { From 45aba48768ecdaf103b6a9af2df2b94f0b3db458 Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Thu, 14 Nov 2024 10:56:46 +1100 Subject: [PATCH 12/13] types: fix kindeOrganizations type --- types.d.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/types.d.ts b/types.d.ts index 60bb6a3b..a6c239a7 100644 --- a/types.d.ts +++ b/types.d.ts @@ -198,13 +198,13 @@ export type KindeOrganization = { orgCode: string | null; orgName?: string | null; properties?: { - org_city?: string; - org_country?: string; - org_industry?: string; - org_postcode?: string; - org_state_region?: string; - org_street_address?: string; - org_street_address_2?: string; + city?: string; + country?: string; + industry?: string; + postcode?: string; + state_region?: string; + street_address?: string; + street_address_2?: string; }; }; From 96fb3102248ff56f440970f3b45954844a160457 Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Thu, 14 Nov 2024 11:16:03 +1100 Subject: [PATCH 13/13] chore: refactor typechecking --- src/utils/generateUserOrganizationsObject.ts | 40 ++++++++++++++++---- src/utils/getClaim.ts | 25 ++++++------ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/utils/generateUserOrganizationsObject.ts b/src/utils/generateUserOrganizationsObject.ts index 88b170f0..a541f257 100644 --- a/src/utils/generateUserOrganizationsObject.ts +++ b/src/utils/generateUserOrganizationsObject.ts @@ -4,20 +4,44 @@ import {getClaim} from './getClaim'; export const generateUserOrganizationsObject = ( idToken: KindeIdToken, accessToken: KindeAccessToken -): KindeOrganizations | null => { - if (!idToken || !accessToken) { - throw new Error('Both idToken and accessToken must be provided'); +): KindeOrganizations => { + if (!idToken) { + throw new Error('idToken must be provided'); } + if (!accessToken) { + throw new Error('accessToken must be provided'); + } + const orgCodes = getClaim({ accessToken, idToken, claim: 'org_codes' - }) as string[]; + }); + + if (!Array.isArray(orgCodes)) { + throw new Error('org_codes claim must be an array of strings'); + } - const orgs = getClaim({accessToken, idToken, claim: 'organizations'}) as { - id: string; - name: string; - }[]; + const orgs = getClaim({accessToken, idToken, claim: 'organizations'}) as + | { + id: string; + name: string; + }[] + | undefined; + + if (!Array.isArray(orgs)) { + throw new Error('organizations claim must be an array of objects'); + } + + if ( + !orgs.every( + (org) => typeof org?.id === 'string' && typeof org?.name === 'string' + ) + ) { + throw new Error( + 'Each organization must have string id and name properties' + ); + } return { orgCodes, diff --git a/src/utils/getClaim.ts b/src/utils/getClaim.ts index 07fe1276..853362fa 100644 --- a/src/utils/getClaim.ts +++ b/src/utils/getClaim.ts @@ -7,30 +7,27 @@ interface GetClaimParams { } export const getClaim = ({accessToken, claim, idToken}: GetClaimParams) => { - let hasuraClaim = 'x-hasura-' + claim; - if (claim === 'org_name') { - hasuraClaim = 'x-hasura-org-name'; - } - if (claim === 'org_code') { - hasuraClaim = 'x-hasura-org-code'; - } - if (claim === 'org_codes') { - hasuraClaim = 'x-hasura-org-codes'; - } + const claimMappings = { + org_name: 'x-hasura-org-name', + org_code: 'x-hasura-org-code', + org_codes: 'x-hasura-org-codes' + }; + + const hasuraClaim = claimMappings[claim] || `x-hasura-${claim}`; - if (idToken[claim]) { + if (claim in idToken && idToken[claim] !== undefined) { return idToken[claim]; } - if (idToken[hasuraClaim]) { + if (hasuraClaim in idToken && idToken[hasuraClaim] !== undefined) { return idToken[hasuraClaim]; } - if (accessToken[claim]) { + if (claim in accessToken && accessToken[claim] !== undefined) { return accessToken[claim]; } - if (accessToken[hasuraClaim]) { + if (hasuraClaim in accessToken && accessToken[hasuraClaim] !== undefined) { return accessToken[hasuraClaim]; }