Updating users roles via Netlify Serverless function (When logged in as another user)

Oh, so you wish to build an admin panel for your own management? That should be possible, but just to clarify, would you be logged in using Identity too or do you have any other auth for the admin dashboard?

Yeah that’s correct, planning on using netlify identity for this too at least for the initial concept. Do you see any issues with this?

I think I might have thought of a way, though I’d have to test it. If it’s not super urgent, do you mind if I revert to you after a few hours or by tomorrow?

Yeah that is fine and appreciate that! Thank you! :pray:

Hey @carvill,

Thank you for your patience. I’ve brought some good news! Turns out it’s possible. Definitely a hack, but it works (at least worked in my production tests :joy:, hope it does for you too).

So yeah, the solution is simple in concept and it’s actually great that you’re going to use the admin account thought Netlify Identity too. So what you’ve got to do is this:

If you’re using GoTrueJS to generate a custom sign-in/sign-up flow, you can do it like this:

import GoTrue from 'gotrue-js'

// call this when you're already logged in as yourself
const admin = new GoTrue({
  APIUrl: 'https://<your-wesbsite>/.netlify/identity'
}).currentUser()

fetch('/.netlify/functions/updateRole/', {
  method: 'patch',
  body: JSON.stringify({
    email: 'target email address', // dynamically fill these values as needed
    role: 'role to apply'
  }),
  headers: {
    'content-type': 'application/json',
    Authorization: `Bearer ${admin.token.access_token}`
  }
}).then(response => {
    return response.json()
  }).then(updatedUser => {
    // updatedUser is the user with new role(s) 
    console.log(updatedUser)
  })

Even if you use Netlify identity Widget, the process is going to be fairly similar, except instead you’d get your own admin account like this: const admin = netlifyIdentity.currentUser().

Finally, in your serverless function (updateRole.js):

import fetch from 'node-fetch'

export async function handler(event, context) {
  const payload = JSON.parse(event.body)
  const identity = context.clientContext.identity
  return fetch(`${identity.url}/admin/users`, {
    headers: {
      Authorization: `Bearer ${identity.token}`
    }
  }).then(response => {
    return response.json()
  }).then(data => {
    const requiredUser = data.users.filter(user => {
      return user.email === payload.email
    })
    return fetch(`${identity.url}/admin/users/${requiredUser[0].id}`, {
      method: 'put',
      body: JSON.stringify({
        app_metadata: {
          roles: [payload.role]
        }
      }),
      headers: {
        Authorization: `Bearer ${identity.token}`
      }
    }).then(response => {
      return response.json()
    }).then(updatedUser => {
      return {
        statusCode: 200,
        body: JSON.stringify(updatedUser)
      }
    })
  })
}

You can simply copy-paste it. To add an explanation on how it works:

Basically, you are sending your own auth token (something that’s required to use Identity stuff inside functions), instead of your users’. This won’t make any difference, only that you could login to your (admin) account. Our problem was that we need to get an auth token to send along with the fetch as a header. This was not possible as we needed to login as the user, but now we don’t. You can login as yourself and get the token and that would work too.

Once you get your identity instance initialised inside the function, there’s an endpoint you can hit to get all users: GitHub - netlify/gotrue-js: JavaScript client library for GoTrue. That’s what we do. We get a list of all users for your identity instance and then filter the one we need. We filter it based on the payload data you send from your frontend. The email you specify there would be matched against the list of users you’ve. Thus, you’d get the required user.

Once you get the required user, you can get their ID and that’s all needed to make another request to update their role. The role is the one you send as payload from your frontend.

Note that, you’d have to add error handling and test for edge cases. This is just a primitive demo to update the roles without logging in as the user. Also, you probably need to add another layer of security. For example, in the current state, anyone with a valid auth token (any user of your identity instance), can hit your function with a JSON payload and update themselves. So, you can do some checks internally to see if the user object carried in the context is your account or not (you can check for email address, or check the role or other meta data). If you can verify it as yourself, only then proceed with the function, or return a 401 (unauthorised).

Hope this helps!

Amazing! will have a look at that today!!! Cheers so much for your help

1 Like

Sorry for the slow response I have been trying to get this to work from my side but I am struggling!

I get this error in my serverless function logs

