context/currentUser in script environment

I am trying to write a redwood script (yarn rw g script <name>, yarn rw exec <name> that uses my db services to populate data into my database. Some of my service methods assume context.currentUser.id is available (to populate updated_by/created_by fields). It’s not clear to me how to initialize the global context variable outside of a browser session. Ideally, i would just initialize currentUser to a user of my choosing, but doing something like this does not work (not surprising really): context.currentUser = { id: 1 } - it does not seem to modify the global.
If neccessary, I could manually auth as a user (i am using dbAuth), but I’m not even sure what methods to call to do that.

Thanks for any help!

Hey @michalgm

Thanks for the message.

First of all, I should mention that I think overriding/setting the user in a script feels like an anti-pattern to me. If you’re having to execute something as a user, unless for debugging purposes, I would re-think whether it really does belong in a script!

That being said, I was interested to see if I could do it:

I tried this:

import { setContext } from '@redwoodjs/context'

export default async () => {
  setContext({
    currentUser: {
      id: 1,
      email: 'danny@mocked.com',
    },
  })

// 👇 current user seems to be there
  console.log(':: Full context ::', context)

// ❌ we get undefined here. Due to asyncStorage/proxy probably
  console.log(':: Current User ::', context.currentUser)
}

This is a bit of a new case, perhaps @Josh-Walker-GM could help given he’s most familiar with the context changes.

Just to clarify the use case, my plan is to create a ‘service account’ type of user - one that probably won’t even be able to log in via the regular web method. But I think it is useful to log records created/modified by this service accounts vs regular users.
I could modify my create methods to not set the updated/created fields when it can’t lookup a currentUser, but I’d rather these actions be logged just like any other.

I tried what you suggested and got similar results - I used import { setContext } from '@redwoodjs/graphql-server/dist/globalContext.js' (i’m still on 6.5.1). Similarly, if I log the context object, it looks fine, but accessing context.currentUser returns undefined.

I was able to move forward by using axios and a cookie jar to make actual http requests to the GraphQl API, but that seems really ugly. I’d expect automated/non-browser-based data manipulation to be a pretty common use case, so it feels like I’m missing something.

For anyone who ends up here - I still haven’t been able to figure out how to auth to the API from a non-browser client. In the meantime, I’ve been able to use axios with a cookie jar to pose as a browser client. This isn’t very pretty, but it got me past my roadblock.

Here’s my ‘apiClient.js’

const axios = require('axios')
const { wrapper } = require('axios-cookiejar-support')
const apiHost = process.env.PUBLIC_URL
const { CookieJar } = require('tough-cookie')

const cookieJar = new CookieJar()

const axiosInstance = wrapper(
  axios.create({
    jar: cookieJar, // Use the cookie jar
    withCredentials: true, // Send cookies with requests
  })
)

let id = null

const apiRequest = async (url, data, method = 'post') => {
  if (!id && !url.match(/auth$/)) {
    const res = await auth()
    id = res.data.id
  }

  const headers = {
    'Auth-Provider': 'dbAuth',
    Authorization: `Bearer ${id}`,
  }

  return axiosInstance({
    url,
    data,
    method,
    headers,
  })
    .then((response) => {
      if (response.data.errors) {
        throw Error('Error:', response.data.errors.join(' '))
      }
      return response
    })
    .catch((error) => {
      if (error?.response?.data?.errors) {
        console.error(error?.response?.data?.errors)
      } else {
        console.log(error?.response?.data)
      }

      throw Error('Error:', error)
    })
}

const auth = async () => {
  return await apiRequest(`${apiHost}/.redwood/functions/auth`, {
    method: 'login',
    username: process.env.IMPORT_USERNAME,
    password: process.env.IMPORT_PASSWORD,
  })
}

const logout = async () => {
  return await apiRequest(`${apiHost}/.redwood/functions/auth`, {
    method: 'logout',
  })
}

export { apiRequest, logout }

It can be invoked like this:

  await apiRequest(`${apiHost}/.redwood/functions/graphql`, {
    operationName: 'TestMutation',
    query,
    variables: { input: testdata },
  })

Hi @michalgm - curious, what’s the overall task you are trying to accomplish?

Is it to seed your database – but using the existing services to help populate?

I’m periodically loading new data from an external source. I realize I could just disable the user logging if the user context does not exist, but I prefer to have these entries logged as being create by the service user.

One approach you could consider is having a function from your script – which it looks like you want to do by invoking the graphql function, but instead some authenticated function via Serverless Functions (API Endpoints) | RedwoodJS Docs

The example in the docs above is:

import type { APIGatewayEvent, Context } from 'aws-lambda'

import { authDecoder } from '@redwoodjs/auth-dbauth-api'
import { useRequireAuth } from '@redwoodjs/graphql-server'

import { getCurrentUser, isAuthenticated } from 'src/lib/auth'
import { logger } from 'src/lib/logger'

const myHandler = async (event: APIGatewayEvent, context: Context) => {
  logger.info('Invoked myHandler')

  if (isAuthenticated()) {
    logger.info('Access myHandler as authenticated user')

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        data: 'myHandler function',
      }),
    }
  } else {
    logger.error('Access to myHandler was denied')

    return {
      statusCode: 401,
    }
  }
}

export const handler = useRequireAuth({
  handlerFn: myHandler,
  getCurrentUser,
  authDecoder,
})

As long as you pass the proper Authorization headers, you’d then be able to check isAuthenticated, context would be populated (say with your “serviceUser”) and then any service that uses context could have that – is in the example above, just use the service in the isAuthenticated block.

Just be sure to pass the auth-provider and “Authorization: Bearer <>” or whatever header is appropriate.

This seems to imply that I would still need to pass a cookie with the request if I am using dbAuth.

I guess I was hoping there would be some way for me to post my login credentials to an auth endpoint, get a token in the response, and then post that token in a header for additional requests. That’s how most APIs I’ve used work.

If the answer is that the redwood API is only designed for browser clients, I can accept that - the axios+cookiejar workaround isn’t that bad