Routing looks not working but currentUser actually have the role

My database schema is on I have Role table, and User table have roleId field which is reference on the Role table.

In the lib\auth.js, for getCurrentUser file I also get the role name back

...
export const getCurrentUser = async (session) => {
  if (!session || typeof session.id !== 'number') {
    throw new Error('Invalid session')
  }

  return db.user.findUnique({
    where: { id: session.id },
    select: {
      id: true,
      email: true,
      lastName: true,
      firstName: true,
      userRole: {
        select: { name: true },
      },
    },
  })
}

export const hasRole = (roles) => {
  if (!isAuthenticated()) {
    return false
  }

  const currentUserRoles = context.currentUser?.userRole.name

  if (typeof roles === 'string') {
    if (typeof currentUserRoles === 'string') {
      // roles to check is a string, currentUser.roles is a string
      return currentUserRoles === roles
    } else if (Array.isArray(currentUserRoles)) {
      // roles to check is a string, currentUser.roles is an array
      return currentUserRoles?.some((allowedRole) => roles === allowedRole)
    }
  }

  if (Array.isArray(roles)) {
    if (Array.isArray(currentUserRoles)) {
      // roles to check is an array, currentUser.roles is an array
      return currentUserRoles?.some((allowedRole) =>
        roles.includes(allowedRole)
      )
    } else if (typeof currentUserRoles === 'string') {
      // roles to check is an array, currentUser.roles is a string
      return roles.some((allowedRole) => currentUserRoles === allowedRole)
    }
  }

  // roles not found
  return false
}
....

So when I look the graphql for currentUser it also return following:

{
  "data": {
    "redwood": {
      "currentUser": {
        "id": 1,
        "email": "vincentcheng787@gmail.com",
        "lastName": "Zheng",
        "firstName": "Vincent",
        "userRole": {
          "name": "admin"
        }
      }
    }
  },
  "extensions": {}
}

But on the Routes.js, which have something like this

const Routes = () => {
  return (
    <Router useAuth={useAuth}>
      <Set wrap={AppHeaderLayout}>
        <Private unauthenticated="forbidden" roles="admin">
          <Route path="/admin" page={AdminPage} name="admin" />
        </Private>
        <Route path="/forbidden" page={ForbiddenPage} name="forbidden" />
        <Route path="/login" page={LoginPage} name="login" />
        <Route path="/signup" page={SignupPage} name="signup" />
        <Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
        <Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />

        <Route path="/about" page={AboutPage} name="about" />
        <Route path="/" page={HomePage} name="home" />
      </Set>
      <Route notfound page={NotFoundPage} />
    </Router>
  )
}

But when I try to access the admin page, it redirect me to forbidden because not admin. What I missing?

OK… Really try hard and still don’t understand how the thing got connected for the auth… if in the user table I add a column call roles and put admin in there, and also in getCurrentUser part, in the select section add roles, that works… I can view the admin page. However it is not the one I want and I don’t know what I need to modify to make it works now…

Hi @vincentz thanks for your patience here! These things can be frustrating to figure out.

Can you share your prisma.schema User and UserRoles (if applicable) models?

Also, have you tried debugging via hasRole()? (doc here) I’m suspicious it might return false if you try hasRole('admin') when you’re logged in.

For reference, I want to make sure you saw this RBAC doc.

model Role {
  id        Int       @id @default(autoincrement())
  name      String?   @db.VarChar(255)
  createdAt DateTime? @default(now()) @map("create_time") @db.Timestamp(0)
  updatedAt DateTime? @default(now()) @updatedAt @map("update_time") @db.Timestamp(0)
  User      User[]

  @@map("app_role")
}

