better-auth / better-auth

The most comprehensive authentication framework for TypeScript
https://better-auth.com
MIT License
15.17k stars 1.06k forks source link

`hasPermission` doesn't work client-side #2687

Closed mintydev789 closed 2 weeks ago

mintydev789 commented 1 month ago

Is this suited for github?

To Reproduce

I am following the documentation for checking if the user has a given permission, but the promise returns a 401 no matter what (I have tried logging in with an admin account, but that still results in canAccessModDashboard returning 401). This is the setup I have:

import { createAuthClient } from "better-auth/react";
import { adminClient, inferAdditionalFields, usernameClient } from "better-auth/client/plugins";
import { auth } from "~/server/auth.ts";
import { ac, admin } from "~/server/permissions.ts";

export const authClient = createAuthClient({
  plugins: [
    usernameClient(),
    adminClient({
      ac,
      roles: {
        admin,
      },
    }),
    inferAdditionalFields<typeof auth>(),
  ],
});

export const canAccessModDashboard = await authClient.admin.hasPermission({ permissions: { modDashboard: ["view"] } });

It's also possible that it works correctly, but I'm doing something wrong due to the documentation being somewhat unclear. For completeness, here is also my permissions.ts setup:

import { createAccessControl } from "better-auth/plugins/access";
import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";

const statement = {
  ...defaultStatements,
  modDashboard: ["view", "view-stats"],
  competitions: ["create", "update", "approve", "delete"],
  meetups: ["create", "update", "approve", "delete"],
  persons: ["create", "update", "approve", "delete"],
} as const;

export const ac = createAccessControl(statement);

export const admin = ac.newRole({
  ...adminAc.statements,
  modDashboard: [...statement.modDashboard],
  competitions: [...statement.competitions],
  meetups: [...statement.meetups],
  persons: [...statement.persons],
});

Current vs. Expected behavior

The function always returns a 401. It should not do so when the user has the specified permissions.

What version of Better Auth are you using?

1.2.7

Provide environment information

Arco Linux. Deno runtime.

Which area(s) are affected? (Select all that apply)

Client

Auth config (if applicable)

import "server-only";
import { betterAuth } from "better-auth";
import * as bcrypt from "bcrypt";
import { db } from "./db/provider.ts";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { nextCookies } from "better-auth/next-js";
import { admin as adminPlugin, username } from "better-auth/plugins";
import { accounts, sessions, users, verifications } from "~/server/db/schema/auth-schema.ts";
import { C } from "~/helpers/constants.ts";
import { sendResetPassword, sendVerificationCode } from "~/server/mailer.ts";
import { ac, admin } from "~/server/permissions.ts";

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
    schema: {
      user: users,
      session: sessions,
      account: accounts,
      verification: verifications,
    },
  }),
  plugins: [
    nextCookies(),
    username(),
    adminPlugin({
      ac,
      roles: {
        admin,
      },
    }),
  ],
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
    sendResetPassword: ({ user, url }) => sendResetPassword(user.email, url),
    password: {
      hash: (password: string) => bcrypt.hash(password, C.passwordSaltRounds),
      verify: (data: { hash: string; password: string }) => bcrypt.compare(data.password, data.hash),
    },
  },
  emailVerification: {
    sendVerificationEmail: ({ user, url }) => sendVerificationCode(user.email, url),
  },
  user: {
    additionalFields: {
      username: {
        type: "string",
        required: true,
      },
      personId: {
        type: "number",
        required: false,
      },
    },
    deleteUser: {
      enabled: true,
    },
  },
});

Additional context

No response

mintydev789 commented 1 month ago

I should also mention I consume the output of the function directly in my client-side React code using canAccessModDashboard.data. I end up having a hydration error (seemingly because it's actually returning correctly while React does the initial render server-side (I'm using Next JS).