From 547ef327a666964b252cd14091a3d79347dff4c0 Mon Sep 17 00:00:00 2001 From: Peter Phanouvong Date: Thu, 21 Sep 2023 17:32:17 +1000 Subject: [PATCH] apli client works for pages router --- package-lock.json | 27 ++++++++ package.json | 1 + server/index.d.ts | 40 ++++++++++++ src/api-client.js | 90 +++++++++++++++++++++++++ src/server/index.js | 1 + src/session/sessionManager.js | 98 ++++++++++++++++++++++++++++ src/utils/isAppRouter.js | 3 + src/utils/pageRouter/isTokenValid.js | 4 +- tsconfig.json | 21 ++++++ 9 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 src/api-client.js create mode 100644 src/session/sessionManager.js create mode 100644 src/utils/isAppRouter.js create mode 100644 tsconfig.json diff --git a/package-lock.json b/package-lock.json index 1913552e..00a26a89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "@kinde-oss/kinde-auth-nextjs", "version": "1.8.18", "dependencies": { + "@kinde-oss/kinde-typescript-sdk": "^2.2.0", "cookie": "^0.5.0", "crypto": "^1.0.1", "crypto-js": "^4.1.1", @@ -678,6 +679,14 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, + "node_modules/@kinde-oss/kinde-typescript-sdk": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@kinde-oss/kinde-typescript-sdk/-/kinde-typescript-sdk-2.2.0.tgz", + "integrity": "sha512-LhrEUlU+p7yRo0yWOg83HvQhA22JuMX9xXsQawbzayNHBvEPHUtHMfvfz977FRBvix1S10I3Hv4IDQCElV2aPg==", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6447,6 +6456,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==" + }, "node_modules/unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", @@ -7470,6 +7484,14 @@ } } }, + "@kinde-oss/kinde-typescript-sdk": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@kinde-oss/kinde-typescript-sdk/-/kinde-typescript-sdk-2.2.0.tgz", + "integrity": "sha512-LhrEUlU+p7yRo0yWOg83HvQhA22JuMX9xXsQawbzayNHBvEPHUtHMfvfz977FRBvix1S10I3Hv4IDQCElV2aPg==", + "requires": { + "uncrypto": "^0.1.3" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -11558,6 +11580,11 @@ "which-boxed-primitive": "^1.0.2" } }, + "uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==" + }, "unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", diff --git a/package.json b/package.json index f9681156..df0bd7df 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "react-dom": "^18.1.0" }, "dependencies": { + "@kinde-oss/kinde-typescript-sdk": "^2.2.0", "cookie": "^0.5.0", "crypto": "^1.0.1", "crypto-js": "^4.1.1", diff --git a/server/index.d.ts b/server/index.d.ts index 30bcfc05..77f35452 100644 --- a/server/index.d.ts +++ b/server/index.d.ts @@ -1,5 +1,23 @@ import {ReactElement, LinkHTMLAttributes} from 'react'; import {NextRequest} from 'next/server'; +import {NextApiResponse} from 'next'; +import { + APIsApi, + ApplicationsApi, + BusinessApi, + CallbacksApi, + ConnectedAppsApi, + EnvironmentsApi, + FeatureFlagsApi, + IndustriesApi, + OAuthApi, + OrganizationsApi, + PermissionsApi, + RolesApi, + SubscribersApi, + TimezonesApi, + UsersApi +} from '@kinde-oss/kinde-typescript-sdk'; export declare function RegisterLink(props): ReactElement; @@ -79,3 +97,25 @@ export declare function handleAuth( export declare function getKindeServerSession(): ServerSession; export declare function authMiddleware(); + +export function createKindeManagementAPIClient( + req?: Request | NextApiResponse, + res?: Response | NextApiResponse +): Promise<{ + getToken: (req?: Request, res?: Response) => string; + usersApi: UsersApi; + oauthApi: OAuthApi; + subscribersApi: SubscribersApi; + organizationsApi: OrganizationsApi; + connectedAppsApi: ConnectedAppsApi; + featureFlagsApi: FeatureFlagsApi; + environmentsApi: EnvironmentsApi; + permissionsApi: PermissionsApi; + rolesApi: RolesApi; + businessApi: BusinessApi; + industriesApi: IndustriesApi; + timezonesApi: TimezonesApi; + applicationsApi: ApplicationsApi; + callbacksApi: CallbacksApi; + apisApi: APIsApi; +}>; diff --git a/src/api-client.js b/src/api-client.js new file mode 100644 index 00000000..24172baa --- /dev/null +++ b/src/api-client.js @@ -0,0 +1,90 @@ +import { + APIsApi, + ApplicationsApi, + BusinessApi, + CallbacksApi, + Configuration, + ConnectedAppsApi, + EnvironmentsApi, + FeatureFlagsApi, + IndustriesApi, + OAuthApi, + OrganizationsApi, + PermissionsApi, + RolesApi, + SubscribersApi, + TimezonesApi, + UsersApi +} from '@kinde-oss/kinde-typescript-sdk'; +import {config} from './config/index'; +import {sessionManager} from './session/sessionManager'; +import {isTokenValid} from './utils/pageRouter/isTokenValid'; + +/** + * Create the Kinde Management API client + * @param {Request | NextApiRequest} [req] - optional request (required when used with pages router) + * @param {Response} [res] - optional response (required when used with pages router) + */ +export const createKindeManagementAPIClient = async (req, res) => { + let apiToken = null; + + const store = sessionManager(req, res); + const tokenFromCookie = store.getSessionItem('kinde_api_access_token'); + if (isTokenValid(tokenFromCookie)) { + apiToken = tokenFromCookie; + } else { + const response = await fetch(`${config.issuerURL}/oauth2/token`, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: config.clientID, + client_secret: config.clientSecret, + audience: config.audience + }) + }); + let apiToken = (await response.json()).access_token; + store.setSessionItem('kinde_api_access_token', apiToken); + } + + const cfg = new Configuration({ + basePath: config.issuerURL, + accessToken: apiToken, + headers: {Accept: 'application/json'} + }); + const usersApi = new UsersApi(cfg); + const oauthApi = new OAuthApi(cfg); + const subscribersApi = new SubscribersApi(cfg); + const organizationsApi = new OrganizationsApi(cfg); + const connectedAppsApi = new ConnectedAppsApi(cfg); + const featureFlagsApi = new FeatureFlagsApi(cfg); + const environmentsApi = new EnvironmentsApi(cfg); + const permissionsApi = new PermissionsApi(cfg); + const rolesApi = new RolesApi(cfg); + const businessApi = new BusinessApi(cfg); + const industriesApi = new IndustriesApi(cfg); + const timezonesApi = new TimezonesApi(cfg); + const applicationsApi = new ApplicationsApi(cfg); + const callbacksApi = new CallbacksApi(cfg); + const apisApi = new APIsApi(cfg); + + return { + usersApi, + oauthApi, + subscribersApi, + organizationsApi, + connectedAppsApi, + featureFlagsApi, + environmentsApi, + permissionsApi, + rolesApi, + businessApi, + industriesApi, + timezonesApi, + applicationsApi, + callbacksApi, + apisApi + }; +}; diff --git a/src/server/index.js b/src/server/index.js index 79ed10a7..29257bfd 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -11,3 +11,4 @@ export {RegisterLink} from '../components/RegisterLink'; export {LoginLink} from '../components/LoginLink'; export {LogoutLink} from '../components/LogoutLink'; export {CreateOrgLink} from '../components/CreateOrgLink'; +export {createKindeManagementAPIClient} from '../api-client'; diff --git a/src/session/sessionManager.js b/src/session/sessionManager.js new file mode 100644 index 00000000..1cdd9df0 --- /dev/null +++ b/src/session/sessionManager.js @@ -0,0 +1,98 @@ +import {isAppRouter} from '../utils/isAppRouter'; +import {cookies} from 'next/headers'; + +var cookie = require('cookie'); + +export const sessionManager = (req, res) => { + if (!req) return appRouterSessionManager(cookies()); + return isAppRouter(req) + ? appRouterSessionManager(cookies()) + : pageRouterSessionManager(req, res); +}; + +export const appRouterSessionManager = (cookieStore) => ({ + getSessionItem: (itemKey) => { + const item = cookieStore.get(itemKey); + if (item) { + try { + const jsonValue = JSON.parse(item.value); + if (typeof jsonValue === 'object') { + return jsonValue; + } + return item.value; + } catch (error) { + return item.value; + } + } + return undefined; + }, + setSessionItem: (itemKey, itemValue) => + cookieStore.set( + itemKey, + typeof itemValue === 'object' ? JSON.stringify(itemValue) : itemValue + ), + removeSessionItem: (itemKey) => cookieStore.delete(itemKey), + destroySession: () => { + [ + 'id_token_payload', + 'id_token', + 'access_token_payload', + 'access_token', + 'user', + 'refresh_token' + ].forEach((name) => cookieStore.delete(name)); + } +}); + +export const pageRouterSessionManager = (req, res) => ({ + getSessionItem: (itemKey) => { + const itemValue = req.cookies[itemKey]; + if (itemValue) { + try { + const jsonValue = JSON.parse(itemValue); + if (typeof jsonValue === 'object') { + return jsonValue; + } + return itemValue; + } catch (error) { + return itemValue; + } + } + return undefined; + }, + setSessionItem: (itemKey, itemValue) => { + let cookies = res.getHeader('Set-Cookie') || []; + + if (!Array.isArray(cookies)) { + cookies = [cookies]; + } + + res.setHeader('Set-Cookie', [ + ...cookies.filter((cookie) => !cookie.startsWith(`${itemKey}=`)), + cookie.serialize( + itemKey, + typeof itemValue === 'object' ? JSON.stringify(itemValue) : itemValue, + {path: '/'} + ) + ]); + }, + removeSessionItem: (itemKey) => { + res.setHeader( + 'Set-Cookie', + cookie.serialize(itemKey, '', {path: '/', maxAge: -1}) + ); + }, + destroySession: () => { + res.setHeader( + 'Set-Cookie', + [ + 'id_token_payload', + 'id_token', + 'access_token_payload', + 'access_token', + 'user', + 'refresh_token' + ].map((name) => cookie.serialize(name, '', {path: '/', maxAge: -1})) + ); + } +}); diff --git a/src/utils/isAppRouter.js b/src/utils/isAppRouter.js new file mode 100644 index 00000000..996ed489 --- /dev/null +++ b/src/utils/isAppRouter.js @@ -0,0 +1,3 @@ +export const isAppRouter = (req) => { + return req instanceof Request; +}; diff --git a/src/utils/pageRouter/isTokenValid.js b/src/utils/pageRouter/isTokenValid.js index 857d27ec..b0ce60e4 100644 --- a/src/utils/pageRouter/isTokenValid.js +++ b/src/utils/pageRouter/isTokenValid.js @@ -9,7 +9,9 @@ const isTokenValid = (token) => { const accessTokenPayload = jwt_decode(accessToken); let isAudienceValid = true; if (config.audience) - isAudienceValid = payload.aud && payload.aud.includes(config.audience); + isAudienceValid = + accessTokenPayload.aud && + accessTokenPayload.aud.includes(config.audience); if ( accessTokenPayload.iss == config.issuerURL && diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..02648b13 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + // Change this to match your project + "include": ["src/**/*"], + "compilerOptions": { + // Tells TypeScript to read JS files, as + // normally they are ignored as source files + "allowJs": true, + // Generate d.ts files + "declaration": true, + // This compiler run should + // only output d.ts files + "emitDeclarationOnly": true, + // Types should go into this directory. + // Removing this would place the .d.ts files + // next to the .js files + "outDir": "dist", + // go to js file when using IDE functions like + // "Go to Definition" in VSCode + "declarationMap": true + } +}