How to create a User object in the db after a signup is verified in Netlify

Hey guys, really nice job the one you are doing here. Very excited to be adopting this framework on such an early stage. Hope to contribute on its growth, at least asking some silly questions. :slight_smile:

I have been playing around with the new auth API and itā€™s great, good job!

Now Iā€™m trying to create a user object in the db after a signup is verified in Netlify but I get a Prisma error Invalid prisma.user.create() invocation (complete error below).

Approach:
To set a webbook notification from Netlify to a custom Function that triggers createUser service with the data of the event.

The question:
Itā€™s possible to import and reuse the service function createUser() created with scaffold inside another Function (lambda), what is the correct way?

On the docs says:

But you could still use it yourselfā€”services are just Javascript functions so you can use them anywhere youā€™d like:
From another service
In a custom lambda function
From a completely separate, custom API

I tried to reuse the API service for the User object, but I couldnā€™t. Ended up using the solution in this ticket, re-importing Prisma client.

What I tried so far:

Iā€™ve imported a service inside handleSignup.js Function, it receives a webhook from Netlify when a user sign ups

// /api/src/functions/handleSignup.js

import { createUser } from 'src/services/users/users'

Then I call it with await with the input data as object:

// /api/src/functions/handleSignup.js

export const handler = async (event, context) => {

  const data = JSON.parse(event.body);
  const { user } = data;

	const newUser = await createUser({
          role: 'admin',
          email: user.email, // 'leoalbin@gmail.com'
          name: user.user_metadata.full_name // 'Leo Test'
  } )

... 

// some other stuff and response

And I get this error:

PrismaClientValidationError: 
00:09:29 api | Invalid `prisma.user.create()` invocation in
00:09:29 api | /Volumes/Documentos/Produccion/WILDTRACKING/wildtracking/api/src/services/users/users.js:14:18
00:09:29 api | 
00:09:29 api | {
00:09:29 api | + data: {
00:09:29 api | +   id?: String,
00:09:29 api | +   role: String,
00:09:29 api | +   email: String,
00:09:29 api | +   name?: String,
00:09:29 api | +   createdAt?: DateTime
00:09:29 api | + }
00:09:29 api | }
00:09:29 api | 
00:09:29 api | Argument data is missing.
00:09:29 api | 
00:09:29 api | Note: Lines with + are required
00:09:29 api | 
00:09:29 api |     at Document.validate (/Volumes/Documentos/Produccion/WILDTRACKING/wildtracking/node_modules/@prisma/client/src/runtime/query.ts:256:19)
00:09:29 api |     at Object.model [as User] (/Volumes/Documentos/Produccion/WILDTRACKING/wildtracking/node_modules/@prisma/client/src/runtime/getPrismaClient.ts:394:20)
00:09:29 api |     at Object.create (/Volumes/Documentos/Produccion/WILDTRACKING/wildtracking/node_modules/@prisma/client/src/runtime/getPrismaClient.ts:519:33)
00:09:29 api |     at createUser (/Volumes/Documentos/Produccion/WILDTRACKING/wildtracking/api/src/services/users/users.js:14:18)
00:09:29 api |     at handler (/Volumes/Documentos/Produccion/WILDTRACKING/wildtracking/api/src/functions/handleSignup.js:24:25)
00:09:29 api |     at /Volumes/Documentos/Produccion/WILDTRACKING/wildtracking/node_modules/@redwoodjs/dev-server/src/main.ts:169:26
00:09:29 api |     at Layer.handle [as handle_request] (/Volumes/Documentos/Produccion/WILDTRACKING/wildtracking/node_modules/express/lib/router/layer.js:95:5)
00:09:29 api |     at next (/Volumes/Documentos/Produccion/WILDTRACKING/wildtracking/node_modules/express/lib/router/route.js:137:13)
00:09:29 api |     at next (/Volumes/Documentos/Produccion/WILDTRACKING/wildtracking/node_modules/express/lib/router/route.js:131:14)
00:09:29 api |     at next (/Volumes/Documentos/Produccion/WILDTRACKING/wildtracking/node_modules/express/lib/router/route.js:131:14)

This is my sdl file for Users:

export const schema = gql`
  type User {
    id: String!
    role: String!
    email: String!
    name: String
    createdAt: DateTime!
  }

  type Query {
    users: [User!]!
    user(id: String!): User!
  }

  input CreateUserInput {
    role: String!
    email: String!
    name: String
  }

  input UpdateUserInput {
    role: String
    email: String
    name: String
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: String!, input: UpdateUserInput!): User!
    deleteUser(id: String!): User!
  }
`

And the service file:

// api/src/services/users/users.js

import { db } from 'src/lib/db'

export const users = () => {
  return db.user.findMany()
}

export const user = ({ id }) => {
  return db.user.findOne({
    where: { id },
  })
}

export const createUser = ({ input }) => {
  return db.user.create({
    data: input,
  })
}

export const updateUser = ({ id, input }) => {
  return db.user.update({
    data: input,
    where: { id },
  })
}

export const deleteUser = ({ id }) => {
  return db.user.delete({
    where: { id },
  })
}

I did this and finally got it to work:

// /api/src/functions/handleSignup.js

const { PrismaClient } = require('@prisma/client')
const dotenv = require('dotenv')
dotenv.config()
const db = new PrismaClient()

export const handler = async (event, context) => {
  const data = JSON.parse(event.body);
  const { user } = data;

  //this response modify the data in Netlify 
	const responseBody = {
    app_metadata: {
      roles: ["admin"],
      my_user_info: "this is some user info"
    },
    user_metadata: {
      ...user.user_metadata, // append current user metadata
      custom_data_from_function: "hurray this is some extra metadata"
    }
  }
  const newUser = await db.user.create(
    { data: {
          role: 'admin',
          email: user.email,
          name: user.user_metadata.full_name
        }
  } )
  db.disconnect()

if(newUser.id) {
  const newAccount = await db.account.create(
    { data: {
          adminUserId: newUser.id,
        }
  } )
  db.disconnect()
}

  return {
    statusCode: 200,
    body: JSON.stringify(responseBody),
  }
}

But I would like to reuse the functions from the services as my database interface. Is that ok or the services were not for that?

What Iā€™m missing here?

Thanks a lot :slight_smile:

1 Like

createUser takes an object with key input as an argument! :slight_smile:

Try this:

const newUser = await createUser({
  input: {
    role: 'admin',
    email: user.email, // 'leoalbin@gmail.com'
    name: user.user_metadata.full_name // 'Leo Test'
  }
})
1 Like

On the Prisma side, I believe you can simply reuse the Client by importing db.js, e.g.:

import { db } from 'src/lib/db'

That import, Robertā€™s suggestion above, and the handler seems like it should do the trick.

Lastly, one reaction about your overall Approach (with the caveat Iā€™m definitely no expert with Netlify Identity) --> instead of a separate hook thatā€™s only called at the time of a createUser event, what if (via a Service) you always checked the following at the time of login:

  • does User exist in DB? (via UID check)
  • does User data on Netlify match data in DB?
  • etc.

ā€¦then took appropriate steps depending on results of checks.

1 Like

Thanks a lot, @thedavid and @Robert, it worked.

Here I copy my current approach, I accept all the improvements proposals, and hope it could be helpful for some else in the future.

Approach:

Netlify will send a webhook notification to a custom Function, set as endpoint in Identity/Notifications setup:

Here is a mockup of the event object that Netlify will send if there is ā€œsignupā€ event:
{"event":"signup","instance_id":"dc6a24b4-d394-4e96-b5de-5928fddeb272","user":{"id":"7e9f75a2-bac0-4a0c-8da9-4b0df2890f77","aud":"","role":"","email":"test6@test.com","confirmation_sent_at":"2020-05-22T22:17:02Z","app_metadata":{"provider":"email"},"user_metadata":{"full_name":"Test"},"created_at":"2020-05-22T22:17:02Z","updated_at":"2020-05-22T22:17:02Z"}}

Looking inside the event we could define what kind of event is and run the appropriate logic doing something like:

if( event === 'login' ) {
    //do some loging stuff
}

if( event === 'signup' ) {
    //do some signup stuff
} 

The response we return will modify the Netlify user data and metadata, in this case we will add the role ā€˜adminā€™, userId (in our db), and the accountId (in our db). So then we could match userId on Netlify with userId and accountId in our db. We could add any field as metadata.

Here is my working version of the custom Function endpoint:

// api/src/funcitons/handleSignup.js

import { createAccount } from 'src/services/accounts/accounts'
import { createUser } from 'src/services/users/users'
import { db } from 'src/lib/db'

const signUpErrorHandler = (e) => {
  console.log(e);
  // switch(e.code){
     // case 'P2002'
      // return { code: '  ' }
  // }
}

export const handler = async (event, context) => {
  const data = JSON.parse(event.body);
  const { user } = data;
  const eventData  = data.event;
  let newAccount = {};
  let newUser = {};

    if( eventData === 'login' ) {
        //TO DO
        // check if user exists on database
          // if not create a user on database
        //  check if user data matches netlify data
          // if not update user data on data base
    }

    if( eventData == 'signup' ) {
      try {
            newUser = await createUser(
            { input: {
                  role: 'admin',
                  email: user.email,
                  name: user.user_metadata.full_name
                }
          } )
          db.disconnect()
          if( newUser && newUser.id ) {
              newAccount = await createAccount({
              input: {
                      adminUserId: newUser.id,
                    }
              } )
             db.disconnect()
              console.log(newUser, newAccount)
          }
       } catch (e) {
          signUpErrorHandler(e)
      }
    }

 const responseBody = {
    app_metadata: {
      roles: ['admin'],
    },
    user_metadata: {
      ...user.user_metadata, // append current user metadata
      'accountID' : newAccount.id,
      'userID' : newUser.id
    }
  }

  return {
    statusCode: 200,
    body: JSON.stringify(responseBody),
  }
}

A couple of open things:

  • We are catching the Prisma errors but not sure if the method is the correct one, and currently the error codes are not very human friendly.
  • We are importing the db from src/lib/db, but it works without that, I tested. I imported just for doing the db.disconnect() line. Not sure if necessary.
5 Likes