-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: add codemod for clerk fix in v4.2.0 (#7676)
* chore: add codemod for clerk fix in v4.2.0 * fix transform file * rename dir for consistency * fix test paths * fix path again
- Loading branch information
Showing
5 changed files
with
354 additions
and
0 deletions.
There are no files selected for viewing
122 changes: 122 additions & 0 deletions
122
.../codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__testfixtures__/default.input.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.") | ||
} | ||
} |
129 changes: 129 additions & 0 deletions
129
...codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/__testfixtures__/default.output.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.") | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
...src/codemods/v4.2.x/updateClerkGetCurrentUser/__tests__/updateClerkGetCurrentUser.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
describe('clerk', () => { | ||
it('updates the getCurrentUser function', async () => { | ||
await matchTransformSnapshot('updateClerkGetCurrentUser', 'default') | ||
}) | ||
}) |
74 changes: 74 additions & 0 deletions
74
packages/codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/updateClerkGetCurrentUser.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', | ||
}) | ||
} |
24 changes: 24 additions & 0 deletions
24
...codemods/src/codemods/v4.2.x/updateClerkGetCurrentUser/updateClerkGetCurrentUser.yargs.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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-clerk-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, 'updateClerkGetCurrentUser.js'), | ||
targetPaths: [path.join(getRWPaths().api.base, 'src', 'lib', authFile)], | ||
}) | ||
|
||
setOutput('All done! Run `yarn rw lint --fix` to prettify your code') | ||
}) | ||
} |