4:46:07 PM: 2021-07-29T15:46:07.470Z	undefined	ERROR	Uncaught Exception 	{"errorType":"Runtime.UserCodeSyntaxError","errorMessage":"SyntaxError: Cannot use import statement outside a module","stack":["Runtime.UserCodeSyntaxError: SyntaxError: Cannot use import statement outside a module","    at _loadUserApp (/var/runtime/UserFunction.js:98:13)","    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)","    at Object.<anonymous> (/var/runtime/index.js:43:30)","    at Module._compile (internal/modules/cjs/loader.js:999:30)","    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)","    at Module.load (internal/modules/cjs/loader.js:863:32)","    at Function.Module._load (internal/modules/cjs/loader.js:708:14)","    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)","    at internal/main/run_main_module.js:17:47"]}
4:46:07 PM: 2021-07-29T15:46:07.689Z	undefined	ERROR	Uncaught Exception 	{"errorType":"Runtime.UserCodeSyntaxError","errorMessage":"SyntaxError: Cannot use import statement outside a module","stack":["Runtime.UserCodeSyntaxError: SyntaxError: Cannot use import statement outside a module","    at _loadUserApp (/var/runtime/UserFunction.js:98:13)","    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)","    at Object.<anonymous> (/var/runtime/index.js:43:30)","    at Module._compile (internal/modules/cjs/loader.js:999:30)","    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)","    at Module.load (internal/modules/cjs/loader.js:863:32)","    at Function.Module._load (internal/modules/cjs/loader.js:708:14)","    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)","    at internal/main/run_main_module.js:17:47"]}
4:46:07 PM: 880460c5 Duration: 173.90 ms	Memory Usage: 15 MB	4:46:07 PM: Unknown application error occurred
Runtime.UserCodeSyntaxError

I have a Vue function that runs this…

setUserRole(role) {
        // // call this when you're already logged in as yourself
        const admin = netlifyIdentity.currentUser();

        fetch('/.netlify/functions/updateRole/', {
          method: 'patch',
          body: JSON.stringify({
            email: this.userEmail, // dynamically fill these values as needed
            role: role
          }),
          headers: {
            'content-type': 'application/json',
            Authorization: `Bearer ${admin.token.access_token}`
          }
        }).then(response => {
            return response.json();
          }).then(updatedUser => {
            // updatedUser is the user with new role(s) 
            console.log(updatedUser)
          })
        }      
      }

Then inside my serverless functin updateRole :

import fetch from 'node-fetch'

export async function handler(event, context) {
  const payload = JSON.parse(event.body)
  const identity = context.clientContext.identity
  return fetch(`${identity.url}/admin/users`, {
    headers: {
      Authorization: `Bearer ${identity.token}`
    }
  }).then(response => {
    return response.json()
  }).then(data => {
    const requiredUser = data.users.filter(user => {
        debugger;
      return user.email === payload.email
    })
    return fetch(`${identity.url}/admin/users/${requiredUser[0].id}`, {
      method: 'put',
      body: JSON.stringify({
        app_metadata: {
          roles: [payload.role]
        }
      }),
      headers: {
        Authorization: `Bearer ${identity.token}`
      }
    }).then(response => {
        console.log(response.json());
      return response.json();

    }).then(updatedUser => {
      return {
        statusCode: 200,
        body: JSON.stringify(updatedUser)
      }
    })
  })
}

I can’t seem to figure out where I am going wrong in comparison to your example?

Sorry, I forgot to mention, you need to add this to netlify.toml:

[functions]
  node_bundler = "esbuild"

This will bundle it into a module.

Also, I’m not sure what the debugger is here:

Getting closer i’m sure!

However nothing in the function log since the addition of:

[functions]
  node_bundler = "esbuild"

But no change of the user role when calling the function :frowning: !

I did re-read your response and you say the following:

So yeah, the solution is simple in concept and it’s actually great that you’re going to use the admin account thought Netlify Identity too:

I might be looking into this too much, but do I need to use my netlify main login rather than a user that has signed up via Netlify identity?

Also I have removed the debugger, that was my bad :confused:

No, what I meant was, the account that you’d be using as an admin is also one of your Netlify Identity members.

That is strange. Could you add some log statements to capture the progress at each step? For example, you could do something like:

import fetch from 'node-fetch'

export async function handler(event, context) {
  const payload = JSON.parse(event.body)
  const identity = context.clientContext.identity
  console.log('step 1 done')
  return fetch(`${identity.url}/admin/users`, {
    headers: {
      Authorization: `Bearer ${identity.token}`
    }
  }).then(response => {
    return response.json()
  }).then(data => {
    console.log('step 2 done')
    const requiredUser = data.users.filter(user => {
      return user.email === payload.email
    })
    return fetch(`${identity.url}/admin/users/${requiredUser[0].id}`, {
      method: 'put',
      body: JSON.stringify({
        app_metadata: {
          roles: [payload.role]
        }
      }),
      headers: {
        Authorization: `Bearer ${identity.token}`
      }
    }).then(response => {
        console.log(response.json());
      return response.json();
    }).then(updatedUser => {
      console.log('step 3 done')
      return {
        statusCode: 200,
        body: JSON.stringify(updatedUser)
      }
    })
  })
}

Then we can see till where the code is actually executing without a problem.

Also, I see that you’ve this line in your client-side code:

It should log the updated user. Doesn’t it mention the new role in it?

Okay, it seems like its not getting into the serverless function now!

Got these errors in my console window obviously the XX is my domain)

patch https://XX/.netlify/functions/updateRole/ 405

and

Uncaught (in promise) SyntaxError: Unexpected end of JSON input

The 405 Method Not Allowed error occurs when the web server is configured in a way that does not allow you to perform a specific action for a particular URL. It’s an HTTP response status code that indicates that the request method is known by the server but is not supported by the target resource.

Anything extra I need to do to allow this?

