mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-29 10:26:49 -05:00
feat(admin): Add support for passing multiple roles as array (#1907)
* feat: add support for passing multiple roles as array * docs(admin): update docs * fix(admin): Fix admin role types not referenced correctly when custom roles are not passed * test(admin): add test for multiple roles * fix(admin): Fix duplicate exports of InferRolesFromOption and parseRoles from admin and organization plugin
This commit is contained in:
@@ -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.
|
||||
|
||||
<Callout>
|
||||
A user can have multiple roles. Multiple roles are stored as string separated by comma (",").
|
||||
</Callout>
|
||||
|
||||
### Permissions
|
||||
|
||||
By default, there are two resources with up to six permissions.
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -96,6 +96,15 @@ export interface AdminOptions {
|
||||
bannedUserMessage?: string;
|
||||
}
|
||||
|
||||
export type InferAdminRolesFromOption<O extends AdminOptions | undefined> =
|
||||
O extends { roles: Record<string, unknown> }
|
||||
? keyof O["roles"]
|
||||
: "user" | "admin";
|
||||
|
||||
function parseRoles(roles: string | string[]): string {
|
||||
return Array.isArray(roles) ? roles.join(",") : roles;
|
||||
}
|
||||
|
||||
export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
const opts = {
|
||||
defaultRole: "user",
|
||||
@@ -205,9 +214,16 @@ export const admin = <O extends AdminOptions>(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 = <O extends AdminOptions>(options?: O) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
$Infer: {
|
||||
body: {} as {
|
||||
userId: string;
|
||||
role:
|
||||
| InferAdminRolesFromOption<O>
|
||||
| InferAdminRolesFromOption<O>[];
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx) => {
|
||||
@@ -254,7 +278,7 @@ export const admin = <O extends AdminOptions>(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 = <O extends AdminOptions>(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 = <O extends AdminOptions>(options?: O) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
$Infer: {
|
||||
body: {} as {
|
||||
email: string;
|
||||
password: string;
|
||||
name: string;
|
||||
role?:
|
||||
| InferAdminRolesFromOption<O>
|
||||
| InferAdminRolesFromOption<O>[];
|
||||
data?: Record<string, any>;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx) => {
|
||||
@@ -349,7 +391,10 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
await ctx.context.internalAdapter.createUser<UserWithRole>({
|
||||
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 = <O extends AdminOptions>(options?: O) => {
|
||||
[key in keyof Statements]?: Array<Statements[key][number]>;
|
||||
};
|
||||
userId?: string;
|
||||
role?: string;
|
||||
role?: InferAdminRolesFromOption<O>;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 = <O extends AdminClientOptions>(options?: O) => {
|
||||
};
|
||||
|
||||
return {
|
||||
id: "better-auth-client",
|
||||
id: "admin-client",
|
||||
$InferServerPlugin: {} as ReturnType<
|
||||
typeof admin<{
|
||||
ac: O["ac"] extends AccessControl
|
||||
|
||||
Reference in New Issue
Block a user