Custom Permissions Not Working

Hey there Since 2 updates ago i seem to get a type-error on custom permissions i wonder if anything has changed without updating the docs? Thanks in advance
No description
No description
No description
15 Replies
bekacru
bekacru2mo ago
can you share your whole auth config? also your tsconfig
Dr.Pepper
Dr.PepperOP2mo ago
Hi i certainly can the auth config is rather large though i got some cleaning up todo: Auth.ts (part 1):
import { betterAuth } from "better-auth";
import { organization, twoFactor } from "better-auth/plugins";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
import { sendMail } from "./send-mail";
import { render } from "@react-email/components";
import { reactInvitationEmail } from "./email/invitation-email";
import { ac, member, admin, machinist, conducteur, ploegbaas, truckdriver, groundworker, paver } from "./permissions/permissions";
import { reactResetPasswordEmail } from "./email/resetpassword-email";
import { reactOtpEmail } from "./email/otp-email";
import { reactEmailVerification } from "./email/verification-email";

const prisma = new PrismaClient();

export const auth = betterAuth({
appName: "redacted",
trustedOrigins: ["http://localhost:3000", "https://www.redacted.be", "https://redacted.be"],
database: prismaAdapter(prisma, {
provider: "mysql",
}),
advanced: {
cookiePrefix: "parapluu",
crossSubDomainCookies: {
enabled: true
}
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
async sendResetPassword({ user, url }) {
try {
await sendMail({
sendTo: user.email,
subject: "Wachtwoord reset",
text: "",
html: await render(
reactResetPasswordEmail({
username: user.name,
resetLink: url
})
)
});
} catch (error) {
console.error("Error sending reset password email:", error);
}
},
},
import { betterAuth } from "better-auth";
import { organization, twoFactor } from "better-auth/plugins";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
import { sendMail } from "./send-mail";
import { render } from "@react-email/components";
import { reactInvitationEmail } from "./email/invitation-email";
import { ac, member, admin, machinist, conducteur, ploegbaas, truckdriver, groundworker, paver } from "./permissions/permissions";
import { reactResetPasswordEmail } from "./email/resetpassword-email";
import { reactOtpEmail } from "./email/otp-email";
import { reactEmailVerification } from "./email/verification-email";

const prisma = new PrismaClient();

export const auth = betterAuth({
appName: "redacted",
trustedOrigins: ["http://localhost:3000", "https://www.redacted.be", "https://redacted.be"],
database: prismaAdapter(prisma, {
provider: "mysql",
}),
advanced: {
cookiePrefix: "parapluu",
crossSubDomainCookies: {
enabled: true
}
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
async sendResetPassword({ user, url }) {
try {
await sendMail({
sendTo: user.email,
subject: "Wachtwoord reset",
text: "",
html: await render(
reactResetPasswordEmail({
username: user.name,
resetLink: url
})
)
});
} catch (error) {
console.error("Error sending reset password email:", error);
}
},
},
emailVerification: {
autoSignInAfterVerification: true,
sendVerificationEmail: async ( { user, url } ) => {
try {
const verLink = url.replace(/callbackURL=\/$/, "callbackURL=/login");

await sendMail({
sendTo: user.email,
subject: "Account verificatie",
text: "",
html: await render(
reactEmailVerification({
username: user.name,
verificationLink: verLink
})
)
});
console.log("Verification email sent successfully.");
} catch (error) {
console.error("Error sending verification email:", error);
}
}
},
plugins: [
twoFactor({
otpOptions: {
async sendOTP({ user, otp }) {
try {
await sendMail({
sendTo: user.email,
subject: "OTP Code",
text: "",
html: await render(
reactOtpEmail({
username: user.name,
otpCode: otp
})
)
});
} catch (error) {
console.error("Error sending OTP email:", error);
}
}
}
}),
organization({
ac: ac,
roles: {
admin,
member,
machinist,
conducteur,
ploegbaas,
truckdriver,
groundworker,
paver
},
async sendInvitationEmail(data) {
try {
const inviteLink = `https://redacted.be/accept-invitation/${data.id}`;
await sendMail({
sendTo: data.email,
subject: `Invitatie redacted${data.organization.name}`,
text: "",
html: await render(
reactInvitationEmail({
username: data.email,
invitedByUsername: data.inviter.user.name,
invitedByEmail: data.inviter.user.email,
teamName: data.organization.name,
inviteLink: inviteLink,
teamImage: data.organization.logo || "",
})
)
});
} catch (error) {
console.error("Error sending invitation email:", error);
}
},
})
]
});
emailVerification: {
autoSignInAfterVerification: true,
sendVerificationEmail: async ( { user, url } ) => {
try {
const verLink = url.replace(/callbackURL=\/$/, "callbackURL=/login");

await sendMail({
sendTo: user.email,
subject: "Account verificatie",
text: "",
html: await render(
reactEmailVerification({
username: user.name,
verificationLink: verLink
})
)
});
console.log("Verification email sent successfully.");
} catch (error) {
console.error("Error sending verification email:", error);
}
}
},
plugins: [
twoFactor({
otpOptions: {
async sendOTP({ user, otp }) {
try {
await sendMail({
sendTo: user.email,
subject: "OTP Code",
text: "",
html: await render(
reactOtpEmail({
username: user.name,
otpCode: otp
})
)
});
} catch (error) {
console.error("Error sending OTP email:", error);
}
}
}
}),
organization({
ac: ac,
roles: {
admin,
member,
machinist,
conducteur,
ploegbaas,
truckdriver,
groundworker,
paver
},
async sendInvitationEmail(data) {
try {
const inviteLink = `https://redacted.be/accept-invitation/${data.id}`;
await sendMail({
sendTo: data.email,
subject: `Invitatie redacted${data.organization.name}`,
text: "",
html: await render(
reactInvitationEmail({
username: data.email,
invitedByUsername: data.inviter.user.name,
invitedByEmail: data.inviter.user.email,
teamName: data.organization.name,
inviteLink: inviteLink,
teamImage: data.organization.logo || "",
})
)
});
} catch (error) {
console.error("Error sending invitation email:", error);
}
},
})
]
});
tsConfig:
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
permissions.ts:
import { createAccessControl } from "better-auth/plugins/access";

