From df72736d36c64747155b80d16d3fb9ea62ff7f25 Mon Sep 17 00:00:00 2001 From: Netrifier Date: Tue, 1 Apr 2025 22:54:02 +0530 Subject: [PATCH] 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 --- docs/content/docs/plugins/admin.mdx | 8 ++- .../src/plugins/admin/admin.test.ts | 56 +++++++++++++++++ .../better-auth/src/plugins/admin/admin.ts | 63 ++++++++++++++++--- .../better-auth/src/plugins/admin/client.ts | 6 +- 4 files changed, 119 insertions(+), 14 deletions(-) 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