Using GraphQL Envelop+Helix in Redwood v0.35+

This is my graphql.ts that I use in an app that uses Netlify Identity to authenticate:

import {
  createGraphQLHandler,
  makeMergedSchema,
  makeServices,
} from '@redwoodjs/graphql-server'
import schemas from 'src/graphql/**/*.{js,ts}'
import services from 'src/services/**/*.{js,ts}'

import { getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'

export const handler = createGraphQLHandler({
  loggerConfig: {
    logger,
    options: { tracing: true, operationName: true },
  },
  getCurrentUser,
  schema: makeMergedSchema({
    schemas,
    services: makeServices({ services }),
  }),
  db,
})

That’s all I need.

If you need the Sentry plugin, then I suggest using Envelop and then setting that in the extraPlugins: [useSentry({...yourOptions}] attribute.

Not sure why you’d need to have

  context: async ({ event, context }) => {

    const authContext = await getAuthenticationContext({ event, context })

    return authContext

  },

as that is done here:

Question:

  • What/who’s your auth provider type?
  • You are running on Render?
  • You are running via api serve? Ie, not serverless?
  • What is in requireAuth() that is checking and saying Forbidden? Is current user present?
  • If you log while in getCurrentUser() do you see the user info?
  • Do you see same behavior locally running via yarn rw serve?
  • Firebase is auth provider.
  • Not deployed to Render yet - was just testing locally so far. But yes I do use render.
  • Yep that’s right. When deployed it’s ran with yarn api-server --functions ./dist/functions --port 80.
  • For local dev - Right now I use yarn rw dev --fwd=\"--disable-host-check=true\" & CONTEXT=PORTAL_APP yarn rw dev web --fwd=\"--disable-host-check=true --port=7910\" --generate=false for dev.

This is my requireAuth

export const requireAuth = ({ role } = {}) => {

  if (!context.currentUser) {

    throw new AuthenticationError("You don't have permission to do that.")

  }

  if (typeof role !== 'undefined' && typeof role === 'string' && !context.currentUser.roles?.includes(role)) {

    throw new ForbiddenError("You don't have access to do that.")

  }

  if (typeof role !== 'undefined' && Array.isArray(role) && !context.currentUser.roles?.some((r) => role.includes(r))) {

    throw new ForbiddenError("You don't have access to do that.")

  }

}
  • If you log while in getCurrentUser() do you see the user info?

Do you mean the token and details like that?

Update:

If I log user info in getCurrentUser I do see a token, and firebase user does get resolved.

export const getCurrentUser = async (_decoded, { token, _type }) => {

  const { _email, uid, ...userProps } = await firebaseApp.auth().verifyIdToken(token)

  console.log(token, uid, userProps)

All of that prints out valid data.

Ok, so the handler does call getCurrentUser and you say the user is decoded and the requireAuth() function is called but context.currrentUser is null or undefined thus raising a Forbidden error? And if you log or inspect the context in requireAuth() it isn’t there?

In local dev do you have the useEnvelop experimental flag set to true in redwood.toml?

Yep useEnvelop is set to true in redwood.toml. I can see api esbuild with envelop in the logs

context.currentUser is actually an empty object, when I print it out in the requireAuth.

This is a stack trace for one of the queries.

api esbuild with envelop | INFO [2021-07-23 05:00:04.942 +0000] (graphql-server): GraphQL execution started
api esbuild with envelop |     operationName: "PublicPortalSidebarQuery"
api esbuild with envelop | {}
api esbuild with envelop | ERROR [2021-07-23 05:00:04.955 +0000] (graphql-server): GraphQL execution completed with errors:
api esbuild with envelop |     errors: [
api esbuild with envelop |       {
api esbuild with envelop |         "message": "You don't have permission to do that.",
api esbuild with envelop |         "locations": [
api esbuild with envelop |           {
api esbuild with envelop |             "line": 2,
api esbuild with envelop |             "column": 3
api esbuild with envelop |           }
api esbuild with envelop |         ],
api esbuild with envelop |         "path": [
api esbuild with envelop |           "myPortalToken"
api esbuild with envelop |         ],
api esbuild with envelop |         "extensions": {
api esbuild with envelop |           "code": "UNAUTHENTICATED"
api esbuild with envelop |         }
api esbuild with envelop |       }
api esbuild with envelop |     ]
api esbuild with envelop |     operationName: "PublicPortalSidebarQuery"
api esbuild with envelop | POST /graphql 200 157.606 ms - 195

Hey DT - were you able to reproduce it on your end? or have any suggestions based on the changes I made to the code?

I was not.

But do you still have

  context: async ({ event, context }) => {

    const authContext = await getAuthenticationContext({ event, context })

    return authContext

  },

in your handler?

Not any more. I have removed it for this implementation.

Out of curiosity, if you run simply via yarn rw dev does it behave differently?

Or even yarn rw serve?

What is the envar CONTEXT=PORTAL_APP used for?

Ive tried with yarn rw dev and same thing happens. I use the CONTEXT var to switch my routes between portal app and admin app - it’s front-end really - shouldn’t affect this.

Not sure if it makes a different but technically I have to run with yarn rw dev --fwd="--disable-host-check=true" because I visit my app at http://app.uservitals.local:8910/

I am actually hitting the same thing now as well where the context.currentUser is now null and requests are saying unauthorized after I updated my code to use envelop + helix

Thanks @KrisCoulson myself and @danny will try to track this down and fix. If you can share your getCurrentUser function, GraphQL handler, and how you are using requireAuth— in a function or a service that can help. Also your auth provider. Thanks

Docs for using Sentry.io with redwood

Install the required libraries:

yarn add @envelop/sentry @sentry/node @sentry/tracing

Create a reusable Sentry file api/src/lib/sentry

import * as Sentry from '@sentry/node'
import * as Tracing from '@sentry/tracing'

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.5,
})

export default Sentry

Then import it into your graphql function api/src/functions/graphql

import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import { useSentry } from '@envelop/sentry'
import Sentry from 'src/lib/sentry'

import { getCurrentUser } from 'src/lib/auth'
import { logger } from 'src/lib/logger'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
import { db } from 'src/lib/db'

const extraPlugins = [
  useSentry({
    includeRawResult: false, // set to `true` in order to include the execution result in the metadata collected
    includeResolverArgs: false, // set to `true` in order to include the args passed to resolvers
    includeExecuteVariables: false, // set to `true` in order to include the operation variables values
    // appendTags: (args) => { return { ... }} // if you wish to add custom "tags" to the Sentry transaction created per operation
  }),
]

export const handler = createGraphQLHandler({
  getCurrentUser,
  loggerConfig: {
    logger,
    options: { tracing: true, operationName: true },
  },
  directives,
  sdls,
  services,
  extraPlugins,
  onException: () => {
    db.$disconnect()
  },
})

For a custom function, such as api/src/functions/checkout

import Sentry from 'src/lib/sentry'
import { processOrder } from 'src/lib/wyre/order'

export const handler = async (event) => {
  // Default response
  let statusCode = 200
  let message = ''

  try {
    let body = event.body
    if (typeof body === 'string') body = JSON.parse(body)

    await processOrder({ orderId: body.orderId, referenceId: body.referenceId })

    return {
      statusCode,
      body: {
        message: 'Success!',
      },
    }
  } catch (e) {
    console.log(event)
    Sentry.captureException(e)
    return {
      statusCode, // Always return 200
      body: {
        message: 'Internal server error',
      },
    }
  }
}

:tada: Tada! Please share your experience if you use Sentry or other plugins

3 Likes

@viperfx You also set up Sentry the other day, didn’t you? Did you do it the same way?

All it’s way easier now.

There is an envelop plug-in and you add it as ‘extraPlugins’ in the createGraphQLHandler.

We’ll add some docs to help know how to setup, but that’s what @viperfx used.

The Guild recently made it easier to set the current user as well.

1 Like

Oh right, I forgot about getCurrentUser

//api/src/lib/auth.js
import Sentry from 'src/lib/sentry'

export const getCurrentUser = async (session) => {
  const member = await db.member.findUnique({ where: { id: session.id } })
  if (!member) return null
  // WARNING: Returned values here are exposed to the FE
  Sentry.setUser({ username: member.username, id: member.id })

Just sent through this and it worked like a charm. Thank you, @pi0neerpat!

2 Likes