Authorise based on Roles

In this tutorial, we will explore how to implement user role authentication within your MagicJS application.

Authorise based on Roles

Before moving forward, it's essential to confirm that user authentication has been properly configured in your MagicJS application, as outlined in the previous section. We'll be utilizing the same files in this tutorial, so ensure that authentication is in place.

If you're unfamiliar with user authentication, please refer Authenticate Users covering the necessary details regarding user authentication setup.

Implementing Role-Based Features

Implement a role-based feature where only users with a specific role can access certain functionalities.

For example, we'll create a "Tell Me a Joke" button that displays a joke, but only users with the "joker" role are allowed to view it.

Backend Logic

1. Creating the "Tell Me a Joke" API

  1. Create a backend file inside the main-features folder.

  2. Name the file as 'get-joke'.

  3. Add a joke to return.

  4. Implement role-based access control in the backend logic. Users without the required role will receive a message indicating they are not authorized to view the joke.

Refer the below snippet:

get-joke.server.tsx
import { createBackendFunction, useFunctionContext } from "@magicjs.dev/backend"

export default createBackendFunction(async function () {
    const ctx = useFunctionContext(this)

    if (ctx.isAuthenticated === false) {
        return "You have to sign in first"
    }

    if (await ctx.isCurrentUserInAnyRoles(["joker"]) === true) {
        return "What do you call a magic dog? A Labracadabrador."
    } else {
        return "You are not a joker."
    }
})
  • In the above code, ctx.isAuthenticated checks if the user is authenticated. If not, it returns a message indicating that the user needs to sign in.

  • Then, ctx.isCurrentUserInAnyRoles(["joker"]) checks if the current user has the role "joker". If true, it returns a joke message. If not, it returns a message indicating that the user is not a joker.

Invoke the API in a button.

  1. Add a button in the 'home.tsx' file and label it as Tell me a joke.

  2. Call the 'getJokeServer' in the button as shown in the below code.

home.tsx
import React from "react"
import { Button } from "antd"
import { useLogin } from "@magicjs.dev/frontend"
import loginServer from "./login.server"
import getMyNameServer from "./get-my-name.server"
import getJokeServer from "./get-joke.server"

export default function Component(props: any) {
    const { current, logout } = useLogin()
    return (
        <div>
            <h1>{`Current User: ${current?.currentUser?.name}`}</h1>

            <Button onClick={() => loginServer('John')}>
                Login
            </Button>

            <Button onClick={() => getMyNameServer().then((name) => alert(name))}>
                Call Protected Backend
            </Button>

            {
                current.isAuthenticated === true ? (
                    <Button onClick={() => logout()}>
                        Logout
                    </Button>
                ) : null
            }
            
            <div>
                <Button onClick={() => getJokeServer().then((name) => alert(name))}>
                    Tell me a Joke
                </Button>
            </div>

        </div>
    )
}
Expand for Tailwind styled code.
import React from "react"
import { Button } from "antd"
import { useLogin } from "@magicjs.dev/frontend"
import loginServer from "./login.server"
import getMyNameServer from "./get-my-name.server"
import getJokeServer from "./get-joke.server"

export default function Component(props: any) {
  const { current, logout } = useLogin()
  return (
    <div className="flex flex-col items-center justify-center h-screen">
      <h1 className="font-semi-bold text-4xl tracking-wide pb-8">
        {`Current User: ${current?.currentUser?.name}`}
      </h1>

      <div>
        <Button onClick={() => loginServer("John")} className="text-xl w-40 h-14">
          Login
        </Button>

        <Button onClick={() => getMyNameServer().then((name) => alert(name))} className="text-xl w-120 h-14">
          Call Protected Backend
        </Button>

        {
          current.isAuthenticated === true ? (
            <Button onClick={() => logout()} className="text-xl w-40 h-14">
              Logout
            </Button>
          ) : null
        }

        <div className=" flex p-2.5 justify-center">
          <Button onClick={() => getJokeServer().then((name) => alert(name))} className="text-xl w-40 h-14">
            Tell me a Joke
          </Button>
        </div>
      </div>
    </div>
  )
}
  • Tell me a Joke button calls the getJokeServer function imported from get-joke.server and alerts the returned joke when clicked.

2. Role Assignment and Removal

To enable users to add or remove their roles, we'll create two backend APIs for role assignment and removal. Users can click the "Add Role" button to assign the "joker" role to themselves and subsequently remove it using the "Remove Role" button.

  1. Create 2 backed files and name them 'add-role' and remove-role' respectively.

Refer the below codes:

add-role.server.tsx
import { createBackendFunction, useFunctionContext, utils } from "@magicjs.dev/backend"