const statement = {
crewInvite: ["view"],
crew: ["create", "update", "edit", "delete", "view"],
crewInventory: ["view"],
invitePerms: ["create"]
} as const;


export const ac = createAccessControl(statement);

export const member = ac.newRole({});


export const admin = ac.newRole({
crewInvite: ["view"],
crew: ["create", "update", "edit", "delete", "view"],
crewInventory: ["view"],
invitePerms: ["create"]
});


/* Infra Roles */
export const machinist = ac.newRole({
crewInvite: ["view"],
crew: ["view"],
crewInventory: ["view"],
});

export const conducteur = ac.newRole({
crewInventory: ["view"],
});

export const ploegbaas = ac.newRole({
crewInventory: ["view"],
});

export const truckdriver = ac.newRole({
crewInventory: ["view"],
});

export const groundworker = ac.newRole({
crewInventory: ["view"],
});

export const paver = ac.newRole({
crewInventory: ["view"],
});


/* End infra roles */
import { createAccessControl } from "better-auth/plugins/access";

const statement = {
crewInvite: ["view"],
crew: ["create", "update", "edit", "delete", "view"],
crewInventory: ["view"],
invitePerms: ["create"]
} as const;


export const ac = createAccessControl(statement);

export const member = ac.newRole({});


export const admin = ac.newRole({
crewInvite: ["view"],
crew: ["create", "update", "edit", "delete", "view"],
crewInventory: ["view"],
invitePerms: ["create"]
});


/* Infra Roles */
export const machinist = ac.newRole({
crewInvite: ["view"],
crew: ["view"],
crewInventory: ["view"],
});

export const conducteur = ac.newRole({
crewInventory: ["view"],
});

export const ploegbaas = ac.newRole({
crewInventory: ["view"],
});

export const truckdriver = ac.newRole({
crewInventory: ["view"],
});

export const groundworker = ac.newRole({
crewInventory: ["view"],
});

export const paver = ac.newRole({
crewInventory: ["view"],
});


/* End infra roles */
bekacru
bekacru2mo ago
could you try to add "baseUrl": "." in your tsconfig?
Dr.Pepper
Dr.PepperOP2mo ago
This did not solve it i reran typescript check and got 11 error's it used to work before so thats why i am kinda like huh:
node_modules/better-auth/dist/shared/better-auth.Dxn5Xplu.d.ts:4867:21
4867 role: InferRolesFromOption<O>;
~~~~
The expected type comes from property 'role' which is declared here on type 'Prettify<{ email: string; role: "admin" | "member" | "owner"; organizationId?: string | undefined; resend?: boolean | undefined; } & { fetchOptions?: { cache?: RequestCache | undefined; ... 34 more ...; disableValidation?: boolean | undefined; } | undefined; }>'

