UNPKG

better-auth

Version:

The most comprehensive authentication library for TypeScript.

1,519 lines (1,514 loc) 48.4 kB
import * as z from 'zod/v4'; import { APIError } from 'better-call'; import './better-auth.CewjboYP.mjs'; import { c as createAuthMiddleware, g as getSessionFromCtx, a as createAuthEndpoint, B as BASE_ERROR_CODES } from './better-auth.DV5EHeYG.mjs'; import { s as setSessionCookie, d as deleteSessionCookie } from './better-auth.UfVWArIB.mjs'; import { m as mergeSchema, b as parseUserOutput } from './better-auth.Dcv8PS7T.mjs'; import './better-auth.CMQ3rA-I.mjs'; import './better-auth.BjBlybv-.mjs'; import { g as getDate } from './better-auth.CW6D9eSx.mjs'; import { g as getEndpointResponse } from './better-auth.DQI8AD7d.mjs'; import { h as hasPermission } from './better-auth.bkwPl2G4.mjs'; const ADMIN_ERROR_CODES = { FAILED_TO_CREATE_USER: "Failed to create user", USER_ALREADY_EXISTS: "User already exists. Use another email.", YOU_CANNOT_BAN_YOURSELF: "You cannot ban yourself", YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE: "You are not allowed to change users role", YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS: "You are not allowed to create users", YOU_ARE_NOT_ALLOWED_TO_LIST_USERS: "You are not allowed to list users", YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS: "You are not allowed to list users sessions", YOU_ARE_NOT_ALLOWED_TO_BAN_USERS: "You are not allowed to ban users", YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS: "You are not allowed to impersonate users", YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS: "You are not allowed to revoke users sessions", YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS: "You are not allowed to delete users", YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD: "You are not allowed to set users password", BANNED_USER: "You have been banned from this application", YOU_ARE_NOT_ALLOWED_TO_GET_USER: "You are not allowed to get user", NO_DATA_TO_UPDATE: "No data to update", YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS: "You are not allowed to update users", YOU_CANNOT_REMOVE_YOURSELF: "You cannot remove yourself" }; const schema = { user: { fields: { role: { type: "string", required: false, input: false }, banned: { type: "boolean", defaultValue: false, required: false, input: false }, banReason: { type: "string", required: false, input: false }, banExpires: { type: "date", required: false, input: false } } }, session: { fields: { impersonatedBy: { type: "string", required: false } } } }; function parseRoles(roles) { return Array.isArray(roles) ? roles.join(",") : roles; } const admin = (options) => { const opts = { defaultRole: options?.defaultRole ?? "user", adminRoles: options?.adminRoles ?? ["admin"], bannedUserMessage: options?.bannedUserMessage ?? "You have been banned from this application. Please contact support if you believe this is an error.", ...options }; const adminMiddleware = createAuthMiddleware(async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session) { throw new APIError("UNAUTHORIZED"); } return { session }; }); return { id: "admin", init() { return { options: { databaseHooks: { user: { create: { async before(user) { return { data: { role: options?.defaultRole ?? "user", ...user } }; } } }, session: { create: { async before(session, ctx) { if (!ctx) { return; } const user = await ctx.context.internalAdapter.findUserById( session.userId ); if (user.banned) { if (user.banExpires && new Date(user.banExpires).getTime() < Date.now()) { await ctx.context.internalAdapter.updateUser( session.userId, { banned: false, banReason: null, banExpires: null } ); return; } if (ctx && (ctx.path.startsWith("/callback") || ctx.path.startsWith("/oauth2/callback"))) { const redirectURI = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`; throw ctx.redirect( `${redirectURI}?error=banned&error_description=${opts.bannedUserMessage}` ); } throw new APIError("FORBIDDEN", { message: opts.bannedUserMessage, code: "BANNED_USER" }); } } } } } } }; }, hooks: { after: [ { matcher(context) { return context.path === "/list-sessions"; }, handler: createAuthMiddleware(async (ctx) => { const response = await getEndpointResponse(ctx); if (!response) { return; } const newJson = response.filter((session) => { return !session.impersonatedBy; }); return ctx.json(newJson); }) } ] }, endpoints: { /** * ### Endpoint * * POST `/admin/set-role` * * ### API Methods * * **server:** * `auth.api.setRole` * * **client:** * `authClient.admin.setRole` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-set-role) */ setRole: createAuthEndpoint( "/admin/set-role", { method: "POST", body: z.object({ userId: z.coerce.string().meta({ description: "The user id" }), role: z.union([ z.string().meta({ description: "The role to set. `admin` or `user` by default" }), z.array( z.string().meta({ description: "The roles to set. `admin` or `user` by default" }) ) ]).meta({ description: "The role to set, this can be a string or an array of strings. Eg: `admin` or `[admin, user]`" }) }), requireHeaders: true, use: [adminMiddleware], metadata: { openapi: { operationId: "setRole", summary: "Set the role of a user", description: "Set the role of a user", responses: { 200: { description: "User role updated", content: { "application/json": { schema: { type: "object", properties: { user: { $ref: "#/components/schemas/User" } } } } } } } }, $Infer: { body: {} } } }, async (ctx) => { const canSetRole = hasPermission({ userId: ctx.context.session.user.id, role: ctx.context.session.user.role, options: opts, permissions: { user: ["set-role"] } }); if (!canSetRole) { throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE }); } const updatedUser = await ctx.context.internalAdapter.updateUser( ctx.body.userId, { role: parseRoles(ctx.body.role) }, ctx ); return ctx.json({ user: updatedUser }); } ), getUser: createAuthEndpoint( "/admin/get-user", { method: "GET", query: z.object({ id: z.string().meta({ description: "The id of the User" }) }), use: [adminMiddleware], metadata: { openapi: { operationId: "getUser", summary: "Get an existing user", description: "Get an existing user", responses: { 200: { description: "User", content: { "application/json": { schema: { type: "object", properties: { user: { $ref: "#/components/schemas/User" } } } } } } } } } }, async (ctx) => { const { id } = ctx.query; const canGetUser = hasPermission({ userId: ctx.context.session.user.id, role: ctx.context.session.user.role, options: opts, permissions: { user: ["get"] } }); if (!canGetUser) { throw ctx.error("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_GET_USER, code: "YOU_ARE_NOT_ALLOWED_TO_GET_USER" }); } const user = await ctx.context.internalAdapter.findUserById(id); if (!user) { throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.USER_NOT_FOUND }); } return parseUserOutput(ctx.context.options, user); } ), /** * ### Endpoint * * POST `/admin/create-user` * * ### API Methods * * **server:** * `auth.api.createUser` * * **client:** * `authClient.admin.createUser` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-create-user) */ createUser: createAuthEndpoint( "/admin/create-user", { method: "POST", body: z.object({ email: z.string().meta({ description: "The email of the user" }), password: z.string().meta({ description: "The password of the user" }), name: z.string().meta({ description: "The name of the user" }), role: z.union([ z.string().meta({ description: "The role of the user" }), z.array( z.string().meta({ description: "The roles of user" }) ) ]).optional().meta({ description: `A string or array of strings representing the roles to apply to the new user. Eg: "user"` }), /** * extra fields for user */ data: z.record(z.string(), z.any()).optional().meta({ description: "Extra fields for the user. Including custom additional fields." }) }), metadata: { openapi: { operationId: "createUser", summary: "Create a new user", description: "Create a new user", responses: { 200: { description: "User created", content: { "application/json": { schema: { type: "object", properties: { user: { $ref: "#/components/schemas/User" } } } } } } } }, $Infer: { body: {} } } }, async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session && (ctx.request || ctx.headers)) { throw ctx.error("UNAUTHORIZED"); } if (session) { const canCreateUser = hasPermission({ userId: session.user.id, role: session.user.role, options: opts, permissions: { user: ["create"] } }); if (!canCreateUser) { throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS }); } } const existUser = await ctx.context.internalAdapter.findUserByEmail( ctx.body.email ); if (existUser) { throw new APIError("BAD_REQUEST", { message: ADMIN_ERROR_CODES.USER_ALREADY_EXISTS }); } const user = await ctx.context.internalAdapter.createUser( { email: ctx.body.email, name: ctx.body.name, role: (ctx.body.role && parseRoles(ctx.body.role)) ?? options?.defaultRole ?? "user", ...ctx.body.data }, ctx ); if (!user) { throw new APIError("INTERNAL_SERVER_ERROR", { message: ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER }); } const hashedPassword = await ctx.context.password.hash( ctx.body.password ); await ctx.context.internalAdapter.linkAccount( { accountId: user.id, providerId: "credential", password: hashedPassword, userId: user.id }, ctx ); return ctx.json({ user }); } ), adminUpdateUser: createAuthEndpoint( "/admin/update-user", { method: "POST", body: z.object({ userId: z.coerce.string().meta({ description: "The user id" }), data: z.record(z.any(), z.any()).meta({ description: "The user data to update" }) }), use: [adminMiddleware], metadata: { openapi: { operationId: "updateUser", summary: "Update a user", description: "Update a user's details", responses: { 200: { description: "User updated", content: { "application/json": { schema: { type: "object", properties: { user: { $ref: "#/components/schemas/User" } } } } } } } } } }, async (ctx) => { const canUpdateUser = hasPermission({ userId: ctx.context.session.user.id, role: ctx.context.session.user.role, options: opts, permissions: { user: ["update"] } }); if (!canUpdateUser) { throw ctx.error("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS, code: "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS" }); } if (Object.keys(ctx.body.data).length === 0) { throw new APIError("BAD_REQUEST", { message: ADMIN_ERROR_CODES.NO_DATA_TO_UPDATE }); } if (ctx.body.data?.role) { ctx.body.data.role = parseRoles(ctx.body.data.role); } const updatedUser = await ctx.context.internalAdapter.updateUser( ctx.body.userId, ctx.body.data, ctx ); return ctx.json(updatedUser); } ), listUsers: createAuthEndpoint( "/admin/list-users", { method: "GET", use: [adminMiddleware], query: z.object({ searchValue: z.string().optional().meta({ description: 'The value to search for. Eg: "some name"' }), searchField: z.enum(["email", "name"]).meta({ description: 'The field to search in, defaults to email. Can be `email` or `name`. Eg: "name"' }).optional(), searchOperator: z.enum(["contains", "starts_with", "ends_with"]).meta({ description: 'The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`. Eg: "contains"' }).optional(), limit: z.string().meta({ description: "The number of users to return" }).or(z.number()).optional(), offset: z.string().meta({ description: "The offset to start from" }).or(z.number()).optional(), sortBy: z.string().meta({ description: "The field to sort by" }).optional(), sortDirection: z.enum(["asc", "desc"]).meta({ description: "The direction to sort by" }).optional(), filterField: z.string().meta({ description: "The field to filter by" }).optional(), filterValue: z.string().meta({ description: "The value to filter by" }).or(z.number()).or(z.boolean()).optional(), filterOperator: z.enum(["eq", "ne", "lt", "lte", "gt", "gte", "contains"]).meta({ description: "The operator to use for the filter" }).optional() }), metadata: { openapi: { operationId: "listUsers", summary: "List users", description: "List users", responses: { 200: { description: "List of users", content: { "application/json": { schema: { type: "object", properties: { users: { type: "array", items: { $ref: "#/components/schemas/User" } }, total: { type: "number" }, limit: { type: "number" }, offset: { type: "number" } }, required: ["users", "total"] } } } } } } } }, async (ctx) => { const session = ctx.context.session; const canListUsers = hasPermission({ userId: ctx.context.session.user.id, role: session.user.role, options: opts, permissions: { user: ["list"] } }); if (!canListUsers) { throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_USERS }); } const where = []; if (ctx.query?.searchValue) { where.push({ field: ctx.query.searchField || "email", operator: ctx.query.searchOperator || "contains", value: ctx.query.searchValue }); } if (ctx.query?.filterValue) { where.push({ field: ctx.query.filterField || "email", operator: ctx.query.filterOperator || "eq", value: ctx.query.filterValue }); } try { const users = await ctx.context.internalAdapter.listUsers( Number(ctx.query?.limit) || void 0, Number(ctx.query?.offset) || void 0, ctx.query?.sortBy ? { field: ctx.query.sortBy, direction: ctx.query.sortDirection || "asc" } : void 0, where.length ? where : void 0 ); const total = await ctx.context.internalAdapter.countTotalUsers( where.length ? where : void 0 ); return ctx.json({ users, total, limit: Number(ctx.query?.limit) || void 0, offset: Number(ctx.query?.offset) || void 0 }); } catch (e) { return ctx.json({ users: [], total: 0 }); } } ), /** * ### Endpoint * * POST `/admin/list-user-sessions` * * ### API Methods * * **server:** * `auth.api.listUserSessions` * * **client:** * `authClient.admin.listUserSessions` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-list-user-sessions) */ listUserSessions: createAuthEndpoint( "/admin/list-user-sessions", { method: "POST", use: [adminMiddleware], body: z.object({ userId: z.coerce.string().meta({ description: "The user id" }) }), metadata: { openapi: { operationId: "listUserSessions", summary: "List user sessions", description: "List user sessions", responses: { 200: { description: "List of user sessions", content: { "application/json": { schema: { type: "object", properties: { sessions: { type: "array", items: { $ref: "#/components/schemas/Session" } } } } } } } } } } }, async (ctx) => { const session = ctx.context.session; const canListSessions = hasPermission({ userId: ctx.context.session.user.id, role: session.user.role, options: opts, permissions: { session: ["list"] } }); if (!canListSessions) { throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS }); } const sessions = await ctx.context.internalAdapter.listSessions(ctx.body.userId); return { sessions }; } ), /** * ### Endpoint * * POST `/admin/unban-user` * * ### API Methods * * **server:** * `auth.api.unbanUser` * * **client:** * `authClient.admin.unbanUser` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-unban-user) */ unbanUser: createAuthEndpoint( "/admin/unban-user", { method: "POST", body: z.object({ userId: z.coerce.string().meta({ description: "The user id" }) }), use: [adminMiddleware], metadata: { openapi: { operationId: "unbanUser", summary: "Unban a user", description: "Unban a user", responses: { 200: { description: "User unbanned", content: { "application/json": { schema: { type: "object", properties: { user: { $ref: "#/components/schemas/User" } } } } } } } } } }, async (ctx) => { const session = ctx.context.session; const canBanUser = hasPermission({ userId: ctx.context.session.user.id, role: session.user.role, options: opts, permissions: { user: ["ban"] } }); if (!canBanUser) { throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS }); } const user = await ctx.context.internalAdapter.updateUser( ctx.body.userId, { banned: false, banExpires: null, banReason: null, updatedAt: /* @__PURE__ */ new Date() } ); return ctx.json({ user }); } ), /** * ### Endpoint * * POST `/admin/ban-user` * * ### API Methods * * **server:** * `auth.api.banUser` * * **client:** * `authClient.admin.banUser` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-ban-user) */ banUser: createAuthEndpoint( "/admin/ban-user", { method: "POST", body: z.object({ userId: z.coerce.string().meta({ description: "The user id" }), /** * Reason for the ban */ banReason: z.string().meta({ description: "The reason for the ban" }).optional(), /** * Number of seconds until the ban expires */ banExpiresIn: z.number().meta({ description: "The number of seconds until the ban expires" }).optional() }), use: [adminMiddleware], metadata: { openapi: { operationId: "banUser", summary: "Ban a user", description: "Ban a user", responses: { 200: { description: "User banned", content: { "application/json": { schema: { type: "object", properties: { user: { $ref: "#/components/schemas/User" } } } } } } } } } }, async (ctx) => { const session = ctx.context.session; const canBanUser = hasPermission({ userId: ctx.context.session.user.id, role: session.user.role, options: opts, permissions: { user: ["ban"] } }); if (!canBanUser) { throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS }); } if (ctx.body.userId === ctx.context.session.user.id) { throw new APIError("BAD_REQUEST", { message: ADMIN_ERROR_CODES.YOU_CANNOT_BAN_YOURSELF }); } const user = await ctx.context.internalAdapter.updateUser( ctx.body.userId, { banned: true, banReason: ctx.body.banReason || options?.defaultBanReason || "No reason", banExpires: ctx.body.banExpiresIn ? getDate(ctx.body.banExpiresIn, "sec") : options?.defaultBanExpiresIn ? getDate(options.defaultBanExpiresIn, "sec") : void 0, updatedAt: /* @__PURE__ */ new Date() }, ctx ); await ctx.context.internalAdapter.deleteSessions(ctx.body.userId); return ctx.json({ user }); } ), /** * ### Endpoint * * POST `/admin/impersonate-user` * * ### API Methods * * **server:** * `auth.api.impersonateUser` * * **client:** * `authClient.admin.impersonateUser` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-impersonate-user) */ impersonateUser: createAuthEndpoint( "/admin/impersonate-user", { method: "POST", body: z.object({ userId: z.coerce.string().meta({ description: "The user id" }) }), use: [adminMiddleware], metadata: { openapi: { operationId: "impersonateUser", summary: "Impersonate a user", description: "Impersonate a user", responses: { 200: { description: "Impersonation session created", content: { "application/json": { schema: { type: "object", properties: { session: { $ref: "#/components/schemas/Session" }, user: { $ref: "#/components/schemas/User" } } } } } } } } } }, async (ctx) => { const canImpersonateUser = hasPermission({ userId: ctx.context.session.user.id, role: ctx.context.session.user.role, options: opts, permissions: { user: ["impersonate"] } }); if (!canImpersonateUser) { throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS }); } const targetUser = await ctx.context.internalAdapter.findUserById( ctx.body.userId ); if (!targetUser) { throw new APIError("NOT_FOUND", { message: "User not found" }); } const session = await ctx.context.internalAdapter.createSession( targetUser.id, ctx, true, { impersonatedBy: ctx.context.session.user.id, expiresAt: options?.impersonationSessionDuration ? getDate(options.impersonationSessionDuration, "sec") : getDate(60 * 60, "sec") // 1 hour }, true ); if (!session) { throw new APIError("INTERNAL_SERVER_ERROR", { message: ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER }); } const authCookies = ctx.context.authCookies; deleteSessionCookie(ctx); const dontRememberMeCookie = await ctx.getSignedCookie( ctx.context.authCookies.dontRememberToken.name, ctx.context.secret ); const adminCookieProp = ctx.context.createAuthCookie("admin_session"); await ctx.setSignedCookie( adminCookieProp.name, `${ctx.context.session.session.token}:${dontRememberMeCookie || ""}`, ctx.context.secret, authCookies.sessionToken.options ); await setSessionCookie( ctx, { session, user: targetUser }, true ); return ctx.json({ session, user: targetUser }); } ), /** * ### Endpoint * * POST `/admin/stop-impersonating` * * ### API Methods * * **server:** * `auth.api.stopImpersonating` * * **client:** * `authClient.admin.stopImpersonating` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-stop-impersonating) */ stopImpersonating: createAuthEndpoint( "/admin/stop-impersonating", { method: "POST", requireHeaders: true }, async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session) { throw new APIError("UNAUTHORIZED"); } if (!session.session.impersonatedBy) { throw new APIError("BAD_REQUEST", { message: "You are not impersonating anyone" }); } const user = await ctx.context.internalAdapter.findUserById( session.session.impersonatedBy ); if (!user) { throw new APIError("INTERNAL_SERVER_ERROR", { message: "Failed to find user" }); } const adminCookieName = ctx.context.createAuthCookie("admin_session").name; const adminCookie = await ctx.getSignedCookie( adminCookieName, ctx.context.secret ); if (!adminCookie) { throw new APIError("INTERNAL_SERVER_ERROR", { message: "Failed to find admin session" }); } const [adminSessionToken, dontRememberMeCookie] = adminCookie?.split(":"); const adminSession = await ctx.context.internalAdapter.findSession(adminSessionToken); if (!adminSession || adminSession.session.userId !== user.id) { throw new APIError("INTERNAL_SERVER_ERROR", { message: "Failed to find admin session" }); } await ctx.context.internalAdapter.deleteSession( session.session.token ); await setSessionCookie(ctx, adminSession, !!dontRememberMeCookie); return ctx.json(adminSession); } ), /** * ### Endpoint * * POST `/admin/revoke-user-session` * * ### API Methods * * **server:** * `auth.api.revokeUserSession` * * **client:** * `authClient.admin.revokeUserSession` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-revoke-user-session) */ revokeUserSession: createAuthEndpoint( "/admin/revoke-user-session", { method: "POST", body: z.object({ sessionToken: z.string().meta({ description: "The session token" }) }), use: [adminMiddleware], metadata: { openapi: { operationId: "revokeUserSession", summary: "Revoke a user session", description: "Revoke a user session", responses: { 200: { description: "Session revoked", content: { "application/json": { schema: { type: "object", properties: { success: { type: "boolean" } } } } } } } } } }, async (ctx) => { const session = ctx.context.session; const canRevokeSession = hasPermission({ userId: ctx.context.session.user.id, role: session.user.role, options: opts, permissions: { session: ["revoke"] } }); if (!canRevokeSession) { throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS }); } await ctx.context.internalAdapter.deleteSession( ctx.body.sessionToken ); return ctx.json({ success: true }); } ), /** * ### Endpoint * * POST `/admin/revoke-user-sessions` * * ### API Methods * * **server:** * `auth.api.revokeUserSessions` * * **client:** * `authClient.admin.revokeUserSessions` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-revoke-user-sessions) */ revokeUserSessions: createAuthEndpoint( "/admin/revoke-user-sessions", { method: "POST", body: z.object({ userId: z.coerce.string().meta({ description: "The user id" }) }), use: [adminMiddleware], metadata: { openapi: { operationId: "revokeUserSessions", summary: "Revoke all user sessions", description: "Revoke all user sessions", responses: { 200: { description: "Sessions revoked", content: { "application/json": { schema: { type: "object", properties: { success: { type: "boolean" } } } } } } } } } }, async (ctx) => { const session = ctx.context.session; const canRevokeSession = hasPermission({ userId: ctx.context.session.user.id, role: session.user.role, options: opts, permissions: { session: ["revoke"] } }); if (!canRevokeSession) { throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS }); } await ctx.context.internalAdapter.deleteSessions(ctx.body.userId); return ctx.json({ success: true }); } ), /** * ### Endpoint * * POST `/admin/remove-user` * * ### API Methods * * **server:** * `auth.api.removeUser` * * **client:** * `authClient.admin.removeUser` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-remove-user) */ removeUser: createAuthEndpoint( "/admin/remove-user", { method: "POST", body: z.object({ userId: z.coerce.string().meta({ description: "The user id" }) }), use: [adminMiddleware], metadata: { openapi: { operationId: "removeUser", summary: "Remove a user", description: "Delete a user and all their sessions and accounts. Cannot be undone.", responses: { 200: { description: "User removed", content: { "application/json": { schema: { type: "object", properties: { success: { type: "boolean" } } } } } } } } } }, async (ctx) => { const session = ctx.context.session; const canDeleteUser = hasPermission({ userId: ctx.context.session.user.id, role: session.user.role, options: opts, permissions: { user: ["delete"] } }); if (!canDeleteUser) { throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS }); } if (ctx.body.userId === ctx.context.session.user.id) { throw new APIError("BAD_REQUEST", { message: ADMIN_ERROR_CODES.YOU_CANNOT_REMOVE_YOURSELF }); } const user = await ctx.context.internalAdapter.findUserById( ctx.body.userId ); if (!user) { throw new APIError("NOT_FOUND", { message: "User not found" }); } await ctx.context.internalAdapter.deleteUser(ctx.body.userId); return ctx.json({ success: true }); } ), /** * ### Endpoint * * POST `/admin/set-user-password` * * ### API Methods * * **server:** * `auth.api.setUserPassword` * * **client:** * `authClient.admin.setUserPassword` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-set-user-password) */ setUserPassword: createAuthEndpoint( "/admin/set-user-password", { method: "POST", body: z.object({ newPassword: z.string().meta({ description: "The new password" }), userId: z.coerce.string().meta({ description: "The user id" }) }), use: [adminMiddleware], metadata: { openapi: { operationId: "setUserPassword", summary: "Set a user's password", description: "Set a user's password", responses: { 200: { description: "Password set", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean" } } } } } } } } } }, async (ctx) => { const canSetUserPassword = hasPermission({ userId: ctx.context.session.user.id, role: ctx.context.session.user.role, options: opts, permissions: { user: ["set-password"] } }); if (!canSetUserPassword) { throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD }); } const hashedPassword = await ctx.context.password.hash( ctx.body.newPassword ); await ctx.context.internalAdapter.updatePassword( ctx.body.userId, hashedPassword ); return ctx.json({ status: true }); } ), /** * ### Endpoint * * POST `/admin/has-permission` * * ### API Methods * * **server:** * `auth.api.userHasPermission` * * **client:** * `authClient.admin.hasPermission` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-has-permission) */ userHasPermission: createAuthEndpoint( "/admin/has-permission", { method: "POST", body: z.object({ userId: z.coerce.string().optional().meta({ description: `The user id. Eg: "user-id"` }), role: z.string().optional().meta({ description: `The role to check permission for. Eg: "admin"` }) }).and( z.union([ z.object({ permission: z.record(z.string(), z.array(z.string())), permissions: z.undefined() }), z.object({ permission: z.undefined(), permissions: z.record(z.string(), z.array(z.string())) }) ]) ), metadata: { openapi: { description: "Check if the user has permission", requestBody: { content: { "application/json": { schema: { type: "object", properties: { permission: { type: "object", description: "The permission to check", deprecated: true }, permissions: { type: "object", description: "The permission to check" } }, required: ["permissions"] } } } }, responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { error: { type: "string" }, success: { type: "boolean" } }, required: ["success"] } } } } } }, $Infer: { body: {} } } }, async (ctx) => { if (!ctx.body?.permission && !ctx.body?.permissions) { throw new APIError("BAD_REQUEST", { message: "invalid permission check. no permission(s) were passed." }); } const session = await getSessionFromCtx(ctx); if (!session && (ctx.request || ctx.headers) && !ctx.body.userId && !ctx.body.role) { throw new APIError("UNAUTHORIZED"); } const user = session?.user || await ctx.context.internalAdapter.findUserById( ctx.body.userId ) || (ctx.body.role ? { id: "", role: ctx.body.role } : null); if (!user) { throw new APIError("BAD_REQUEST", { message: "user not found" }); } const result = hasPermission({ userId: user.id, role: user.role, options, permissions: ctx.body.permissions ?? ctx.body.permission }); return ctx.json({ error: null, success: result }); } ) }, $ERROR_CODES: ADMIN_ERROR_CODES, schema: mergeSchema(schema, opts.schema), options }; }; export { admin as a };