Skip to content

Commit

Permalink
Merge branch 'main' of github.com:redwoodjs/redwood into feat/api-ski…
Browse files Browse the repository at this point in the history
…p-prebuild

* 'main' of github.com:redwoodjs/redwood:
  chore(deps): update dependency @types/uuid to v9.0.1 (redwoodjs#7680)
  chore(deps): update dependency @replayio/playwright to v0.3.23 (redwoodjs#7677)
  chore(deps): update dependency @npmcli/arborist to v6.2.3 (redwoodjs#7675)
  chore(deps): update dependency @envelop/types to v3.0.2 (redwoodjs#7674)
  chore: add codemod for clerk fix in v4.2.0 (redwoodjs#7676)
  chore(deps): update dependency @clerk/types to v3.28.1 (redwoodjs#7652)
  chore(deps): update dependency @envelop/testing to v5.0.6 (redwoodjs#7673)
  Update directives.md (redwoodjs#7670)
  fix(deps): update dependency vscode-languageserver-types to v3.17.3 (redwoodjs#7636)
  Fix `yarn rw exec` to set nonzero exit code on error (redwoodjs#7660)
  • Loading branch information
dac09 committed Feb 24, 2023
2 parents d86822e + da0bc32 commit ccebbeb
Show file tree
Hide file tree
Showing 14 changed files with 425 additions and 70 deletions.
2 changes: 1 addition & 1 deletion docs/docs/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ type user {

and if the `currentUser` is an `ADMIN`, then skip the masking transform and simply return the original resolved field value:

```jsx title="./api/directives/maskedEmail.directive.js"
```jsx title="./api/src/directives/maskedEmail.directive.js"
import { createTransformerDirective, TransformerDirectiveFunc } from '@redwoodjs/graphql-server'

export const schema = gql`
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"vscode-languageserver": "6.1.1",
"vscode-languageserver-protocol": "3.15.3",
"vscode-languageserver-textdocument": "1.0.8",
"vscode-languageserver-types": "3.17.2"
"vscode-languageserver-types": "3.17.3"
},
"devDependencies": {
"@actions/core": "1.10.0",
Expand All @@ -49,10 +49,10 @@
"@babel/preset-react": "7.18.6",
"@babel/preset-typescript": "7.18.6",
"@babel/runtime-corejs3": "7.20.13",
"@npmcli/arborist": "6.2.2",
"@npmcli/arborist": "6.2.3",
"@nrwl/nx-cloud": "15.0.3",
"@playwright/test": "1.30.0",
"@replayio/playwright": "0.3.21",
"@replayio/playwright": "0.3.23",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/auth-providers/clerk/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@babel/cli": "7.20.7",
"@babel/core": "7.20.12",
"@clerk/clerk-react": "4.11.3",
"@clerk/types": "3.27.0",
"@clerk/types": "3.28.1",
"@types/react": "18.0.28",
"jest": "29.4.2",
"react": "18.2.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/auth-providers/dbAuth/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@simplewebauthn/server": "6.2.2",
"@types/crypto-js": "4.1.1",
"@types/md5": "2.3.2",
"@types/uuid": "9.0.0",
"@types/uuid": "9.0.1",
"jest": "29.4.2",
"typescript": "4.9.5"
},
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/execHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export const handler = async (args) => {
})
} catch (e) {
console.error(c.error(`Error in script: ${e.message}`))
throw e
}
},
},
Expand Down
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.")
}
}
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.")
}
}
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')
})
})
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',
})
}
Loading

0 comments on commit ccebbeb

Please sign in to comment.