From 6bb202b848f55c8effd75e988edd5693f3587fb8 Mon Sep 17 00:00:00 2001 From: yyaskriloff <69407772+yyaskriloff@users.noreply.github.com> Date: Sun, 28 Apr 2024 16:49:54 +0300 Subject: [PATCH 01/11] implenment aunthentication function to protect Page components --- src/handlers/protect.js | 49 +++++++++++++++++++++++++++++++++++++++++ src/server/index.js | 3 ++- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/handlers/protect.js diff --git a/src/handlers/protect.js b/src/handlers/protect.js new file mode 100644 index 0000000..555b443 --- /dev/null +++ b/src/handlers/protect.js @@ -0,0 +1,49 @@ +export { default as getKindeServerSession } from '../session/index'; +import { redirect } from 'next/navigation' + +/** + * A higher-order function that wraps a page component and adds protection logic. + * @param {import('react').ReactNode} page - The page component to be protected. + * @param {Object} config - The configuration options for the protection logic. + * @param {string} config.redirect - The redirect path if the user is not authenticated or does not have the required role or permissions. + * @param {string[]} config.role - The required role(s) for accessing the protected page. + * @param {string|string[]} config.permissions - The required permission(s) for accessing the protected page. + * @param {number} config.statusCode - The status code for the redirect response. + * @returns {Function} - The protected page component. + */ + +const protectPage = (page, config = { redirect: '/api/login', statusCode: 302 }) => async (props) => { + const { isAuthenticated, getAccessToken, getPermission } = kinde() + const isSignedIn = await isAuthenticated() + + if (!isSignedIn) { + return redirect(config.redirect, { statusCode: 302 }) + } + + if (config.role) { + const token = await getAccessToken() + const roles = token?.roles + if (!roles || !config.role.some((role) => roles.includes(role))) { + return redirect(config.redirect, { statusCode: 302 }) + } + } + + if (typeof config.permissions === "string") { + const hasPermission = await getPermission(config.permissions) + if (!hasPermission) { + return redirect(config.redirect, { statusCode: 302 }) + } + + } + + if (Array.isArray(config.permissions)) { + const hasPermission = await Promise.all(config.permissions.map((permission) => getPermission(permission))) + if (!hasPermission.some((permission) => permission)) { + return redirect(config.redirect, { statusCode: 302 }) + } + } + + return page(props) +} + +export default protectPage \ No newline at end of file diff --git a/src/server/index.js b/src/server/index.js index 0aa696f..ae3a21a 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -7,4 +7,5 @@ export { RegisterLink } from '../components/index'; export {createKindeManagementAPIClient} from '../api-client'; -export {default as handleAuth} from '../handlers/auth'; +export { default as handleAuth } from '../handlers/auth'; +export { default as protectPage} from '../handlers/protect'; From b74442d0abccc09f10aa66fb49e98051339d4973 Mon Sep 17 00:00:00 2001 From: yyaskriloff <69407772+yyaskriloff@users.noreply.github.com> Date: Sun, 28 Apr 2024 17:14:06 +0300 Subject: [PATCH 02/11] use config.status code and correct formatting changes --- src/handlers/protect.js | 57 ++++++++++++++++++++++------------------- src/server/index.js | 4 +-- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/handlers/protect.js b/src/handlers/protect.js index 555b443..33eb70e 100644 --- a/src/handlers/protect.js +++ b/src/handlers/protect.js @@ -1,5 +1,5 @@ -export { default as getKindeServerSession } from '../session/index'; -import { redirect } from 'next/navigation' +export {default as getKindeServerSession} from '../session/index'; +import {redirect} from 'next/navigation'; /** * A higher-order function that wraps a page component and adds protection logic. @@ -12,38 +12,41 @@ import { redirect } from 'next/navigation' * @returns {Function} - The protected page component. */ -const protectPage = (page, config = { redirect: '/api/login', statusCode: 302 }) => async (props) => { - const { isAuthenticated, getAccessToken, getPermission } = kinde() - const isSignedIn = await isAuthenticated() - +const protectPage = + (page, config = {redirect: '/api/login', statusCode: 302}) => + async (props) => { + const {isAuthenticated, getAccessToken, getPermission} = kinde(); + const isSignedIn = await isAuthenticated(); + if (!isSignedIn) { - return redirect(config.redirect, { statusCode: 302 }) + return redirect(config.redirect, {statusCode}); } if (config.role) { - const token = await getAccessToken() - const roles = token?.roles - if (!roles || !config.role.some((role) => roles.includes(role))) { - return redirect(config.redirect, { statusCode: 302 }) - } + const token = await getAccessToken(); + const roles = token?.roles; + if (!roles || !config.role.some((role) => roles.includes(role))) { + return redirect(config.redirect, {statusCode}); + } } - if (typeof config.permissions === "string") { - const hasPermission = await getPermission(config.permissions) - if (!hasPermission) { - return redirect(config.redirect, { statusCode: 302 }) - } - + if (typeof config.permissions === 'string') { + const hasPermission = await getPermission(config.permissions); + if (!hasPermission) { + return redirect(config.redirect, {statusCode}); + } } if (Array.isArray(config.permissions)) { - const hasPermission = await Promise.all(config.permissions.map((permission) => getPermission(permission))) - if (!hasPermission.some((permission) => permission)) { - return redirect(config.redirect, { statusCode: 302 }) - } + const hasPermission = await Promise.all( + config.permissions.map((permission) => getPermission(permission)) + ); + if (!hasPermission.some((permission) => permission)) { + return redirect(config.redirect, {statusCode}); + } } - - return page(props) -} - -export default protectPage \ No newline at end of file + + return page(props); + }; + +export default protectPage; diff --git a/src/server/index.js b/src/server/index.js index ae3a21a..e817553 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -7,5 +7,5 @@ export { RegisterLink } from '../components/index'; export {createKindeManagementAPIClient} from '../api-client'; -export { default as handleAuth } from '../handlers/auth'; -export { default as protectPage} from '../handlers/protect'; +export {default as handleAuth} from '../handlers/auth'; +export {default as protectPage} from '../handlers/protect'; From 3d2abc2fb6cc2dbfbf80ebd694fe61e9ba892c83 Mon Sep 17 00:00:00 2001 From: yyaskriloff <69407772+yyaskriloff@users.noreply.github.com> Date: Sun, 28 Apr 2024 17:18:48 +0300 Subject: [PATCH 03/11] use get permissions instead of mapping over individual permissions and creating multiple asynchronous calls. --- src/handlers/protect.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/handlers/protect.js b/src/handlers/protect.js index 33eb70e..28d7744 100644 --- a/src/handlers/protect.js +++ b/src/handlers/protect.js @@ -15,7 +15,8 @@ import {redirect} from 'next/navigation'; const protectPage = (page, config = {redirect: '/api/login', statusCode: 302}) => async (props) => { - const {isAuthenticated, getAccessToken, getPermission} = kinde(); + const {isAuthenticated, getAccessToken, getPermission, getPermissions} = + kinde(); const isSignedIn = await isAuthenticated(); if (!isSignedIn) { @@ -38,10 +39,12 @@ const protectPage = } if (Array.isArray(config.permissions)) { - const hasPermission = await Promise.all( - config.permissions.map((permission) => getPermission(permission)) - ); - if (!hasPermission.some((permission) => permission)) { + const permissions = await getPermissions(); + if ( + !config.permissions.some((permission) => + permissions.includes(permission) + ) + ) { return redirect(config.redirect, {statusCode}); } } From 0e852a56b7bc7b2166cfefa6649c2867ee397b41 Mon Sep 17 00:00:00 2001 From: yyaskriloff <69407772+yyaskriloff@users.noreply.github.com> Date: Sun, 28 Apr 2024 17:35:37 +0300 Subject: [PATCH 04/11] chore: add error handiling --- src/handlers/protect.js | 52 +++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/handlers/protect.js b/src/handlers/protect.js index 28d7744..90849b4 100644 --- a/src/handlers/protect.js +++ b/src/handlers/protect.js @@ -17,36 +17,42 @@ const protectPage = async (props) => { const {isAuthenticated, getAccessToken, getPermission, getPermissions} = kinde(); - const isSignedIn = await isAuthenticated(); + try { + const isSignedIn = await isAuthenticated(); - if (!isSignedIn) { - return redirect(config.redirect, {statusCode}); - } - - if (config.role) { - const token = await getAccessToken(); - const roles = token?.roles; - if (!roles || !config.role.some((role) => roles.includes(role))) { + if (!isSignedIn) { return redirect(config.redirect, {statusCode}); } - } - if (typeof config.permissions === 'string') { - const hasPermission = await getPermission(config.permissions); - if (!hasPermission) { - return redirect(config.redirect, {statusCode}); + if (config.role) { + const token = await getAccessToken(); + const roles = token?.roles; + if (!roles || !config.role.some((role) => roles.includes(role))) { + return redirect(config.redirect, {statusCode}); + } } - } - if (Array.isArray(config.permissions)) { - const permissions = await getPermissions(); - if ( - !config.permissions.some((permission) => - permissions.includes(permission) - ) - ) { - return redirect(config.redirect, {statusCode}); + if (typeof config.permissions === 'string') { + const hasPermission = await getPermission(config.permissions); + if (!hasPermission) { + return redirect(config.redirect, {statusCode}); + } + } + + if (Array.isArray(config.permissions)) { + const permissions = await getPermissions(); + if ( + !config.permissions.some((permission) => + permissions.includes(permission) + ) + ) { + return redirect(config.redirect, {statusCode}); + } } + } catch (error) { + // return redirect(config.redirect, {statusCode}); + console.error('Error protecting page', error); + return null; } return page(props); From cec30371b8e79225b58de26412306bfe09167387 Mon Sep 17 00:00:00 2001 From: yyaskriloff <69407772+yyaskriloff@users.noreply.github.com> Date: Sun, 28 Apr 2024 17:51:38 +0300 Subject: [PATCH 05/11] fix: defualt redirect url to proper ligin route --- src/handlers/protect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/protect.js b/src/handlers/protect.js index 90849b4..3c6ea8f 100644 --- a/src/handlers/protect.js +++ b/src/handlers/protect.js @@ -13,7 +13,7 @@ import {redirect} from 'next/navigation'; */ const protectPage = - (page, config = {redirect: '/api/login', statusCode: 302}) => + (page, config = {redirect: '/api/auth/login', statusCode: 302}) => async (props) => { const {isAuthenticated, getAccessToken, getPermission, getPermissions} = kinde(); From 76d4e2cbca6f384a7fda70ca75b0e8d09d250b7b Mon Sep 17 00:00:00 2001 From: yyaskriloff <69407772+yyaskriloff@users.noreply.github.com> Date: Sun, 28 Apr 2024 18:00:28 +0300 Subject: [PATCH 06/11] feature: add same authentication function with api routes --- src/handlers/protect.js | 59 +++++++++++++++++++++++++++++++++++++++-- src/server/index.js | 2 +- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/handlers/protect.js b/src/handlers/protect.js index 3c6ea8f..937eefc 100644 --- a/src/handlers/protect.js +++ b/src/handlers/protect.js @@ -1,5 +1,6 @@ export {default as getKindeServerSession} from '../session/index'; import {redirect} from 'next/navigation'; +import {NextResponse} from 'next/server'; /** * A higher-order function that wraps a page component and adds protection logic. @@ -12,7 +13,7 @@ import {redirect} from 'next/navigation'; * @returns {Function} - The protected page component. */ -const protectPage = +export const protectPage = (page, config = {redirect: '/api/auth/login', statusCode: 302}) => async (props) => { const {isAuthenticated, getAccessToken, getPermission, getPermissions} = @@ -58,4 +59,58 @@ const protectPage = return page(props); }; -export default protectPage; +/** + * Protects a Next.js API route handler with authentication and authorization. + * @param {Function} handler - The Next.js API route handler. + * @param {Object} config - The configuration object. + * @param {string[]} config.role - The required role(s) for accessing the protected page. + * @param {string|string[]} config.permissions - The required permission(s) for accessing the protected page. + * @returns {Function} - The protected API route handler. + */ + +export const protectApi = (handler, config) => async (req) => { + const {isAuthenticated, getAccessToken, getPermission, getPermissions} = + kinde(); + try { + const isSignedIn = await isAuthenticated(); + + if (!isSignedIn) { + return NextResponse.json({statusCode: 401, message: 'Unauthorized'}); + } + + if (config.role) { + const token = await getAccessToken(); + const roles = token?.roles; + if (!roles || !config.role.some((role) => roles.includes(role))) { + return res.redirect({statusCode: 403, message: 'Forbidden'}); + } + } + + if (typeof config.permissions === 'string') { + const hasPermission = await getPermission(config.permissions); + if (!hasPermission) { + return NextResponse.json({statusCode: 403, message: 'Forbidden'}); + } + } + + if (Array.isArray(config.permissions)) { + const permissions = await getPermissions(); + if ( + !config.permissions.some((permission) => + permissions.includes(permission) + ) + ) { + return NextResponse.json({statusCode: 403, message: 'Forbidden'}); + } + } + } catch (error) { + // return NextResponse.json({ + // statusCode: 500, + // message: 'Internal Server Error' + // }); + console.error('Error protecting page', error); + return null; + } + + return handler(req); +}; diff --git a/src/server/index.js b/src/server/index.js index e817553..414233e 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -8,4 +8,4 @@ export { } from '../components/index'; export {createKindeManagementAPIClient} from '../api-client'; export {default as handleAuth} from '../handlers/auth'; -export {default as protectPage} from '../handlers/protect'; +export {protectPage, protectApi} from '../handlers/protect'; From 2275654198d9d48000dca6a75a0ce6163fa7c092 Mon Sep 17 00:00:00 2001 From: yyaskriloff <69407772+yyaskriloff@users.noreply.github.com> Date: Wed, 1 May 2024 13:29:44 +0300 Subject: [PATCH 07/11] fix: remove sttus code option and properly check roles --- src/handlers/protect.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/handlers/protect.js b/src/handlers/protect.js index 937eefc..48814e5 100644 --- a/src/handlers/protect.js +++ b/src/handlers/protect.js @@ -9,12 +9,11 @@ import {NextResponse} from 'next/server'; * @param {string} config.redirect - The redirect path if the user is not authenticated or does not have the required role or permissions. * @param {string[]} config.role - The required role(s) for accessing the protected page. * @param {string|string[]} config.permissions - The required permission(s) for accessing the protected page. - * @param {number} config.statusCode - The status code for the redirect response. * @returns {Function} - The protected page component. */ export const protectPage = - (page, config = {redirect: '/api/auth/login', statusCode: 302}) => + (page, config = {redirect: '/api/auth/login'}) => async (props) => { const {isAuthenticated, getAccessToken, getPermission, getPermissions} = kinde(); @@ -22,21 +21,23 @@ export const protectPage = const isSignedIn = await isAuthenticated(); if (!isSignedIn) { - return redirect(config.redirect, {statusCode}); + return redirect(config.redirect); } if (config.role) { const token = await getAccessToken(); const roles = token?.roles; - if (!roles || !config.role.some((role) => roles.includes(role))) { - return redirect(config.redirect, {statusCode}); + if (!roles) return redirect(config.redirect); + const roleNames = new Set(roles.map((r) => r.name)); + if (!config.role.some((role) => roleNames.has(role))) { + return redirect(config.redirect); } } if (typeof config.permissions === 'string') { const hasPermission = await getPermission(config.permissions); if (!hasPermission) { - return redirect(config.redirect, {statusCode}); + return redirect(config.redirect); } } @@ -47,12 +48,12 @@ export const protectPage = permissions.includes(permission) ) ) { - return redirect(config.redirect, {statusCode}); + return redirect(config.redirect); } } } catch (error) { - // return redirect(config.redirect, {statusCode}); console.error('Error protecting page', error); + // return redirect(config.redirect); return null; } @@ -81,8 +82,11 @@ export const protectApi = (handler, config) => async (req) => { if (config.role) { const token = await getAccessToken(); const roles = token?.roles; - if (!roles || !config.role.some((role) => roles.includes(role))) { - return res.redirect({statusCode: 403, message: 'Forbidden'}); + if (!roles) + return NextResponse.json({statusCode: 401, message: 'Unauthorized'}); + const roleNames = new Set(roles.map((r) => r.name)); + if (!config.role.some((role) => roleNames.has(role))) { + return NextResponse.json({statusCode: 401, message: 'Unauthorized'}); } } @@ -104,11 +108,11 @@ export const protectApi = (handler, config) => async (req) => { } } } catch (error) { + console.error('Error protecting page', error); // return NextResponse.json({ // statusCode: 500, // message: 'Internal Server Error' // }); - console.error('Error protecting page', error); return null; } From 3f3b104d2255d09ddd6a62165911b6d7be8ed718 Mon Sep 17 00:00:00 2001 From: yyaskriloff <69407772+yyaskriloff@users.noreply.github.com> Date: Wed, 1 May 2024 13:31:13 +0300 Subject: [PATCH 08/11] rename config.role to config.roles --- src/handlers/protect.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/handlers/protect.js b/src/handlers/protect.js index 48814e5..aafb71a 100644 --- a/src/handlers/protect.js +++ b/src/handlers/protect.js @@ -7,7 +7,7 @@ import {NextResponse} from 'next/server'; * @param {import('react').ReactNode} page - The page component to be protected. * @param {Object} config - The configuration options for the protection logic. * @param {string} config.redirect - The redirect path if the user is not authenticated or does not have the required role or permissions. - * @param {string[]} config.role - The required role(s) for accessing the protected page. + * @param {string[]} config.roles - The required role(s) for accessing the protected page. * @param {string|string[]} config.permissions - The required permission(s) for accessing the protected page. * @returns {Function} - The protected page component. */ @@ -24,12 +24,12 @@ export const protectPage = return redirect(config.redirect); } - if (config.role) { + if (config.roles) { const token = await getAccessToken(); const roles = token?.roles; if (!roles) return redirect(config.redirect); const roleNames = new Set(roles.map((r) => r.name)); - if (!config.role.some((role) => roleNames.has(role))) { + if (!config.roles.some((role) => roleNames.has(role))) { return redirect(config.redirect); } } @@ -64,7 +64,7 @@ export const protectPage = * Protects a Next.js API route handler with authentication and authorization. * @param {Function} handler - The Next.js API route handler. * @param {Object} config - The configuration object. - * @param {string[]} config.role - The required role(s) for accessing the protected page. + * @param {string[]} config.roles - The required role(s) for accessing the protected page. * @param {string|string[]} config.permissions - The required permission(s) for accessing the protected page. * @returns {Function} - The protected API route handler. */ @@ -79,13 +79,13 @@ export const protectApi = (handler, config) => async (req) => { return NextResponse.json({statusCode: 401, message: 'Unauthorized'}); } - if (config.role) { + if (config.roles) { const token = await getAccessToken(); const roles = token?.roles; if (!roles) return NextResponse.json({statusCode: 401, message: 'Unauthorized'}); const roleNames = new Set(roles.map((r) => r.name)); - if (!config.role.some((role) => roleNames.has(role))) { + if (!config.roles.some((role) => roleNames.has(role))) { return NextResponse.json({statusCode: 401, message: 'Unauthorized'}); } } From 4dbb0c5b751f7163914d81f822a46a4d0c897ec4 Mon Sep 17 00:00:00 2001 From: yyaskriloff <69407772+yyaskriloff@users.noreply.github.com> Date: Fri, 10 May 2024 15:54:10 +0300 Subject: [PATCH 09/11] remove comments --- src/handlers/protect.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/handlers/protect.js b/src/handlers/protect.js index aafb71a..6a7f0b4 100644 --- a/src/handlers/protect.js +++ b/src/handlers/protect.js @@ -53,7 +53,6 @@ export const protectPage = } } catch (error) { console.error('Error protecting page', error); - // return redirect(config.redirect); return null; } @@ -109,10 +108,6 @@ export const protectApi = (handler, config) => async (req) => { } } catch (error) { console.error('Error protecting page', error); - // return NextResponse.json({ - // statusCode: 500, - // message: 'Internal Server Error' - // }); return null; } From 24b97e3ffa95f3400ab9a590fc2767340408db83 Mon Sep 17 00:00:00 2001 From: yyaskriloff <69407772+yyaskriloff@users.noreply.github.com> Date: Fri, 24 May 2024 13:35:59 +0300 Subject: [PATCH 10/11] fix/return compnent not function --- src/handlers/protect.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/protect.js b/src/handlers/protect.js index 6a7f0b4..270a036 100644 --- a/src/handlers/protect.js +++ b/src/handlers/protect.js @@ -13,7 +13,7 @@ import {NextResponse} from 'next/server'; */ export const protectPage = - (page, config = {redirect: '/api/auth/login'}) => + (Page, config = {redirect: '/api/auth/login'}) => async (props) => { const {isAuthenticated, getAccessToken, getPermission, getPermissions} = kinde(); @@ -56,7 +56,7 @@ export const protectPage = return null; } - return page(props); + return ; }; /** From ca953ac2b609b12dc5c050bc96ac411d089534a9 Mon Sep 17 00:00:00 2001 From: yyaskriloff <69407772+yyaskriloff@users.noreply.github.com> Date: Fri, 24 May 2024 13:41:59 +0300 Subject: [PATCH 11/11] chore/use getRoles instead of getAccessToken --- src/handlers/protect.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/handlers/protect.js b/src/handlers/protect.js index 270a036..31a6603 100644 --- a/src/handlers/protect.js +++ b/src/handlers/protect.js @@ -15,8 +15,7 @@ import {NextResponse} from 'next/server'; export const protectPage = (Page, config = {redirect: '/api/auth/login'}) => async (props) => { - const {isAuthenticated, getAccessToken, getPermission, getPermissions} = - kinde(); + const {isAuthenticated, getPermission, getPermissions, getRoles} = kinde(); try { const isSignedIn = await isAuthenticated(); @@ -25,8 +24,7 @@ export const protectPage = } if (config.roles) { - const token = await getAccessToken(); - const roles = token?.roles; + const roles = await getRoles(); if (!roles) return redirect(config.redirect); const roleNames = new Set(roles.map((r) => r.name)); if (!config.roles.some((role) => roleNames.has(role))) { @@ -69,8 +67,7 @@ export const protectPage = */ export const protectApi = (handler, config) => async (req) => { - const {isAuthenticated, getAccessToken, getPermission, getPermissions} = - kinde(); + const {isAuthenticated, getPermission, getPermissions, getRoles} = kinde(); try { const isSignedIn = await isAuthenticated(); @@ -79,8 +76,7 @@ export const protectApi = (handler, config) => async (req) => { } if (config.roles) { - const token = await getAccessToken(); - const roles = token?.roles; + const roles = await getRoles(); if (!roles) return NextResponse.json({statusCode: 401, message: 'Unauthorized'}); const roleNames = new Set(roles.map((r) => r.name));