Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

403 forbidden even when user is allowed #344

Open
YeudaBy opened this issue Feb 13, 2024 · 8 comments
Open

403 forbidden even when user is allowed #344

YeudaBy opened this issue Feb 13, 2024 · 8 comments

Comments

@YeudaBy
Copy link

YeudaBy commented Feb 13, 2024

Describe the bug
Hey!
i'm trying to edit something, but even when the user is clearly are allowed, it gives me 403 error, here is the code example:

const nu = {
            name,
            active,
            district,
            roles: userRoles
        }
console.log("allowed", usersRepo.metadata.apiUpdateAllowed(nu))
const newUser = await usersRepo.update(_user.id, nu)

here the entity declarition:

export const AdminRoles = [UserRole.Admin, UserRole.SuperAdmin]

@Entity("users", {
    allowApiRead: true,
    allowApiUpdate: () => !!remult.user?.roles?.length && AdminRoles.includes(remult.user.roles[0] as UserRole),
    allowApiDelete: () => !!remult.user?.roles?.length && AdminRoles.includes(remult.user.roles[0] as UserRole),
    allowApiInsert: () => !!remult.user?.roles?.length && AdminRoles.includes(remult.user.roles[0] as UserRole),
})
export class User extends IdEntity {

    @Fields.string()
    name!: string;

    @Fields.string()
    email!: string;

    @Fields.boolean()
    active: boolean = true;

    @Fields.createdAt()
    createdAt!: Date;

    @Fields.object()
    district?: District;

    @Fields.object()
    roles: UserRole = UserRole.Dispatcher;

    get userInfo(): UserInfo {
        return {
            name: this.email,
            id: this.id,
            roles: [this.roles],
        }
    }

    get isAdmin() {
        return AdminRoles.includes(this.roles)
    }
}

and here a screenshot:
image

i don't know if it's a bug or maybe i just do not understand how to do that...

To Reproduce
Steps to reproduce the behavior:

  1. Go to '...'
  2. Click on '....'
  3. Scroll down to '....'
  4. See error

Expected behavior
A clear and concise description of what you expected to happen.

Screenshots
If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):

  • OS: [e.g. iOS]
  • Browser [e.g. chrome, safari]
  • Version [e.g. 22]

Smartphone (please complete the following information):

  • Device: [e.g. iPhone6]
  • OS: [e.g. iOS8.1]
  • Browser [e.g. stock browser, safari]
  • Version [e.g. 22]

Additional context
Add any other context about the problem here.

@noam-honig
Copy link
Collaborator

Hi,

If I were you, I would add a console.log in the allowApiUpdate and see it's result on the terminal that displays messages from the server.