fetch('/.netlify/functions/updateRole/', {
          method: 'patch',
          body: JSON.stringify({
            email: this.userEmail, // dynamically fill these values as needed
            role: role
          }),
          headers: {
            'content-type': 'application/json',
            Authorization: `Bearer ${admin.token.access_token}`
          }
        }).then(response => {
            return response.json();
          }).then(updatedUser => {
            // updatedUser is the user with new role(s) 
            console.log(updatedUser)
          })
        }   

Could you try changing it to post? I think I might have changed it to patch just for the fun of it after I got the code working otherwise. I don’t remember if that’s the case exactly, but yeah, post should do it.

Okay, getting closer, but back to this error in the console log:

Response {type: "basic", url: "https://xx.netlify.app/.netlify/functions/updateRole/", redirected: false, status: 502, ok: false, …}
body: (...)
bodyUsed: true
headers: Headers {}
ok: false
redirected: false
status: 502
statusText: ""
type: "basic"
url: "https://xx.netlify.app/.netlify/functions/updateRole/"
__proto__: Response
{errorType: "Runtime.UserCodeSyntaxError", errorMessage: "SyntaxError: Cannot use import statement outside a module", trace: Array(10)}
errorMessage: "SyntaxError: Cannot use import statement outside a module"
errorType: "Runtime.UserCodeSyntaxError"
trace: (10) ["Runtime.UserCodeSyntaxError: SyntaxError: Cannot use import statement outside a module", "    at _loadUserApp (/var/runtime/UserFunction.js:98:13)", "    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)", "    at Object.<anonymous> (/var/runtime/index.js:43:30)", "    at Module._compile (internal/modules/cjs/loader.js:999:30)", "    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)", "    at Module.load (internal/modules/cjs/loader.js:863:32)", "    at Function.Module._load (internal/modules/cjs/loader.js:708:14)", "    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)", "    at internal/main/run_main_module.js:17:47"]
__proto__: Object

And this error in the serverless functions log:

11:02:25 AM: 2021-07-30T10:02:25.581Z	undefined	ERROR	Uncaught Exception 	{"errorType":"Runtime.UserCodeSyntaxError","errorMessage":"SyntaxError: Cannot use import statement outside a module","stack":["Runtime.UserCodeSyntaxError: SyntaxError: Cannot use import statement outside a module","    at _loadUserApp (/var/runtime/UserFunction.js:98:13)","    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)","    at Object.<anonymous> (/var/runtime/index.js:43:30)","    at Module._compile (internal/modules/cjs/loader.js:999:30)","    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)","    at Module.load (internal/modules/cjs/loader.js:863:32)","    at Function.Module._load (internal/modules/cjs/loader.js:708:14)","    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)","    at internal/main/run_main_module.js:17:47"]}
11:02:25 AM: 2021-07-30T10:02:25.794Z	undefined	ERROR	Uncaught Exception 	{"errorType":"Runtime.UserCodeSyntaxError","errorMessage":"SyntaxError: Cannot use import statement outside a module","stack":["Runtime.UserCodeSyntaxError: SyntaxError: Cannot use import statement outside a module","    at _loadUserApp (/var/runtime/UserFunction.js:98:13)","    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)","    at Object.<anonymous> (/var/runtime/index.js:43:30)","    at Module._compile (internal/modules/cjs/loader.js:999:30)","    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)","    at Module.load (internal/modules/cjs/loader.js:863:32)","    at Function.Module._load (internal/modules/cjs/loader.js:708:14)","    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)","    at internal/main/run_main_module.js:17:47"]}
11:02:25 AM: 7daffa72 Duration: 152.61 ms	Memory Usage: 14 MB	11:02:25 AM: Unknown application error occurred
Runtime.UserCodeSyntaxError

That is strange. Do you have a repo I could look at (and probably test it myself)?

Are we able to do this a bit more privately? rather than share credentials over the forum? does that option exist?

Yeah definitely, you could send the link to me in a PM (go to my user profile and send me a message), or if your repo is on GitHub, you can directly add me to it, my GitHub username: Hrishikesh-K.

Thanks pinged a message over!

So, according to the discussion we had, the code was working fine for a small number of users. But, @carvill had around 14k users where this code was failing. Chances are, the size of the users array was getting more than what a serverless function could handle. Thus, the solution was to store the identity user ID in an external database which you can query. Then, you just have to pass the ID with the payload and in the function, change role like:

const fetch = require('node-fetch')

exports.handler =  async (event, context) => {
  const payload = JSON.parse(event.body)
  const identity = context.clientContext.identity
  return fetch(`${identity.url}/admin/users/${payload.netlifyId}`, {
    method: 'put',
    body: JSON.stringify({
      app_metadata: {
        roles: [payload.role]
      }
    }),
    headers: {
      Authorization: `Bearer ${identity.token}`
    }
  }).then(response => {
    return response.json()
  }).then(updatedUser => {
    return {
      statusCode: 200,
      body: JSON.stringify(updatedUser)
    }
  })
}
1 Like

Thanks again for your help on this, you have been outstanding!
I can confirm the reposne marked as “Solution” is working for me for over 14k users! :muscle: Big big thanks again to @hrishikesh