export default createBackendFunction(async function () {
    const ctx = useFunctionContext(this)

    await utils.assignRoleToUser(ctx.currentUser._id, 'joker');

    return "Message from server"
})
remove-role.server.tsx
import { createBackendFunction, useFunctionContext, utils } from "@magicjs.dev/backend"

export default createBackendFunction(async function () {
  const ctx = useFunctionContext(this)

  await utils.unassignRoleFromUser(ctx.currentUser._id, 'joker');

  return "Message from server"
})
  1. Invoke the add-role and remove-role with respective buttons in home.tsx.

Refer the code below.

home.tsx
import React from "react"
import { Button } from "antd"
import { useLogin } from "@magicjs.dev/frontend"
import loginServer from "./login.server"
import getMyNameServer from "./get-my-name.server"
import getJokeServer from "./get-joke.server"
import addRoleServer from "./add-role.server"
import removeRoleServer from "./remove-role.server"

export default function Component(props: any) {
    const { current, logout } = useLogin()
    return (
        <div>
            <h1>{`Current User: ${current?.currentUser?.name}`}</h1>

            <Button onClick={() => loginServer('John')}>
                Login
            </Button>

            <Button onClick={() => getMyNameServer().then((name) => alert(name))}>
                Call Protected Backend
            </Button>

            {
                current.isAuthenticated === true ? (
                    <Button onClick={() => logout()}>
                        Logout
                    </Button>
                ) : null
            }
            
            <div>
                <Button onClick={() => addRoleServer().then(() => alert('Role added'))}>
                    Add Role
                </Button>
                <Button onClick={() => getJokeServer().then((name) => alert(name))}>
                    Tell me a Joke
                </Button>
                <Button onClick={() => removeRoleServer().then(() => alert('Role removed'))}>
                    Remove Role
                </Button>
            </div>
        </div>
    )
}
Expand for Tailwind styled code.
home.tsx
import React from "react"
import { Button } from "antd"
import { useLogin } from "@magicjs.dev/frontend"
import loginServer from "./login.server"
import getMyNameServer from "./get-my-name.server"
import getJokeServer from "./get-joke.server"
import addRoleServer from "./add-role.server"
import removeRoleServer from "./remove-role.server"

export default function Component(props: any) {
  const { current, logout, isCurrentUserInAnyRoles } = useLogin()
  return (
    <div className="flex flex-col items-center justify-center h-screen">
      <h1 className="font-semi-bold text-4xl tracking-wide pb-8">
        {`Current User: ${current?.currentUser?.name}`}
      </h1>

      <div>
        <Button onClick={() => loginServer("John")} className="text-xl w-40 h-14">
          Login
        </Button>

        <Button onClick={() => getMyNameServer().then((name) => alert(name))} className="text-xl w-120 h-14">
          Call Protected Backend
        </Button>

        {
          current.isAuthenticated === true ? (
            <Button onClick={() => logout()} className="text-xl w-40 h-14">
              Logout
            </Button>
          ) : null
        }

        <div className=" flex p-2.5 justify-center">
          <Button onClick={() => addRoleServer().then(() => alert('Role added'))} className="text-xl w-40 h-14">
            Add Role
          </Button>

          <Button onClick={() => getJokeServer().then((name) => alert(name))} className="text-xl w-40 h-14">
            Tell me a Joke
          </Button>

          <Button onClick={() => removeRoleServer().then(() => alert('Role removed'))} className="text-xl w-40 h-14">
            Remove Role
          </Button>
        </div>
      </div>
    </div>
  )
}
  • Add Role button calls the addRoleServer function imported from add-role.server and assigns the role 'joker' to the current user when clicked.

  • Remove Role button calls the removeRoleServer function imported from remove-role.server and unassigns the role 'joker' from the current user when clicked.

Ensuring Frontend Protection

Adding Frontend Protection ensures that frontend components and features are only accessible to users with the requisite roles.

In conjunction with backend role-based access control mechanisms, frontend role checks will be integrated using the isCurrentUserInAnyRoles function provided by MagicJS.

  1. Add function isCurrentUserInAnyRoles in the useLogin hook.

  2. Wrap the Tell me a joke button inside this function as shown in the below code.

Code:

home.tsx
import React from "react"
import { Button } from "antd"
import { useLogin } from "@magicjs.dev/frontend"
import loginServer from "./login.server"
import getMyNameServer from "./get-my-name.server"
import getJokeServer from "./get-joke.server"
import addRoleServer from "./add-role.server"
import removeRoleServer from "./remove-role.server"

