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:
Netrifier
2025-04-01 22:54:02 +05:30
committed by GitHub
parent e62ea0ff31
commit df72736d36
4 changed files with 119 additions and 14 deletions

View File

@@ -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.

View File

@@ -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(
{

View File

@@ -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>;
},
},
},

View File

@@ -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