From dac3a3772fb0ad4fdcbdbd03852cce982c1e999b Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 22 Feb 2023 16:39:21 -0800 Subject: [PATCH 1/5] chore: add codemod for clerk fix in v4.2.0 --- .../__testfixtures__/default.input.js | 122 +++++++++++++++++ .../__testfixtures__/default.output.js | 129 ++++++++++++++++++ .../__tests__/clerkGetCurrentUser.test.ts | 5 + .../clerkGetCurrentUser.ts | 74 ++++++++++ .../clerkGetCurrentUser.yargs.ts | 24 ++++ 5 files changed, 354 insertions(+) create mode 100644 packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__testfixtures__/default.input.js create mode 100644 packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__testfixtures__/default.output.js create mode 100644 packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__tests__/clerkGetCurrentUser.test.ts create mode 100644 packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.ts create mode 100644 packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.yargs.ts diff --git a/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__testfixtures__/default.input.js b/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__testfixtures__/default.input.js new file mode 100644 index 000000000000..488fe1323898 --- /dev/null +++ b/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__testfixtures__/default.input.js @@ -0,0 +1,122 @@ +import { parseJWT } from '@redwoodjs/api' +import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server' + +import { logger } from 'src/lib/logger' + +/** + * getCurrentUser returns the user information together with + * an optional collection of roles used by requireAuth() to check + * if the user is authenticated or has role-based access + * + * @param decoded - The decoded access token containing user info and JWT claims like `sub`. Note could be null. + * @param { token, SupportedAuthTypes type } - The access token itself as well as the auth provider type + * @param { APIGatewayEvent event, Context context } - An object which contains information from the invoker + * such as headers and cookies, and the context information about the invocation such as IP Address + * + * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples + */ +export const getCurrentUser = async ( + decoded, + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + { token, type }, + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + { event, context } +) => { + if (!decoded) { + logger.warn('Missing decoded user') + return null + } + + const { roles } = parseJWT({ decoded }) + + if (roles) { + return { ...decoded, roles } + } + + return { ...decoded } +} + +/** + * The user is authenticated if there is a currentUser in the context + * + * @returns {boolean} - If the currentUser is authenticated + */ +export const isAuthenticated = () => { + return !!context.currentUser +} + +/** + * When checking role membership, roles can be a single value, a list, or none. + * You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client` + */ +type AllowedRoles = string | string[] | undefined + +/** + * When checking role membership, roles can be a single value, a list, or none. + * You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client` + */ + +/** + * Checks if the currentUser is authenticated (and assigned one of the given roles) + * + * @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles + * + * @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles, + * or when no roles are provided to check against. Otherwise returns false. + */ +export const hasRole = (roles: AllowedRoles): boolean => { + if (!isAuthenticated()) { + return false + } + + const currentUserRoles = context.currentUser?.roles + + if (typeof roles === 'string') { + if (typeof currentUserRoles === 'string') { + // roles to check is a string, currentUser.roles is a string + return currentUserRoles === roles + } else if (Array.isArray(currentUserRoles)) { + // roles to check is a string, currentUser.roles is an array + return currentUserRoles?.some((allowedRole) => roles === allowedRole) + } + } + + if (Array.isArray(roles)) { + if (Array.isArray(currentUserRoles)) { + // roles to check is an array, currentUser.roles is an array + return currentUserRoles?.some((allowedRole) => + roles.includes(allowedRole) + ) + } else if (typeof currentUserRoles === 'string') { + // roles to check is an array, currentUser.roles is a string + return roles.some((allowedRole) => currentUserRoles === allowedRole) + } + } + + // roles not found + return false +} + +/** + * Use requireAuth in your services to check that a user is logged in, + * whether or not they are assigned a role, and optionally raise an + * error if they're not. + * + * @param roles: {@link AllowedRoles} - When checking role membership, these roles grant access. + * + * @returns - If the currentUser is authenticated (and assigned one of the given roles) + * + * @throws {@link AuthenticationError} - If the currentUser is not authenticated + * @throws {@link ForbiddenError} If the currentUser is not allowed due to role permissions + * + * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples + */ +export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => { + if (!isAuthenticated()) { + throw new AuthenticationError("You don't have permission to do that.") + } + + if (roles && !hasRole(roles)) { + throw new ForbiddenError("You don't have access to do that.") + } +} diff --git a/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__testfixtures__/default.output.js b/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__testfixtures__/default.output.js new file mode 100644 index 000000000000..4dcab72e8b60 --- /dev/null +++ b/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__testfixtures__/default.output.js @@ -0,0 +1,129 @@ +import { parseJWT } from '@redwoodjs/api' +import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server' + +import { logger } from 'src/lib/logger' + +/** + * getCurrentUser returns the user information together with + * an optional collection of roles used by requireAuth() to check + * if the user is authenticated or has role-based access + * + * @param decoded - The decoded access token containing user info and JWT claims like `sub`. Note could be null. + * @param { token, SupportedAuthTypes type } - The access token itself as well as the auth provider type + * @param { APIGatewayEvent event, Context context } - An object which contains information from the invoker + * such as headers and cookies, and the context information about the invocation such as IP Address + * + * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples + */ +export const getCurrentUser = async ( + decoded, + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + { token, type }, + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + { event, context } +) => { + if (!decoded) { + logger.warn('Missing decoded user') + return null + } + + const { roles } = parseJWT({ decoded }) + + const { privateMetadata, ...userWithoutPrivateMetadata } = decoded + + if (roles) { + return { + roles, + ...userWithoutPrivateMetadata, + } + } + + return { + ...userWithoutPrivateMetadata + } +} + +/** + * The user is authenticated if there is a currentUser in the context + * + * @returns {boolean} - If the currentUser is authenticated + */ +export const isAuthenticated = () => { + return !!context.currentUser +} + +/** + * When checking role membership, roles can be a single value, a list, or none. + * You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client` + */ +type AllowedRoles = string | string[] | undefined + +/** + * When checking role membership, roles can be a single value, a list, or none. + * You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client` + */ + +/** + * Checks if the currentUser is authenticated (and assigned one of the given roles) + * + * @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles + * + * @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles, + * or when no roles are provided to check against. Otherwise returns false. + */ +export const hasRole = (roles: AllowedRoles): boolean => { + if (!isAuthenticated()) { + return false + } + + const currentUserRoles = context.currentUser?.roles + + if (typeof roles === 'string') { + if (typeof currentUserRoles === 'string') { + // roles to check is a string, currentUser.roles is a string + return currentUserRoles === roles + } else if (Array.isArray(currentUserRoles)) { + // roles to check is a string, currentUser.roles is an array + return currentUserRoles?.some((allowedRole) => roles === allowedRole) + } + } + + if (Array.isArray(roles)) { + if (Array.isArray(currentUserRoles)) { + // roles to check is an array, currentUser.roles is an array + return currentUserRoles?.some((allowedRole) => + roles.includes(allowedRole) + ) + } else if (typeof currentUserRoles === 'string') { + // roles to check is an array, currentUser.roles is a string + return roles.some((allowedRole) => currentUserRoles === allowedRole) + } + } + + // roles not found + return false +} + +/** + * Use requireAuth in your services to check that a user is logged in, + * whether or not they are assigned a role, and optionally raise an + * error if they're not. + * + * @param roles: {@link AllowedRoles} - When checking role membership, these roles grant access. + * + * @returns - If the currentUser is authenticated (and assigned one of the given roles) + * + * @throws {@link AuthenticationError} - If the currentUser is not authenticated + * @throws {@link ForbiddenError} If the currentUser is not allowed due to role permissions + * + * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples + */ +export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => { + if (!isAuthenticated()) { + throw new AuthenticationError("You don't have permission to do that.") + } + + if (roles && !hasRole(roles)) { + throw new ForbiddenError("You don't have access to do that.") + } +} diff --git a/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__tests__/clerkGetCurrentUser.test.ts b/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__tests__/clerkGetCurrentUser.test.ts new file mode 100644 index 000000000000..57fc55941fc5 --- /dev/null +++ b/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__tests__/clerkGetCurrentUser.test.ts @@ -0,0 +1,5 @@ +describe('clerk', () => { + it('updates the getCurrentUser function', async () => { + await matchTransformSnapshot('clerk', 'default') + }) +}) diff --git a/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.ts b/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.ts new file mode 100644 index 000000000000..b90d69aac46e --- /dev/null +++ b/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.ts @@ -0,0 +1,74 @@ +import type { FileInfo, API, ObjectExpression } from 'jscodeshift' + +const newReturn = `userWithoutPrivateMetadata` +const destructureStatement = `const { privateMetadata, ...${newReturn} } = decoded` + +export default function transform(file: FileInfo, api: API) { + const j = api.jscodeshift + const ast = j(file.source) + + // Insert `const { privateMetadata, ...userWithoutPrivateMetadata } = decoded` after `const { roles } = parseJWT({ decoded })` + // + // So, before... + // + // ```ts + // const { roles } = parseJWT({ decoded }) + // ``` + // + // and after... + // + // ```ts + // const { roles } = parseJWT({ decoded }) + // + // const { privateMetadata, ...userWithoutPrivateMetadata } = decoded + // ``` + const parseJWTStatement = ast.find(j.VariableDeclaration, { + declarations: [ + { + type: 'VariableDeclarator', + init: { + type: 'CallExpression', + callee: { + name: 'parseJWT', + }, + }, + }, + ], + }) + + parseJWTStatement.insertAfter(destructureStatement) + + // Swap `decoded` with `userWithoutPrivateMetadata` in the two return statements + ast + .find(j.ReturnStatement, { + argument: { + type: 'ObjectExpression', + properties: [ + { + type: 'SpreadElement', + argument: { + name: 'decoded', + }, + }, + ], + }, + }) + .replaceWith((path) => { + const properties = ( + path.value.argument as ObjectExpression + ).properties.filter( + (property) => + property.type !== 'SpreadElement' && property.name !== 'decoded' + ) + + properties.push(j.spreadElement(j.identifier(newReturn))) + + return j.returnStatement(j.objectExpression(properties)) + }) + + return ast.toSource({ + trailingComma: true, + quote: 'single', + lineTerminator: '\n', + }) +} diff --git a/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.yargs.ts b/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.yargs.ts new file mode 100644 index 000000000000..ff44cdb6ddf2 --- /dev/null +++ b/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.yargs.ts @@ -0,0 +1,24 @@ +import path from 'path' + +import task, { TaskInnerAPI } from 'tasuku' + +import getRWPaths from '../../../lib/getRWPaths' +import isTSProject from '../../../lib/isTSProject' +import runTransform from '../../../lib/runTransform' + +export const command = 'update-get-current-user' +export const description = + '(v4.1.x->v4.2.x) For Clerk users; updates the getCurrentUser function' + +export const handler = () => { + task('Update getCurrentUser', async ({ setOutput }: TaskInnerAPI) => { + const authFile = isTSProject ? 'auth.ts' : 'auth.js' + + await runTransform({ + transformPath: path.join(__dirname, 'useArmor.js'), + targetPaths: [path.join(getRWPaths().api.base, 'src', 'lib', authFile)], + }) + + setOutput('All done! Run `yarn rw lint --fix` to prettify your code') + }) +} From 04344abc74a43c1a263c75875ffbabcbd25dce3b Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 22 Feb 2023 17:23:38 -0800 Subject: [PATCH 2/5] fix transform file --- .../v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.yargs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.yargs.ts b/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.yargs.ts index ff44cdb6ddf2..c4779c5bab1e 100644 --- a/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.yargs.ts +++ b/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.yargs.ts @@ -6,7 +6,7 @@ import getRWPaths from '../../../lib/getRWPaths' import isTSProject from '../../../lib/isTSProject' import runTransform from '../../../lib/runTransform' -export const command = 'update-get-current-user' +export const command = 'update-clerk-get-current-user' export const description = '(v4.1.x->v4.2.x) For Clerk users; updates the getCurrentUser function' @@ -15,7 +15,7 @@ export const handler = () => { const authFile = isTSProject ? 'auth.ts' : 'auth.js' await runTransform({ - transformPath: path.join(__dirname, 'useArmor.js'), + transformPath: path.join(__dirname, 'clerkGetCurrentUser.js'), targetPaths: [path.join(getRWPaths().api.base, 'src', 'lib', authFile)], }) From 25dcf2de5f96c445a77a961b5e356622eae2f740 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 22 Feb 2023 17:25:25 -0800 Subject: [PATCH 3/5] rename dir for consistency --- .../__testfixtures__/default.input.js | 0 .../__testfixtures__/default.output.js | 0 .../__tests__/clerkGetCurrentUser.test.ts | 0 .../updateClerkGetCurrentUser.ts} | 0 .../updateClerkGetCurrentUser.yargs.ts} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename packages/codemods/src/codemods/v4.2.x/{clerkGetCurrentUser => updateClerkGetCurrentUser}/__testfixtures__/default.input.js (100%) rename packages/codemods/src/codemods/v4.2.x/{clerkGetCurrentUser => updateClerkGetCurrentUser}/__testfixtures__/default.output.js (100%) rename packages/codemods/src/codemods/v4.2.x/{clerkGetCurrentUser => updateClerkGetCurrentUser}/__tests__/clerkGetCurrentUser.test.ts (100%) rename packages/codemods/src/codemods/v4.2.x/{clerkGetCurrentUser/clerkGetCurrentUser.ts => updateClerkGetCurrentUser/updateClerkGetCurrentUser.ts} (100%) rename packages/codemods/src/codemods/v4.2.x/{clerkGetCurrentUser/clerkGetCurrentUser.yargs.ts => updateClerkGetCurrentUser/updateClerkGetCurrentUser.yargs.ts} (100%) diff --git a/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__testfixtures__/default.input.js b/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__testfixtures__/default.input.js similarity index 100% rename from packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__testfixtures__/default.input.js rename to packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__testfixtures__/default.input.js diff --git a/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__testfixtures__/default.output.js b/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__testfixtures__/default.output.js similarity index 100% rename from packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__testfixtures__/default.output.js rename to packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__testfixtures__/default.output.js diff --git a/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__tests__/clerkGetCurrentUser.test.ts b/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__tests__/clerkGetCurrentUser.test.ts similarity index 100% rename from packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/__tests__/clerkGetCurrentUser.test.ts rename to packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__tests__/clerkGetCurrentUser.test.ts diff --git a/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.ts b/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/updateClerkGetCurrentUser.ts similarity index 100% rename from packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.ts rename to packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/updateClerkGetCurrentUser.ts diff --git a/packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.yargs.ts b/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/updateClerkGetCurrentUser.yargs.ts similarity index 100% rename from packages/codemods/src/codemods/v4.2.x/clerkGetCurrentUser/clerkGetCurrentUser.yargs.ts rename to packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/updateClerkGetCurrentUser.yargs.ts From 097a6087c7d5a9675a9534849756388ec7e7bf27 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 22 Feb 2023 17:34:35 -0800 Subject: [PATCH 4/5] fix test paths --- ...GetCurrentUser.test.ts => updateClerkGetCurrentUser.test.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__tests__/{clerkGetCurrentUser.test.ts => updateClerkGetCurrentUser.test.ts} (55%) diff --git a/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__tests__/clerkGetCurrentUser.test.ts b/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__tests__/updateClerkGetCurrentUser.test.ts similarity index 55% rename from packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__tests__/clerkGetCurrentUser.test.ts rename to packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__tests__/updateClerkGetCurrentUser.test.ts index 57fc55941fc5..42b0deeae5ab 100644 --- a/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__tests__/clerkGetCurrentUser.test.ts +++ b/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__tests__/updateClerkGetCurrentUser.test.ts @@ -1,5 +1,5 @@ describe('clerk', () => { it('updates the getCurrentUser function', async () => { - await matchTransformSnapshot('clerk', 'default') + await matchTransformSnapshot('updateClerkGetCurrentUser', 'default') }) }) From c25ad776e4ce5166f87baa57d45960a196291c8e Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 22 Feb 2023 17:42:46 -0800 Subject: [PATCH 5/5] fix path again --- .../updateClerkGetCurrentUser.yargs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/updateClerkGetCurrentUser.yargs.ts b/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/updateClerkGetCurrentUser.yargs.ts index c4779c5bab1e..a4ca472a298f 100644 --- a/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/updateClerkGetCurrentUser.yargs.ts +++ b/packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/updateClerkGetCurrentUser.yargs.ts @@ -15,7 +15,7 @@ export const handler = () => { const authFile = isTSProject ? 'auth.ts' : 'auth.js' await runTransform({ - transformPath: path.join(__dirname, 'clerkGetCurrentUser.js'), + transformPath: path.join(__dirname, 'updateClerkGetCurrentUser.js'), targetPaths: [path.join(getRWPaths().api.base, 'src', 'lib', authFile)], })