Something like:

    allowApiUpdate: () =>{
    const result =  !!remult.user?.roles?.length && AdminRoles.includes(remult.user.roles[0] as UserRole),
console.log({result})
    return result

This will help you see the result of your expression, you'll also be able to add other peices of information that can help you investigate.

Also please make sure that the getUser option for remultExpress is correctly configured and working - if the user is not set, the backend will give a forbidden.

One last thing, you can simplify your code by saying:

allowApiUpdate: ()=> remult.isAllowed(AdminRoles)

Or even shorter:

allowApiUpdate:AdminRoles

@YeudaBy
Copy link
Author

YeudaBy commented Feb 13, 2024

You where right, getUser returns null :(
Thank you!

there's a chance that you've some example of how to fetch the userInfo from the db in next js (pages version)?

@noam-honig
Copy link
Collaborator

I'll prepare something for tomorrow morning

@noam-honig
Copy link
Collaborator

Hi @YeudaBy,

Here's how to do that:

  1. Add a User entity, with a few helper functions
import { Entity, Fields, Remult, UserInfo } from 'remult'

@Entity('users', {
  allowApiCrud: true,
})
export class User {
  @Fields.cuid()
  id = ''
  @Fields.string()
  name = ''
  @Fields.boolean()
  admin = false
}

// Signs in a user by its name (you can add password etc...)
export async function signIn(remult: Remult, name: string) {
  const user = await remult.repo(User).findFirst({ name })
  if (user) return buildUserInfo(user)
}

// Finds a user by its id
export async function findUserById(remult: Remult, id: string) {
  const user = await remult.repo(User).findFirst({ id })
  if (user) return buildUserInfo(user)
}
// Translates a User entity to a UserInfo object
function buildUserInfo(u: User): UserInfo {
  return { id: u.id, name: u.name, roles: u.admin ? ['admin'] : [] }
}

Adjust the [...nextauth].ts file to use these methods:

import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import api from '../[...remult]'
import { signIn, findUserById } from '../../../shared/user'

export default NextAuth({
  providers: [
    Credentials({
      credentials: {
        name: {
          placeholder: 'Try Steve or Jane',
        },
      },
      authorize: async (info) => {
        if (!info?.name) return null

        // This route doesn't pass through `remultNext` so we need to get the remult instance manually
        const remult = await api.getRemult({} as any)
        return (await signIn(remult, info.name)) || null
      },
    }),
  ],
  callbacks: {
    //  this callback is called by the `useSession` hook in the frontend code
    session: async ({ session, token }) => {
      if (!token?.sub) return session
      // This route doesn't pass through `remultNext` so we need to get the remult instance manually
      const remult = await api.getRemult({} as any)
      return {
        ...session,
        user: await findUserById(remult, token.sub),
      }
    },
  },
})

Finally, adjust the [...remult].ts file accordingly

import { remultNext } from 'remult/remult-next'
import { getToken } from 'next-auth/jwt'
import { Task } from '../../shared/task'
import { TasksController } from '../../shared/tasksController'
import { User, findUserById } from '../../shared/user'
import { remult } from 'remult'

const api = remultNext({
  entities: [Task, User],
  controllers: [TasksController],
  getUser: async (req) => {
    const jwtToken = await getToken({ req })
    if (!jwtToken?.sub) return undefined
    return findUserById(remult, jwtToken.sub)
  },
  admin: true,
})
export default api

Check it out and let me know if it works for you or if you have any questions

@noam-honig
Copy link
Collaborator

noam-honig commented Feb 14, 2024

If you're feeling experimental, There's an easier way of writing that, in the next version that'll release, let me know if you want an example

@YeudaBy
Copy link
Author

YeudaBy commented Feb 14, 2024

OFC!! i still facing some errors but i think this is more relateable to next-auth and not remult.

@noam-honig
Copy link
Collaborator

Cool - in the current exp version (npm i remult@exp) we've added a withRemult method, that allows you to use remult in non remult code implicitly, without having to use getRemult and sending it as a parameter from one place to the other.

Here's how that code would look - the sign in and find user functions would not require a remult parameter

export async function signIn(name: string) {
  const user = await repo(User).findFirst({ name })
  if (user) return buildUserInfo(user)
}

// Finds a user by its id
export async function findUserById(id: string) {
  const user = await repo(User).findFirst({ id })
  if (user) return buildUserInfo(user)
}

And in [...nextauth] you won't use getRemult, instead you'll use withRemult to enter that state where the implicit remult is available.

import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import api from '../[...remult]'
import { signIn, findUserById } from '../../../shared/user'

export default NextAuth({
  providers: [
    Credentials({
      credentials: {
        name: {
          placeholder: 'Try Steve or Jane',
        },
      },
      authorize: async (info) =>
        // This route doesn't pass through `remultNext` so we need to get the remult instance manually
        api.withRemult(undefined, async () => {
          {
            if (!info?.name) return null
            return (await signIn(info.name)) || null
          }
        }),
    }),
  ],
  callbacks: {
    //  this callback is called by the `useSession` hook in the frontend code
    session: async ({ session, token }) =>
      // This route doesn't pass through `remultNext` so we need to get the remult instance manually
      api.withRemult(undefined, async () => {
        if (!token?.sub) return session
        return {
          ...session,
          user: await findUserById(token.sub),
        }
      }),
  },
})

Try it out and let me know what you think

@noam-honig
Copy link
Collaborator

Checkout this video - I think you'll like it
https://www.youtube.com/watch?v=9lWQwAUcKEM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants