diff --git a/docs/content/docs/plugins/admin.mdx b/docs/content/docs/plugins/admin.mdx
index 7a504d9b5c..a38580059c 100644
--- a/docs/content/docs/plugins/admin.mdx
+++ b/docs/content/docs/plugins/admin.mdx
@@ -79,7 +79,7 @@ const newUser = await authClient.admin.createUser({
name: "Test User",
email: "test@example.com",
password: "password123",
- role: "user",
+ role: "user", // this can also be an array for multiple roles (e.g. ["user", "sale"])
data: {
// any additional on the user table including plugin fields and custom fields
customField: "customValue",
@@ -176,7 +176,7 @@ Changes the role of a user.
```ts title="admin.ts"
const updatedUser = await authClient.admin.setRole({
userId: "user_id_here",
- role: "admin",
+ role: "admin", // this can also be an array for multiple roles (e.g. ["admin", "sale"])
});
```
@@ -272,6 +272,10 @@ By default, there are two roles:
`user`: Users with the user role have no control over other users.
+
+ A user can have multiple roles. Multiple roles are stored as string separated by comma (",").
+
+
### Permissions
By default, there are two resources with up to six permissions.
diff --git a/packages/better-auth/src/plugins/admin/admin.test.ts b/packages/better-auth/src/plugins/admin/admin.test.ts
index dd2b58b72a..e7acb749c2 100644
--- a/packages/better-auth/src/plugins/admin/admin.test.ts
+++ b/packages/better-auth/src/plugins/admin/admin.test.ts
@@ -79,6 +79,29 @@ describe("Admin plugin", async () => {
expect(newUser?.role).toBe("user");
});
+ it("should allow admin to create user with multiple roles", async () => {
+ const res = await client.admin.createUser(
+ {
+ name: "Test User mr",
+ email: "testmr@test.com",
+ password: "test",
+ role: ["user", "admin"],
+ },
+ {
+ headers: adminHeaders,
+ },
+ );
+ expect(res.data?.user.role).toBe("user,admin");
+ await client.admin.removeUser(
+ {
+ userId: res.data?.user.id || "",
+ },
+ {
+ headers: adminHeaders,
+ },
+ );
+ });
+
it("should not allow non-admin to create users", async () => {
const res = await client.admin.createUser(
{
@@ -211,6 +234,39 @@ describe("Admin plugin", async () => {
expect(res.data?.user?.role).toBe("admin");
});
+ it("should allow to set multiple user roles", async () => {
+ const createdUser = await client.admin.createUser(
+ {
+ name: "Test User mr",
+ email: "testmr@test.com",
+ password: "test",
+ role: "user",
+ },
+ {
+ headers: adminHeaders,
+ },
+ );
+ expect(createdUser.data?.user.role).toBe("user");
+ const res = await client.admin.setRole(
+ {
+ userId: createdUser.data?.user.id || "",
+ role: ["user", "admin"],
+ },
+ {
+ headers: adminHeaders,
+ },
+ );
+ expect(res.data?.user?.role).toBe("user,admin");
+ await client.admin.removeUser(
+ {
+ userId: createdUser.data?.user.id || "",
+ },
+ {
+ headers: adminHeaders,
+ },
+ );
+ });
+
it("should not allow non-admin to set user role", async () => {
const res = await client.admin.setRole(
{
diff --git a/packages/better-auth/src/plugins/admin/admin.ts b/packages/better-auth/src/plugins/admin/admin.ts
index 940bcca4d0..7a186091fd 100644
--- a/packages/better-auth/src/plugins/admin/admin.ts
+++ b/packages/better-auth/src/plugins/admin/admin.ts
@@ -96,6 +96,15 @@ export interface AdminOptions {
bannedUserMessage?: string;
}
+export type InferAdminRolesFromOption =
+ O extends { roles: Record }
+ ? keyof O["roles"]
+ : "user" | "admin";
+
+function parseRoles(roles: string | string[]): string {
+ return Array.isArray(roles) ? roles.join(",") : roles;
+}
+
export const admin = (options?: O) => {
const opts = {
defaultRole: "user",
@@ -205,9 +214,16 @@ export const admin = (options?: O) => {
userId: z.coerce.string({
description: "The user id",
}),
- role: z.string({
- description: "The role to set. `admin` or `user` by default",
- }),
+ role: z.union([
+ z.string({
+ description: "The role to set. `admin` or `user` by default",
+ }),
+ z.array(
+ z.string({
+ description: "The roles to set. `admin` or `user` by default",
+ }),
+ ),
+ ]),
}),
use: [adminMiddleware],
metadata: {
@@ -233,6 +249,14 @@ export const admin = (options?: O) => {
},
},
},
+ $Infer: {
+ body: {} as {
+ userId: string;
+ role:
+ | InferAdminRolesFromOption
+ | InferAdminRolesFromOption[];
+ },
+ },
},
},
async (ctx) => {
@@ -254,7 +278,7 @@ export const admin = (options?: O) => {
const updatedUser = await ctx.context.internalAdapter.updateUser(
ctx.body.userId,
{
- role: ctx.body.role,
+ role: parseRoles(ctx.body.role),
},
ctx,
);
@@ -278,9 +302,16 @@ export const admin = (options?: O) => {
description: "The name of the user",
}),
role: z
- .string({
- description: "The role of the user",
- })
+ .union([
+ z.string({
+ description: "The role of the user",
+ }),
+ z.array(
+ z.string({
+ description: "The roles of user",
+ }),
+ ),
+ ])
.optional(),
/**
* extra fields for user
@@ -315,6 +346,17 @@ export const admin = (options?: O) => {
},
},
},
+ $Infer: {
+ body: {} as {
+ email: string;
+ password: string;
+ name: string;
+ role?:
+ | InferAdminRolesFromOption
+ | InferAdminRolesFromOption[];
+ data?: Record;
+ },
+ },
},
},
async (ctx) => {
@@ -349,7 +391,10 @@ export const admin = (options?: O) => {
await ctx.context.internalAdapter.createUser({
email: ctx.body.email,
name: ctx.body.name,
- role: ctx.body.role ?? options?.defaultRole ?? "user",
+ role:
+ (ctx.body.role && parseRoles(ctx.body.role)) ??
+ options?.defaultRole ??
+ "user",
...ctx.body.data,
});
@@ -1206,7 +1251,7 @@ export const admin = (options?: O) => {
[key in keyof Statements]?: Array;
};
userId?: string;
- role?: string;
+ role?: InferAdminRolesFromOption;
},
},
},
diff --git a/packages/better-auth/src/plugins/admin/client.ts b/packages/better-auth/src/plugins/admin/client.ts
index 90d9fb0af3..f300969c66 100644
--- a/packages/better-auth/src/plugins/admin/client.ts
+++ b/packages/better-auth/src/plugins/admin/client.ts
@@ -5,8 +5,8 @@ import { adminAc, defaultStatements, userAc } from "./access";
import type { admin } from "./admin";
interface AdminClientOptions {
- ac: AccessControl;
- roles: {
+ ac?: AccessControl;
+ roles?: {
[key in string]: Role;
};
}
@@ -23,7 +23,7 @@ export const adminClient = (options?: O) => {
};
return {
- id: "better-auth-client",
+ id: "admin-client",
$InferServerPlugin: {} as ReturnType<
typeof admin<{
ac: O["ac"] extends AccessControl