node_modules/better-auth/dist/shared/better-auth.Dxn5Xplu.d.ts:6555:21
6555 permission: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
~~~~~~~~~~
The expected type comes from property 'permission' which is declared here on type 'Prettify<{ permission: { readonly organization?: ("update" | "delete")[] | undefined; readonly member?: ("create" | "update" | "delete")[] | undefined; readonly invitation?: ("create" | "cancel")[] | undefined; readonly team?: ("create" | ... 1 more ... | "delete")[] | undefined; }; organizationId?: string | undefin...'

node_modules/better-auth/dist/plugins/access/index.d.ts:39:5
39 statements: TStatements;
~~~~~~~~~~
'statements' is declared here.

lib/auth.ts:103:17 - error TS2741: Property 'authorize' is missing in type 'Role<{ readonly crewInvite: readonly ["view"]; readonly crew: readonly ["create", "update", "edit", "delete", "view"]; readonly crewInventory: readonly ["view"]; readonly invitePerms: readonly ["create"]; }>' but required in type 'Role<any>'.

103 paver

node_modules/better-auth/dist/plugins/access/index.d.ts:13:5
13 authorize: (request: any, connector?: "OR" | "AND") => AuthortizeResponse;
~~~~~~~~~
'authorize' is declared here.
node_modules/better-auth/dist/shared/better-auth.Dxn5Xplu.d.ts:4867:21
4867 role: InferRolesFromOption<O>;
~~~~
The expected type comes from property 'role' which is declared here on type 'Prettify<{ email: string; role: "admin" | "member" | "owner"; organizationId?: string | undefined; resend?: boolean | undefined; } & { fetchOptions?: { cache?: RequestCache | undefined; ... 34 more ...; disableValidation?: boolean | undefined; } | undefined; }>'