model User {
  id                  Int       @id @default(autoincrement())
  lastName            String?   @map("last_name") @db.VarChar(255)
  firstName           String?   @map("first_name") @db.VarChar(255)
  username            String?   @db.VarChar(255)
  email               String?   @unique(map: "app_user_pk") @db.VarChar(255)
  hashedPassword      String?   @map("password") @db.VarChar(255)
  salt                String
  resetToken          String?   @map("reset_token")
  resetTokenExpiresAt DateTime? @map("reset_token_expires_at")
  createdAt           DateTime? @default(now()) @map("create_time") @db.Timestamp(0)
  updatedAt           DateTime? @default(now()) @updatedAt @map("update_time") @db.Timestamp(0)
  roles               String    @map("roles")
  roleId              Int       @default(3) @map("role_id")
  userRole            Role      @relation(fields: [roleId], references: [id])

  @@map("app_user")
}

I try to debug hasRole using IntelliJ, but it looks like didn’t hit there…

And here is the table sturcture I have for the user/role part
image

Play around a little bit and it looks like I still don’t understand how the thing got connected…
Here is the change I made right now
in api/src/lib/auth.js, my hasRole function was looks like following:

export const hasRole = (roles) => {
  if (!isAuthenticated()) {
    return false
  }

  const currentUserRoles = context.currentUser?.userRole['name']

  if (typeof roles === 'string') {
    if (typeof currentUserRoles === 'string') {
      // roles to check is a string, currentUser.roles is a string
      return currentUserRoles === roles
    } else if (Array.isArray(currentUserRoles)) {
      // roles to check is a string, currentUser.roles is an array
      return currentUserRoles?.some((allowedRole) => roles === allowedRole)
    }
  }

  if (Array.isArray(roles)) {
    if (Array.isArray(currentUserRoles)) {
      // roles to check is an array, currentUser.roles is an array
      return currentUserRoles?.some((allowedRole) =>
        roles.includes(allowedRole)
      )
    } else if (typeof currentUserRoles === 'string') {
      // roles to check is an array, currentUser.roles is a string
      return roles.some((allowedRole) => currentUserRoles === allowedRole)
    }
  }

  // roles not found
  return false
}

And then in web\src\Routes.js file I change to following…

// In this file, all Page components from 'src/pages` are auto-imported. Nested
// directories are supported, and should be uppercase. Each subdirectory will be
// prepended onto the component name.
//
// Examples:
//
// 'src/pages/HomePage/HomePage.js'         -> HomePage
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage

import { Router, Route, Set, Private } from '@redwoodjs/router'

import AppHeaderLayout from 'src/layouts/AppHeaderLayout'

import { useAuth } from './auth'

const Routes = () => {
  return (
    <Router useAuth={useAuth}>
      <Set wrap={AppHeaderLayout}>
        <Private unauthenticated="forbidden" hasRole={'ADMIN'}>
          <Route path="/admin" page={AdminPage} name="admin" />
        </Private>
        <Route path="/forbidden" page={ForbiddenPage} name="forbidden" />
        <Route path="/login" page={LoginPage} name="login" />
        <Route path="/signup" page={SignupPage} name="signup" />
        <Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
        <Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />
        <Route path="/about" page={AboutPage} name="about" />
        <Route path="/" page={HomePage} name="home" />
      </Set>
      <Route notfound page={NotFoundPage} />
    </Router>
  )
}

export default Routes

Now the problem becomes… everyone can view the admin page now… either my currentUser was change from ADMIN to USER…

OK… just check the Private tag again and I realized there is no hasRole properties… that probably why everyone can view the page… back to beginning…

And also because this issue… I also want to know is it a good practice to seperate the User and Role Table or just adding the column said… roles in User table… it looks like the second way redwood more prefer and not that complicated?

Hi @vincentz without being able to spend time on this, it looks like the way you set your DB models up (role to many users) is conflicting with the logic for hasRole (in api/src/lib/auth.js)

My recommendation is to follow along in this document (maybe starting a new project just to learn), using the example code as reference:

Once you get that working, I think you’ll have a better understanding about how to get your project working.

Lastly, if you’re jumping straight in RBAC, there are a lot of fundamentals you’re building on. Is it possible you can think about this incrementally? E.g. just use isAuthenticated for private content at the start of your project. Then, as you continue to develop and add, incrementally add RBAC. Just an idea if helpful.

Do keep going! You’re learning a lot.

yup will do, try to make the thing simple now

1 Like