export default function Component(props: any) {
    const { current, logout, isCurrentUserInAnyRoles } = useLogin()
    return (
        <div>

            <h1>{`Current User: ${current?.currentUser?.name}`}</h1>

            <Button onClick={() => loginServer('John')}>
                Login
            </Button>

            <Button onClick={() => getMyNameServer().then((name) => alert(name))}>
                Call Protected Backend
            </Button>

            {
                current.isAuthenticated === true ? (
                    <Button onClick={() => logout()}>
                        Logout
                    </Button>
                ) : null
            }
            <div>
                <Button onClick={() => addRoleServer().then(() => alert('Role added'))}>
                    Add Role
                </Button>

                {
                    isCurrentUserInAnyRoles(['joker']) === true ? (
                        <Button onClick={() => getJokeServer().then((name) => alert(name))}>
                            Tell me a Joke
                        </Button>
                    ) : (
                        null
                    )
                }

                <Button onClick={() => removeRoleServer().then(() => alert('Role removed'))}>
                    Remove Role
                </Button>
            </div>
        </div>
    )
}
Expand for Tailwind styled code.
home.tsx
import React from "react"
import { Button } from "antd"
import { useLogin } from "@magicjs.dev/frontend"
import loginServer from "./login.server"
import getMyNameServer from "./get-my-name.server"
import getJokeServer from "./get-joke.server"
import addRoleServer from "./add-role.server"
import removeRoleServer from "./remove-role.server"

export default function Component(props: any) {
  const { current, logout, isCurrentUserInAnyRoles } = useLogin()
  return (
    <div className="flex flex-col items-center justify-center h-screen">
      <h1 className="font-semi-bold text-4xl tracking-wide pb-8">
        {`Current User: ${current?.currentUser?.name}`}
      </h1>

      <div>
        <Button onClick={() => loginServer("John")} className="text-xl w-40 h-14">
          Login
        </Button>

        <Button onClick={() => getMyNameServer().then((name) => alert(name))} className="text-xl w-120 h-14">
          Call Protected Backend
        </Button>

        {
          current.isAuthenticated === true ? (
            <Button onClick={() => logout()} className="text-xl w-40 h-14">
              Logout
            </Button>
          ) : null
        }

        <div className=" flex p-2.5 justify-center">
          <Button onClick={() => addRoleServer().then(() => alert('Role added'))} className="text-xl w-40 h-14">
            Add Role
          </Button>

          {
            isCurrentUserInAnyRoles(['joker']) === true ? (
              <Button onClick={() => getJokeServer().then((name) => alert(name))} className="text-xl w-40 h-14">
                Tell me a Joke
              </Button>
            ) : (
              null
            )
          }

          <Button onClick={() => removeRoleServer().then(() => alert('Role removed'))} className="text-xl w-40 h-14">
            Remove Role
          </Button>
        </div>
      </div>
    </div>
  )
}```typescript
import React from "react"
import { Button } from "antd"
import { useLogin } from "@magicjs.dev/frontend"
import loginServer from "./login.server"
import getMyNameServer from "./get-my-name.server"
import getJokeServer from "./get-joke.server"
import addRoleServer from "./add-role.server"
import removeRoleServer from "./remove-role.server"

export default function Component(props: any) {
  const { current, logout, isCurrentUserInAnyRoles } = useLogin()
  return (
    <div className="flex flex-col items-center justify-center h-screen">
      <h1 className="font-semi-bold text-4xl tracking-wide pb-8">
        {`Current User: ${current?.currentUser?.name}`}
      </h1>

      <div>
        <Button onClick={() => loginServer("John")} className="text-xl w-40 h-14">
          Login
        </Button>

        <Button onClick={() => getMyNameServer().then((name) => alert(name))} className="text-xl w-120 h-14">
          Call Protected Backend
        </Button>

        {
          current.isAuthenticated === true ? (
            <Button onClick={() => logout()} className="text-xl w-40 h-14">
              Logout
            </Button>
          ) : null
        }

        <div className=" flex p-2.5 justify-center">
          <Button onClick={() => addRoleServer().then(() => alert('Role added'))} className="text-xl w-40 h-14">
            Add Role
          </Button>

          {
            isCurrentUserInAnyRoles(['joker']) === true ? (
              <Button onClick={() => getJokeServer().then((name) => alert(name))} className="text-xl w-40 h-14">
                Tell me a Joke
              </Button>
            ) : (
              null
            )
          }

          <Button onClick={() => removeRoleServer().then(() => alert('Role removed'))} className="text-xl w-40 h-14">
            Remove Role
          </Button>
        </div>
      </div>
    </div>
  )
}
```

Note: Please be advised that in order for the modifications to be implemented, it is necessary to refresh the page each time.

Implementing a state in the frontend file will continuously compute, eliminating the need for repetitive manual refreshing.

By leveraging user role authentication and role management in your MagicJS application, you can tailor access permissions and enhance security. Empowered with role-based access control, you can customize the user experience and ensure that users interact with features aligned with their roles.

🎉 Congratulations! You've learned how to authenticate a user role seamlessly in MERN.AI using the MagicJS framework.

Last updated