node_modules/better-auth/dist/shared/better-auth.Dxn5Xplu.d.ts:6555:21
6555 permission: { [key in keyof (O["ac"] extends AccessControl<infer S extends Statements> ? S : {
~~~~~~~~~~
The expected type comes from property 'permission' which is declared here on type 'Prettify<{ permission: { readonly organization?: ("update" | "delete")[] | undefined; readonly member?: ("create" | "update" | "delete")[] | undefined; readonly invitation?: ("create" | "cancel")[] | undefined; readonly team?: ("create" | ... 1 more ... | "delete")[] | undefined; }; organizationId?: string | undefin...'

node_modules/better-auth/dist/plugins/access/index.d.ts:39:5
39 statements: TStatements;
~~~~~~~~~~
'statements' is declared here.

lib/auth.ts:103:17 - error TS2741: Property 'authorize' is missing in type 'Role<{ readonly crewInvite: readonly ["view"]; readonly crew: readonly ["create", "update", "edit", "delete", "view"]; readonly crewInventory: readonly ["view"]; readonly invitePerms: readonly ["create"]; }>' but required in type 'Role<any>'.

103 paver

node_modules/better-auth/dist/plugins/access/index.d.ts:13:5
13 authorize: (request: any, connector?: "OR" | "AND") => AuthortizeResponse;
~~~~~~~~~
'authorize' is declared here.
It seems like the role's are not getting set it's like this:
roles: {
admin,
member,
machinist,
conducteur,
ploegbaas,
truckdriver,
groundworker,
paver
},
roles: {
admin,
member,
machinist,
conducteur,
ploegbaas,
truckdriver,
groundworker,
paver
},
now requires something like
admin: {
statements: statements,
authorize(request, connector) {

},
}
admin: {
statements: statements,
authorize(request, connector) {

},
}
Dr.Pepper
Dr.PepperOP2mo ago
as you can see that whole part is red
No description
bekacru
bekacru2mo ago
It could be because you have 2 versions of better auth. The new release adds new interface to roles and that's what causing the type mismatch. Check your better-auth versions if you have multiple, remove node_modules and re-install.
Dr.Pepper
Dr.PepperOP2mo ago
Alright let me try that That was indeed the issue for some reason it reffered to a global installed better-auth even though i don't remember globally installing it thanks! However the custom permission still seems to error after loading the standard (invitation,team) etc works but my crewInventory still gives:
Object literal may only specify known properties, and 'crewInventory' does not exist in type '{ readonly organization?: ("update" | "delete")[] | undefined; readonly member?: ("update" | "delete" | "create")[] | undefined; readonly invitation?: ("create" | "cancel")[] | undefined; readonly team?: ("update" | ... 1 more ... | "create")[] | undefined; }'.ts(2353)
better-auth.Dxn5Xplu.d.ts(6555, 21): The expected type comes from property 'permission' which is declared here on type 'Prettify<{ permission: { readonly organization?: ("update" | "delete")[] | undefined; readonly member?: ("update" | "delete" | "create")[] | undefined; readonly invitation?: ("create" | "cancel")[] | undefined; readonly team?: ("update" | ... 1 more ... | "create")[] | undefined; }; organizationId?: string | undefin...'
Object literal may only specify known properties, and 'crewInventory' does not exist in type '{ readonly organization?: ("update" | "delete")[] | undefined; readonly member?: ("update" | "delete" | "create")[] | undefined; readonly invitation?: ("create" | "cancel")[] | undefined; readonly team?: ("update" | ... 1 more ... | "create")[] | undefined; }'.ts(2353)
better-auth.Dxn5Xplu.d.ts(6555, 21): The expected type comes from property 'permission' which is declared here on type 'Prettify<{ permission: { readonly organization?: ("update" | "delete")[] | undefined; readonly member?: ("update" | "delete" | "create")[] | undefined; readonly invitation?: ("create" | "cancel")[] | undefined; readonly team?: ("update" | ... 1 more ... | "create")[] | undefined; }; organizationId?: string | undefin...'
├── @heroui/[email protected]
├── @heroui/[email protected]
├── @heroui/[email protected]
├── @iconify/[email protected]
├── @prisma/[email protected]
├── @radix-ui/[email protected]
├── @radix-ui/[email protected]
├── @radix-ui/[email protected]
├── @radix-ui/[email protected]
├── @react-email/[email protected]
├── @reactuses/[email protected]
├── @tailwindcss/[email protected]
├── @tanstack/[email protected]
├── @tanstack/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @vercel/[email protected]
├── @heroui/[email protected]
├── @heroui/[email protected]
├── @heroui/[email protected]
├── @iconify/[email protected]
├── @prisma/[email protected]
├── @radix-ui/[email protected]
├── @radix-ui/[email protected]
├── @radix-ui/[email protected]
├── @radix-ui/[email protected]
├── @react-email/[email protected]
├── @reactuses/[email protected]
├── @tailwindcss/[email protected]
├── @tanstack/[email protected]
├── @tanstack/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @vercel/[email protected]
this is the list of packages Also it's not only local its local and on the production server Ima try to revert to a older version and see if it works Reverting to 1.1.19 fixed it from 1.2.0 and up it does not work
bekacru
bekacru2mo ago
when does this error happens? on auth.api.hasPermission or on the client?
Dr.Pepper
Dr.PepperOP2mo ago
on the client so:
authClient.organization.hasPermission
authClient.organization.hasPermission
Dr.Pepper
Dr.PepperOP2mo ago
It might have todo with the multiple role support you added?
No description
Dr.Pepper
Dr.PepperOP2mo ago
Since it seems you now have to define your role idk its alot of changes to look over
No description
bekacru
bekacru2mo ago
will be fixed on the next release. thanks for the collaboration :))
Dr.Pepper
Dr.PepperOP2mo ago
Thanks, np thank you for all the amazing work better-auth really changed the development speed by alot! especially for someone working alone on a huge project.
bekacru
bekacru2mo ago
should be fixed on 1.2.4-beta.3
Dr.Pepper
Dr.PepperOP2mo ago
Correct this issue is fixed, you might also want to take a look into authClient.organization.inviteMember Type 'string' is not assignable to type '"member" | "admin" | "owner"'.ts(2322) it does not allow for a custom role at:
type InferRolesFromOption<O extends OrganizationOptions | undefined> = O extends {
roles: any;
} ? keyof O["roles"] : "admin" | "member" | "owner";
type InferRolesFromOption<O extends OrganizationOptions | undefined> = O extends {
roles: any;
} ? keyof O["roles"] : "admin" | "member" | "owner";
Nvm the hasPermission still doesn't work however the issue's in the auth.ts config have been fixed For everyone wondering about the issue from authClient.organization.inviteMember the temp fix is todo something like:
await authClient.organization.hasPermission({
permission: { crewInvite: ["view"] } as any,
});
await authClient.organization.hasPermission({
permission: { crewInvite: ["view"] } as any,
});

Did you find this page helpful?