Redwood and Supabase Auth Example

@0x1a4f7d58 Have you tried some Prisma nested reads?

In their example:

const getUser = await prisma.user.findUnique({
  where: {
    id: 1,
  },
  // select: { name: true } <-- Can't do this!
  include: {
    posts: {
      select: {
        title: true,
      },
    },
  },
})

and that returns


{
  "id": 1,
  "name": null,
  "email": "martina@prisma.io",
  "profileViews": 0,
  "role": "USER",
  "coinflips": [],
  "posts": [
    {
      "title": "How to grow salad"
    },
    {
      "title": "How to ride a horse"
    }
  ]
}

you might be able to do

  const user = await db.user.findUnique({
    where: { uuid: uuid },
    include: {
      account: {
        select: {
          slug: true,
        },
     }
  })

And the user would have all the user attributes and probably an account with a slug attribute.

I think.

const enrichedUserObj = { ...decoded, user.accountId, user.account.slug }

Or, you could just do a nested query on the account and include, join in on the user uuid to return the id of the account and its slug.

That way it is one single db call.

1 Like

@dthyresson Thanks. The query works, however, passing the values to the enriched object doesnā€™t. Iā€™m OK to leave as is for the time being but get that it would be better to do it in a single query.

> db.user.findUnique({where: {uuid: 'cfe9bd27-0122-4fe8-ab58-3f7a6bfb6d43'}, include: {account: {select: {slug: true}}}})
{
  id: 126,
  uuid: 'cfe9bd27-0122-4fe8-ab58-3f7a6bfb6d43',
  createdAt: 2021-07-08T14:37:34.877Z,
  updatedAt: 2021-07-08T14:37:34.877Z,
  email: 'email@email.com',
  name: null,
  accountId: 130,
  account: { slug: 'V52NDE' }
}

Ah, I was coding on the fly, I bet it should be

const enrichedUserObj = { ...decoded, accountId: user.accountId, slug: user.account.slug }
1 Like

Thanks @dthyresson. That works :+1:

I have been experimenting with userMetadata and itā€™s not clear how I can write to either userMetadata.app_metadata or userMetadata.user_metadata.

Supabase API:

UPDATE USER
const { user, error } = await supabase.auth.update({
  email: "new@email.com",
  password: "new-password",
  data: { hello: 'world' }
})

Should I write a custom function for this? Something like Update user data Ā· Issue #23 Ā· supabase/supabase-js Ā· GitHub

I noticed in Docs - Authentication : RedwoodJS Docs that there is an Auth Provider Specific Integration section for most providers but not yet Supabase. If you know of an example or can provide any guidance, that would great. I can update the docs once clear.


Btw, this post might be more suited to this thread Supabase redwood experiments

Hi @0x1a4f7d58 Actually I think thatā€™s exactly how you would update the user metdata:

But, you can still to this in a service.

  • Create SDL for a Mutation that defines the attributes you want in userMetadata (you could do JSON, but better probably if you set). Call that mutation maybe updateUserMetadata()
  • In a userMetadata service, create you update method
  • That method should
  • requireAuth so you make sure they are logged in
  • create an instance of the supabase client but instead of using the non key or the superadmin service role key, use the currentUserā€™s token. That will say that the supabase client can only act on that userā€™s info
  • then supabase.auth.update() the input data from your mutation

You should be able to get the bearer token using context[1].token

So in that service, your supabase client is:

const supabase = createClient('https://xyzcompany.supabase.co', context[1].token)
// ...
const { user, error } = await supabase.auth.update({
  data: { hello: 'world' }
})
export type AuthContextPayload = [
  string | Record<string, unknown> | null,
  { type: SupportedAuthTypes } & AuthorizationHeader,
  { event: APIGatewayProxyEvent; context: GlobalContext & LambdaContext }
]

is what context has so and index 1 is { type: SupportedAuthTypes } & AuthorizationHeader, and the AuthorizationHeader has

export interface AuthorizationHeader {
  schema: 'Bearer' | 'Basic' | string
  token: string
}

So thatā€™s where you get your token to use when creating the supabase client.

I havenā€™t tried this code, but maybe something like:

// userMetadata.sdl
export const schema = gql`
  type UserMetadata {
    full_name: String!
    favorite_color: String
  }
  type Query {
    userMetadata: UserMetadata!
  }
  input UpdateUserMetadataInput {
    full_name: String
    favorite_color: String
  }
  type Mutation {
    updateUserMetadata(input: UpdateUserMetadataInput!): UserMetadata!
  }
`

// services/userMetadata.js
import { createClient } from '@supabase/supabase-js'

export const updateUserMetadata = async ({ input }) => {
  requireAuth()
 
 const supabase = createClient('https://xyzcompany.supabase.co', context[1].token)

 const { user, error } = await supabase.auth.update({
  data: { ...input }
})

return user.user_metadata
}

Or something close to that.

1 Like

Hi @dthyresson Thanks! I think Iā€™m v close.

  1. I had to add the following to graphql.js in order for the context to work, as described here
// api/src/functions/graphql.js
import { getAuthenticationContext } from '@redwoodjs/api/dist/auth'

export const handler = createGraphQLHandler({
  context: async ({ event, context }) => {
    const authContext = await getAuthenticationContext({ event, context })
    return authContext
  },
...
  1. I can make status 200 posts, however, I cannot get the data to save down. I end-up with {"data":{"updateUserMetadata":null}}

This is what Iā€™ve got

// userMetadata.sdl.js
export const schema = gql`
  type UserMetadata {
    firstName: String
    currentLocationId: Int
  }
  type Query {
    userMetadata: UserMetadata
  }
  input UpdateUserMetadataInput {
    firstName: String
    currentLocationId: Int
  }
  type Mutation {
    updateUserMetadata(input: UpdateUserMetadataInput!): UserMetadata
  }
`

and

//userMetadata.js

import { requireAuth } from 'src/lib/auth'
import { createClient } from '@supabase/supabase-js'

export const updateUserMetadata = async ({ input }) => {
  requireAuth()

  const supabase = createClient(process.env.SUPABASE_URL, context[1].token)

  const { user, error } = await supabase.auth.update({
    data: {
      firstName: input.firstName,
      currentLocationId: input.currentLocationId,
    },
    // data: { ...input },
  })

  return user
}

export const userMetadata = () => {
  const supabase = createClient(
    process.env.SUPABASE_URL,
    process.env.SUPABASE_KEY
  )

  const user = supabase.auth.user()

  return user
}

Note: return user.user_metadata here gives ā€œCannot read property ā€˜user_metadataā€™ of nullā€

and then Iā€™m calling the service and passing data on an onClick event (just for testing purposes)

//HomePage.js
...

const UPDATE_USER_METADATA = gql`
  mutation UpdateUserMetadata($input: UpdateUserMetadataInput!) {
    updateUserMetadata(input: $input) {
      firstName
      currentLocationId
    }
  }
`

const HomePage = () => {
  const [update] = useMutation(UPDATE_USER_METADATA, {
    onCompleted: async (data) => {
      toast.success('User metadata updated!')
    },
  })

  const onClick = async () => {
    update({
      variables: {
        input: { firstName: 'Metadata', currentLocationId: 130 },
      },
    })
  }
...

Appreciate thatā€™s a lot to take in.

Not 100% on this but in your cell handling after the metadata update, instead of showing information from the graphql result, you would want to show the userMetadata from useAuth() hook.

What I wonder, though, is if you will need to force Supabase to re-authenticate and update the user metadata is has stored in its session.

You can get the client from the useAuth() hook as well and force a re-auth via await reauthenticate().

That might help.

1 Like

Also, if you log out and login again, do you see your userMetadata updates?

I donā€™t. Feels like the mutation is failing and input = null is being passed to the service (supabase.auth.update)

Hereā€™s what I see after I click

image

image

I donā€™t know if you found a solution, I found this: Supabase authentication && reauthenticate

1 Like