better-auth 1.4.6-beta.2 → 1.4.6-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{access-vPNO48H4.mjs → access-BktEfzR6.mjs} +1 -1
- package/dist/{access-h-uttVNZ.mjs → access-DZRRE6Tq.mjs} +1 -1
- package/dist/adapters/drizzle-adapter/index.mjs +1 -6
- package/dist/adapters/index.d.mts +2 -237
- package/dist/adapters/index.mjs +1 -6
- package/dist/adapters/kysely-adapter/index.d.mts +1 -1
- package/dist/adapters/kysely-adapter/index.mjs +2 -7
- package/dist/adapters/memory-adapter/index.mjs +1 -6
- package/dist/adapters/mongodb-adapter/index.d.mts +1 -1
- package/dist/adapters/mongodb-adapter/index.mjs +1 -6
- package/dist/adapters/prisma-adapter/index.mjs +1 -6
- package/dist/adapters/test.mjs +2 -3
- package/dist/admin-6fpWm8vW.mjs +971 -0
- package/dist/{anonymous-DxoIOUPc.mjs → anonymous-BvvfPHFI.mjs} +5 -6
- package/dist/api/index.d.mts +2 -4
- package/dist/api/index.mjs +10 -13
- package/dist/{api-CkmycQ2x.mjs → api-B67AcvJ9.mjs} +67 -91
- package/dist/auth/minimal.d.mts +3 -4
- package/dist/auth/minimal.mjs +12 -15
- package/dist/{auth-DfR8_5w3.mjs → auth-D5QxgEcO.mjs} +3 -3
- package/dist/{base-CiRMFqet.mjs → base-CtTlmMzu.mjs} +40 -10
- package/dist/{bearer-C6RSCXv_.mjs → bearer-GIQo0kPs.mjs} +1 -1
- package/dist/{captcha-Dbvurc2Z.mjs → captcha-av_BAGRz.mjs} +1 -1
- package/dist/{chunk-DbI2OXuS.mjs → chunk-DieNfLhd.mjs} +2 -4
- package/dist/client/index.d.mts +40 -21
- package/dist/client/index.mjs +4 -5
- package/dist/client/lynx/index.d.mts +4 -6
- package/dist/client/lynx/index.mjs +5 -7
- package/dist/client/plugins/index.d.mts +45 -46
- package/dist/client/plugins/index.mjs +10 -11
- package/dist/client/react/index.d.mts +17 -19
- package/dist/client/react/index.mjs +5 -7
- package/dist/client/solid/index.d.mts +3 -4
- package/dist/client/solid/index.mjs +4 -4
- package/dist/client/svelte/index.d.mts +18 -19
- package/dist/client/svelte/index.mjs +4 -4
- package/dist/client/vue/index.d.mts +18 -19
- package/dist/client/vue/index.mjs +4 -4
- package/dist/{client-BN9e9KX1.mjs → client-BUwkFYja.mjs} +2 -2
- package/dist/cookies/index.d.mts +3 -5
- package/dist/cookies/index.mjs +4 -5
- package/dist/{cookies-D72PbWdz.mjs → cookies-DmQmKltR.mjs} +4 -4
- package/dist/crypto/index.mjs +1 -1
- package/dist/{custom-session-CaUbM_of.mjs → custom-session-BB-BwDl8.mjs} +2 -2
- package/dist/db/index.d.mts +3 -5
- package/dist/db/index.mjs +5 -10
- package/dist/device-authorization-Bo7zd0O8.mjs +593 -0
- package/dist/{dialect-D9ZUZA4J.mjs → dialect-BHuPIP4Z.mjs} +2 -2
- package/dist/email-otp-BDqXTWoL.mjs +736 -0
- package/dist/{esm-BXhcK_bt.mjs → esm-CyZgw_uF.mjs} +83 -83
- package/dist/{generic-oauth-BGbWWUh9.mjs → generic-oauth-DdO9dcA6.mjs} +385 -346
- package/dist/{get-migration-Bf0TuCzm.mjs → get-migration--xV8I5XU.mjs} +36 -8
- package/dist/{has-permission-qCjQqGds.mjs → has-permission-BxveqtYZ.mjs} +1 -1
- package/dist/{haveibeenpwned-BSsFUFTi.mjs → haveibeenpwned-DLiHZcSj.mjs} +1 -1
- package/dist/{index-RbmRVtaI.d.mts → index-1pOfmpZV.d.mts} +3 -3
- package/dist/{index-BvZWUEoC.d.mts → index-7pFhhF0x.d.mts} +6 -6
- package/dist/{index-rnNTrfA7.d.mts → index-B1fASdrI.d.mts} +1 -1
- package/dist/{index-BcnaxwZM.d.mts → index-BDCxDMd9.d.mts} +36 -80
- package/dist/{index-hBQ1qSDX.d.mts → index-BP8GKS1P.d.mts} +4 -4
- package/dist/{index-DlOKn_yt.d.mts → index-BfGjzoFj.d.mts} +24 -24
- package/dist/{index-pQ7I9EOb.d.mts → index-BgCLcoV7.d.mts} +2 -2
- package/dist/{index-CfdutUAu.d.mts → index-BgRi_ahI.d.mts} +6 -6
- package/dist/{index-D0UwTS5_.d.mts → index-Bzvy2fZc.d.mts} +15 -15
- package/dist/{index-Bqj2Kztr.d.mts → index-C-MbAsf4.d.mts} +9 -9
- package/dist/{index--dkKT2P2.d.mts → index-CVZJwI8R.d.mts} +5 -5
- package/dist/{index-73Iin-Jq.d.mts → index-CoRq_As_.d.mts} +6 -6
- package/dist/{index-Be0syNaw.d.mts → index-D8JLGqFA.d.mts} +85 -181
- package/dist/{index-DA5jLHwS.d.mts → index-DfQ1cHMu.d.mts} +2 -2
- package/dist/{index-Bt92FfJd.d.mts → index-GPso9wJG.d.mts} +33 -33
- package/dist/{index-CLll1XPI.d.mts → index-HVSLuFvF.d.mts} +8 -8
- package/dist/{index-BevOkArW.d.mts → index-ItLtiMMN.d.mts} +249 -257
- package/dist/{index-CGKooORC.d.mts → index-MexUZ7RH.d.mts} +4 -4
- package/dist/{index-akAuJUJt.d.mts → index-P-wYkAD5.d.mts} +170 -197
- package/dist/{index-Dqjfj0PE.d.mts → index-V081dNa-.d.mts} +808 -1024
- package/dist/{index-BVpFCwyn.d.mts → index-ZA57__3E.d.mts} +1 -1
- package/dist/{index-CWcnHbjn.d.mts → index-fnLzWr2C.d.mts} +2 -2
- package/dist/{index-BfHctM9K.d.mts → index-veGVCQDP.d.mts} +7 -7
- package/dist/index.d.mts +5 -6
- package/dist/index.mjs +15 -18
- package/dist/integrations/next-js.d.mts +4 -4
- package/dist/integrations/next-js.mjs +4 -5
- package/dist/integrations/node.d.mts +2 -4
- package/dist/integrations/svelte-kit.d.mts +4 -6
- package/dist/integrations/svelte-kit.mjs +4 -5
- package/dist/integrations/tanstack-start.d.mts +4 -4
- package/dist/integrations/tanstack-start.mjs +4 -5
- package/dist/{jwt-Ar1sciI2.mjs → jwt-YwDmNjLC.mjs} +18 -15
- package/dist/{magic-link-QdHvtdfs.mjs → magic-link-DHqYwrFb.mjs} +19 -17
- package/dist/{multi-session-CbEYz_wJ.mjs → multi-session-DFQQVetu.mjs} +7 -5
- package/dist/{oauth-proxy-Dz1E1SVN.mjs → oauth-proxy-EqrMlSDE.mjs} +10 -9
- package/dist/oauth2/index.d.mts +2 -4
- package/dist/oauth2/index.mjs +10 -13
- package/dist/{oidc-provider-B9SsN23J.mjs → oidc-provider-iL1I_C0p.mjs} +56 -51
- package/dist/{one-tap-Bz8Q39Od.mjs → one-tap-DL0Yr6_1.mjs} +5 -4
- package/dist/{one-time-token-C8YQxf38.mjs → one-time-token-B14NUG5P.mjs} +5 -4
- package/dist/{open-api-DZG02vyi.mjs → open-api-DFxa4uca.mjs} +3 -3
- package/dist/{organization-BdJSRNgM.mjs → organization-BHI3Ry6r.mjs} +139 -112
- package/dist/phone-number-D4JdoQf_.mjs +565 -0
- package/dist/plugins/access/index.d.mts +1 -2
- package/dist/plugins/access/index.mjs +1 -1
- package/dist/plugins/admin/access/index.d.mts +23 -25
- package/dist/plugins/admin/access/index.mjs +2 -2
- package/dist/plugins/admin/index.d.mts +22 -25
- package/dist/plugins/admin/index.mjs +15 -18
- package/dist/plugins/anonymous/index.d.mts +3 -5
- package/dist/plugins/anonymous/index.mjs +11 -14
- package/dist/plugins/bearer/index.d.mts +1 -1
- package/dist/plugins/bearer/index.mjs +5 -6
- package/dist/plugins/captcha/index.d.mts +1 -1
- package/dist/plugins/captcha/index.mjs +2 -2
- package/dist/plugins/custom-session/index.d.mts +3 -5
- package/dist/plugins/custom-session/index.mjs +12 -15
- package/dist/plugins/device-authorization/index.d.mts +1 -1
- package/dist/plugins/device-authorization/index.mjs +9 -13
- package/dist/plugins/email-otp/index.d.mts +1 -1
- package/dist/plugins/email-otp/index.mjs +12 -15
- package/dist/plugins/generic-oauth/index.d.mts +1 -5
- package/dist/plugins/generic-oauth/index.mjs +11 -14
- package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
- package/dist/plugins/haveibeenpwned/index.mjs +11 -14
- package/dist/plugins/index.d.mts +23 -25
- package/dist/plugins/index.mjs +41 -45
- package/dist/plugins/jwt/index.d.mts +3 -5
- package/dist/plugins/jwt/index.mjs +11 -14
- package/dist/plugins/magic-link/index.d.mts +1 -1
- package/dist/plugins/magic-link/index.mjs +11 -14
- package/dist/plugins/multi-session/index.d.mts +1 -1
- package/dist/plugins/multi-session/index.mjs +11 -14
- package/dist/plugins/oauth-proxy/index.d.mts +1 -1
- package/dist/plugins/oauth-proxy/index.mjs +12 -15
- package/dist/plugins/oidc-provider/index.d.mts +22 -25
- package/dist/plugins/oidc-provider/index.mjs +12 -15
- package/dist/plugins/one-tap/index.d.mts +1 -1
- package/dist/plugins/one-tap/index.mjs +11 -14
- package/dist/plugins/one-time-token/index.d.mts +3 -5
- package/dist/plugins/one-time-token/index.mjs +11 -14
- package/dist/plugins/open-api/index.d.mts +1 -2
- package/dist/plugins/open-api/index.mjs +11 -14
- package/dist/plugins/organization/access/index.d.mts +22 -25
- package/dist/plugins/organization/access/index.mjs +2 -2
- package/dist/plugins/organization/index.d.mts +22 -25
- package/dist/plugins/organization/index.mjs +14 -18
- package/dist/plugins/phone-number/index.d.mts +3 -5
- package/dist/plugins/phone-number/index.mjs +11 -14
- package/dist/plugins/siwe/index.d.mts +3 -5
- package/dist/plugins/siwe/index.mjs +11 -14
- package/dist/plugins/two-factor/index.d.mts +3 -5
- package/dist/plugins/two-factor/index.mjs +13 -16
- package/dist/plugins/username/index.d.mts +2 -3
- package/dist/plugins/username/index.mjs +11 -14
- package/dist/{plugins-Dmw3tk5H.d.mts → plugins-DLdyc73z.d.mts} +1 -1
- package/dist/{plugins-DgSTpOzm.mjs → plugins-qDvRRRie.mjs} +89 -83
- package/dist/{promise-B1BZ0y5h.mjs → promise-CL99vzc3.mjs} +245 -243
- package/dist/{proxy-DplNCOES.mjs → proxy-ehujpdXc.mjs} +2 -2
- package/dist/{schema-Bb7wzeK_.mjs → schema-dfOF7vRb.mjs} +4 -1
- package/dist/{session-AaRl3_x-.mjs → session-Dpj2Z_b_.mjs} +5 -5
- package/dist/{siwe-D81Y4fkp.mjs → siwe-CAoLY7AB.mjs} +10 -9
- package/dist/test-utils/index.d.mts +1154 -1155
- package/dist/test-utils/index.mjs +47 -52
- package/dist/{two-factor-BDQvVILL.mjs → two-factor-BjnAESdO.mjs} +41 -32
- package/dist/types/index.d.mts +3 -4
- package/dist/types/index.mjs +0 -2
- package/dist/{username-C_wmkXwt.mjs → username-BcWNDdCk.mjs} +13 -11
- package/dist/{utils-db7gNqd-.mjs → utils-D4_n6vJh.mjs} +4 -10
- package/package.json +8 -8
- package/dist/adapter-factory-HF3JB9cT.mjs +0 -820
- package/dist/admin-D-OMdNIc.mjs +0 -742
- package/dist/device-authorization-9_f1Up5D.mjs +0 -579
- package/dist/email-otp-CiznqFUN.mjs +0 -614
- package/dist/get-model-name-D4DUV7S2.mjs +0 -366
- package/dist/json-CnHxKYpj.mjs +0 -24
- package/dist/misc-BwNc0MKr.mjs +0 -7
- package/dist/phone-number-wU9XYnr8.mjs +0 -507
- package/dist/types-BOTDmeLz.mjs +0 -1
- /package/dist/{access-Dx8TLKnw.mjs → access-BCQibqkF.mjs} +0 -0
- /package/dist/{bun-sqlite-dialect-C3P_ISb5.mjs → bun-sqlite-dialect-BGIIaWxx.mjs} +0 -0
- /package/dist/{client-CMPEe5-e.mjs → client-7xkXfvW4.mjs} +0 -0
- /package/dist/{crypto-CFUhAR9W.mjs → crypto-DgVHxgLL.mjs} +0 -0
- /package/dist/{get-request-ip-D6st-mto.mjs → get-request-ip-G2Tcmzbb.mjs} +0 -0
- /package/dist/{helper-DFzV6jvx.d.mts → helper-BBvhhJRX.d.mts} +0 -0
- /package/dist/{node-sqlite-dialect-D6w8Ekdz.mjs → node-sqlite-dialect-DL3qojbZ.mjs} +0 -0
- /package/dist/{parser-pHp5yoAv.mjs → parser-CQYBJKoR.mjs} +0 -0
- /package/dist/{password-BFQK0cLg.mjs → password-BRmR7rWA.mjs} +0 -0
- /package/dist/{permission-JwliMugl.mjs → permission-BZUPzNK6.mjs} +0 -0
- /package/dist/{plugin-helper-zFdFWLgL.mjs → plugin-helper-BneBaGtD.mjs} +0 -0
- /package/dist/{types-CEepZ-RG.d.mts → types-Bde2wFm4.d.mts} +0 -0
- /package/dist/{url-CB8xCwz-.mjs → url-B7VXiggp.mjs} +0 -0
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
import { l as parseUserOutput, t as mergeSchema, u as getDate } from "./schema-dfOF7vRb.mjs";
|
|
2
|
+
import { t as APIError } from "./api-B67AcvJ9.mjs";
|
|
3
|
+
import { c as setSessionCookie, n as deleteSessionCookie } from "./cookies-DmQmKltR.mjs";
|
|
4
|
+
import { r as getSessionFromCtx } from "./session-Dpj2Z_b_.mjs";
|
|
5
|
+
import { t as hasPermission } from "./has-permission-BxveqtYZ.mjs";
|
|
6
|
+
import { t as getEndpointResponse } from "./plugin-helper-BneBaGtD.mjs";
|
|
7
|
+
import { BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
8
|
+
import { defineErrorCodes } from "@better-auth/core/utils";
|
|
9
|
+
import * as z from "zod";
|
|
10
|
+
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
11
|
+
|
|
12
|
+
//#region src/plugins/admin/error-codes.ts
|
|
13
|
+
const ADMIN_ERROR_CODES = defineErrorCodes({
|
|
14
|
+
FAILED_TO_CREATE_USER: "Failed to create user",
|
|
15
|
+
USER_ALREADY_EXISTS: "User already exists.",
|
|
16
|
+
USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "User already exists. Use another email.",
|
|
17
|
+
YOU_CANNOT_BAN_YOURSELF: "You cannot ban yourself",
|
|
18
|
+
YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE: "You are not allowed to change users role",
|
|
19
|
+
YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS: "You are not allowed to create users",
|
|
20
|
+
YOU_ARE_NOT_ALLOWED_TO_LIST_USERS: "You are not allowed to list users",
|
|
21
|
+
YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS: "You are not allowed to list users sessions",
|
|
22
|
+
YOU_ARE_NOT_ALLOWED_TO_BAN_USERS: "You are not allowed to ban users",
|
|
23
|
+
YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS: "You are not allowed to impersonate users",
|
|
24
|
+
YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS: "You are not allowed to revoke users sessions",
|
|
25
|
+
YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS: "You are not allowed to delete users",
|
|
26
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD: "You are not allowed to set users password",
|
|
27
|
+
BANNED_USER: "You have been banned from this application",
|
|
28
|
+
YOU_ARE_NOT_ALLOWED_TO_GET_USER: "You are not allowed to get user",
|
|
29
|
+
NO_DATA_TO_UPDATE: "No data to update",
|
|
30
|
+
YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS: "You are not allowed to update users",
|
|
31
|
+
YOU_CANNOT_REMOVE_YOURSELF: "You cannot remove yourself",
|
|
32
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE: "You are not allowed to set a non-existent role value"
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/plugins/admin/routes.ts
|
|
37
|
+
/**
|
|
38
|
+
* Ensures a valid session, if not will throw.
|
|
39
|
+
* Will also provide additional types on the user to include role types.
|
|
40
|
+
*/
|
|
41
|
+
const adminMiddleware = createAuthMiddleware(async (ctx) => {
|
|
42
|
+
const session = await getSessionFromCtx(ctx);
|
|
43
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
44
|
+
return { session };
|
|
45
|
+
});
|
|
46
|
+
function parseRoles(roles) {
|
|
47
|
+
return Array.isArray(roles) ? roles.join(",") : roles;
|
|
48
|
+
}
|
|
49
|
+
const setRoleBodySchema = z.object({
|
|
50
|
+
userId: z.coerce.string().meta({ description: "The user id" }),
|
|
51
|
+
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]`" })
|
|
52
|
+
});
|
|
53
|
+
/**
|
|
54
|
+
* ### Endpoint
|
|
55
|
+
*
|
|
56
|
+
* POST `/admin/set-role`
|
|
57
|
+
*
|
|
58
|
+
* ### API Methods
|
|
59
|
+
*
|
|
60
|
+
* **server:**
|
|
61
|
+
* `auth.api.setRole`
|
|
62
|
+
*
|
|
63
|
+
* **client:**
|
|
64
|
+
* `authClient.admin.setRole`
|
|
65
|
+
*
|
|
66
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-set-role)
|
|
67
|
+
*/
|
|
68
|
+
const setRole = (opts) => createAuthEndpoint("/admin/set-role", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: setRoleBodySchema,
|
|
71
|
+
requireHeaders: true,
|
|
72
|
+
use: [adminMiddleware],
|
|
73
|
+
metadata: {
|
|
74
|
+
openapi: {
|
|
75
|
+
operationId: "setUserRole",
|
|
76
|
+
summary: "Set the role of a user",
|
|
77
|
+
description: "Set the role of a user",
|
|
78
|
+
responses: { 200: {
|
|
79
|
+
description: "User role updated",
|
|
80
|
+
content: { "application/json": { schema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: { user: { $ref: "#/components/schemas/User" } }
|
|
83
|
+
} } }
|
|
84
|
+
} }
|
|
85
|
+
},
|
|
86
|
+
$Infer: { body: {} }
|
|
87
|
+
}
|
|
88
|
+
}, async (ctx) => {
|
|
89
|
+
if (!hasPermission({
|
|
90
|
+
userId: ctx.context.session.user.id,
|
|
91
|
+
role: ctx.context.session.user.role,
|
|
92
|
+
options: opts,
|
|
93
|
+
permissions: { user: ["set-role"] }
|
|
94
|
+
})) throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE });
|
|
95
|
+
const roles = opts.roles;
|
|
96
|
+
if (roles) {
|
|
97
|
+
const inputRoles = Array.isArray(ctx.body.role) ? ctx.body.role : [ctx.body.role];
|
|
98
|
+
for (const role of inputRoles) if (!roles[role]) throw new APIError("BAD_REQUEST", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE });
|
|
99
|
+
}
|
|
100
|
+
const updatedUser = await ctx.context.internalAdapter.updateUser(ctx.body.userId, { role: parseRoles(ctx.body.role) });
|
|
101
|
+
return ctx.json({ user: updatedUser });
|
|
102
|
+
});
|
|
103
|
+
const getUserQuerySchema = z.object({ id: z.string().meta({ description: "The id of the User" }) });
|
|
104
|
+
const getUser = (opts) => createAuthEndpoint("/admin/get-user", {
|
|
105
|
+
method: "GET",
|
|
106
|
+
query: getUserQuerySchema,
|
|
107
|
+
use: [adminMiddleware],
|
|
108
|
+
metadata: { openapi: {
|
|
109
|
+
operationId: "getUser",
|
|
110
|
+
summary: "Get an existing user",
|
|
111
|
+
description: "Get an existing user",
|
|
112
|
+
responses: { 200: {
|
|
113
|
+
description: "User",
|
|
114
|
+
content: { "application/json": { schema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: { user: { $ref: "#/components/schemas/User" } }
|
|
117
|
+
} } }
|
|
118
|
+
} }
|
|
119
|
+
} }
|
|
120
|
+
}, async (ctx) => {
|
|
121
|
+
const { id } = ctx.query;
|
|
122
|
+
if (!hasPermission({
|
|
123
|
+
userId: ctx.context.session.user.id,
|
|
124
|
+
role: ctx.context.session.user.role,
|
|
125
|
+
options: opts,
|
|
126
|
+
permissions: { user: ["get"] }
|
|
127
|
+
})) throw ctx.error("FORBIDDEN", {
|
|
128
|
+
message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_GET_USER,
|
|
129
|
+
code: "YOU_ARE_NOT_ALLOWED_TO_GET_USER"
|
|
130
|
+
});
|
|
131
|
+
const user = await ctx.context.internalAdapter.findUserById(id);
|
|
132
|
+
if (!user) throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.USER_NOT_FOUND });
|
|
133
|
+
return parseUserOutput(ctx.context.options, user);
|
|
134
|
+
});
|
|
135
|
+
const createUserBodySchema = z.object({
|
|
136
|
+
email: z.string().meta({ description: "The email of the user" }),
|
|
137
|
+
password: z.string().meta({ description: "The password of the user" }),
|
|
138
|
+
name: z.string().meta({ description: "The name of the user" }),
|
|
139
|
+
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\"` }),
|
|
140
|
+
data: z.record(z.string(), z.any()).optional().meta({ description: "Extra fields for the user. Including custom additional fields." })
|
|
141
|
+
});
|
|
142
|
+
/**
|
|
143
|
+
* ### Endpoint
|
|
144
|
+
*
|
|
145
|
+
* POST `/admin/create-user`
|
|
146
|
+
*
|
|
147
|
+
* ### API Methods
|
|
148
|
+
*
|
|
149
|
+
* **server:**
|
|
150
|
+
* `auth.api.createUser`
|
|
151
|
+
*
|
|
152
|
+
* **client:**
|
|
153
|
+
* `authClient.admin.createUser`
|
|
154
|
+
*
|
|
155
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-create-user)
|
|
156
|
+
*/
|
|
157
|
+
const createUser = (opts) => createAuthEndpoint("/admin/create-user", {
|
|
158
|
+
method: "POST",
|
|
159
|
+
body: createUserBodySchema,
|
|
160
|
+
metadata: {
|
|
161
|
+
openapi: {
|
|
162
|
+
operationId: "createUser",
|
|
163
|
+
summary: "Create a new user",
|
|
164
|
+
description: "Create a new user",
|
|
165
|
+
responses: { 200: {
|
|
166
|
+
description: "User created",
|
|
167
|
+
content: { "application/json": { schema: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: { user: { $ref: "#/components/schemas/User" } }
|
|
170
|
+
} } }
|
|
171
|
+
} }
|
|
172
|
+
},
|
|
173
|
+
$Infer: { body: {} }
|
|
174
|
+
}
|
|
175
|
+
}, async (ctx) => {
|
|
176
|
+
const session = await getSessionFromCtx(ctx);
|
|
177
|
+
if (!session && (ctx.request || ctx.headers)) throw ctx.error("UNAUTHORIZED");
|
|
178
|
+
if (session) {
|
|
179
|
+
if (!hasPermission({
|
|
180
|
+
userId: session.user.id,
|
|
181
|
+
role: session.user.role,
|
|
182
|
+
options: opts,
|
|
183
|
+
permissions: { user: ["create"] }
|
|
184
|
+
})) throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS });
|
|
185
|
+
}
|
|
186
|
+
const email = ctx.body.email.toLowerCase();
|
|
187
|
+
if (!z.email().safeParse(email).success) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_EMAIL });
|
|
188
|
+
if (await ctx.context.internalAdapter.findUserByEmail(email)) throw new APIError("BAD_REQUEST", { message: ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL });
|
|
189
|
+
const user = await ctx.context.internalAdapter.createUser({
|
|
190
|
+
email,
|
|
191
|
+
name: ctx.body.name,
|
|
192
|
+
role: (ctx.body.role && parseRoles(ctx.body.role)) ?? opts?.defaultRole ?? "user",
|
|
193
|
+
...ctx.body.data
|
|
194
|
+
});
|
|
195
|
+
if (!user) throw new APIError("INTERNAL_SERVER_ERROR", { message: ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER });
|
|
196
|
+
const hashedPassword = await ctx.context.password.hash(ctx.body.password);
|
|
197
|
+
await ctx.context.internalAdapter.linkAccount({
|
|
198
|
+
accountId: user.id,
|
|
199
|
+
providerId: "credential",
|
|
200
|
+
password: hashedPassword,
|
|
201
|
+
userId: user.id
|
|
202
|
+
});
|
|
203
|
+
return ctx.json({ user });
|
|
204
|
+
});
|
|
205
|
+
const adminUpdateUserBodySchema = z.object({
|
|
206
|
+
userId: z.coerce.string().meta({ description: "The user id" }),
|
|
207
|
+
data: z.record(z.any(), z.any()).meta({ description: "The user data to update" })
|
|
208
|
+
});
|
|
209
|
+
/**
|
|
210
|
+
* ### Endpoint
|
|
211
|
+
*
|
|
212
|
+
* POST `/admin/update-user`
|
|
213
|
+
*
|
|
214
|
+
* ### API Methods
|
|
215
|
+
*
|
|
216
|
+
* **server:**
|
|
217
|
+
* `auth.api.adminUpdateUser`
|
|
218
|
+
*
|
|
219
|
+
* **client:**
|
|
220
|
+
* `authClient.admin.updateUser`
|
|
221
|
+
*
|
|
222
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-update-user)
|
|
223
|
+
*/
|
|
224
|
+
const adminUpdateUser = (opts) => createAuthEndpoint("/admin/update-user", {
|
|
225
|
+
method: "POST",
|
|
226
|
+
body: adminUpdateUserBodySchema,
|
|
227
|
+
use: [adminMiddleware],
|
|
228
|
+
metadata: { openapi: {
|
|
229
|
+
operationId: "updateUser",
|
|
230
|
+
summary: "Update a user",
|
|
231
|
+
description: "Update a user's details",
|
|
232
|
+
responses: { 200: {
|
|
233
|
+
description: "User updated",
|
|
234
|
+
content: { "application/json": { schema: {
|
|
235
|
+
type: "object",
|
|
236
|
+
properties: { user: { $ref: "#/components/schemas/User" } }
|
|
237
|
+
} } }
|
|
238
|
+
} }
|
|
239
|
+
} }
|
|
240
|
+
}, async (ctx) => {
|
|
241
|
+
if (!hasPermission({
|
|
242
|
+
userId: ctx.context.session.user.id,
|
|
243
|
+
role: ctx.context.session.user.role,
|
|
244
|
+
options: opts,
|
|
245
|
+
permissions: { user: ["update"] }
|
|
246
|
+
})) throw ctx.error("FORBIDDEN", {
|
|
247
|
+
message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS,
|
|
248
|
+
code: "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS"
|
|
249
|
+
});
|
|
250
|
+
if (Object.keys(ctx.body.data).length === 0) throw new APIError("BAD_REQUEST", { message: ADMIN_ERROR_CODES.NO_DATA_TO_UPDATE });
|
|
251
|
+
if (ctx.body.data?.role) ctx.body.data.role = parseRoles(ctx.body.data.role);
|
|
252
|
+
const updatedUser = await ctx.context.internalAdapter.updateUser(ctx.body.userId, ctx.body.data);
|
|
253
|
+
return ctx.json(updatedUser);
|
|
254
|
+
});
|
|
255
|
+
const listUsersQuerySchema = z.object({
|
|
256
|
+
searchValue: z.string().optional().meta({ description: "The value to search for. Eg: \"some name\"" }),
|
|
257
|
+
searchField: z.enum(["email", "name"]).meta({ description: "The field to search in, defaults to email. Can be `email` or `name`. Eg: \"name\"" }).optional(),
|
|
258
|
+
searchOperator: z.enum([
|
|
259
|
+
"contains",
|
|
260
|
+
"starts_with",
|
|
261
|
+
"ends_with"
|
|
262
|
+
]).meta({ description: "The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`. Eg: \"contains\"" }).optional(),
|
|
263
|
+
limit: z.string().meta({ description: "The number of users to return" }).or(z.number()).optional(),
|
|
264
|
+
offset: z.string().meta({ description: "The offset to start from" }).or(z.number()).optional(),
|
|
265
|
+
sortBy: z.string().meta({ description: "The field to sort by" }).optional(),
|
|
266
|
+
sortDirection: z.enum(["asc", "desc"]).meta({ description: "The direction to sort by" }).optional(),
|
|
267
|
+
filterField: z.string().meta({ description: "The field to filter by" }).optional(),
|
|
268
|
+
filterValue: z.string().meta({ description: "The value to filter by" }).or(z.number()).or(z.boolean()).optional(),
|
|
269
|
+
filterOperator: z.enum([
|
|
270
|
+
"eq",
|
|
271
|
+
"ne",
|
|
272
|
+
"lt",
|
|
273
|
+
"lte",
|
|
274
|
+
"gt",
|
|
275
|
+
"gte",
|
|
276
|
+
"contains"
|
|
277
|
+
]).meta({ description: "The operator to use for the filter" }).optional()
|
|
278
|
+
});
|
|
279
|
+
const listUsers = (opts) => createAuthEndpoint("/admin/list-users", {
|
|
280
|
+
method: "GET",
|
|
281
|
+
use: [adminMiddleware],
|
|
282
|
+
query: listUsersQuerySchema,
|
|
283
|
+
metadata: { openapi: {
|
|
284
|
+
operationId: "listUsers",
|
|
285
|
+
summary: "List users",
|
|
286
|
+
description: "List users",
|
|
287
|
+
responses: { 200: {
|
|
288
|
+
description: "List of users",
|
|
289
|
+
content: { "application/json": { schema: {
|
|
290
|
+
type: "object",
|
|
291
|
+
properties: {
|
|
292
|
+
users: {
|
|
293
|
+
type: "array",
|
|
294
|
+
items: { $ref: "#/components/schemas/User" }
|
|
295
|
+
},
|
|
296
|
+
total: { type: "number" },
|
|
297
|
+
limit: { type: "number" },
|
|
298
|
+
offset: { type: "number" }
|
|
299
|
+
},
|
|
300
|
+
required: ["users", "total"]
|
|
301
|
+
} } }
|
|
302
|
+
} }
|
|
303
|
+
} }
|
|
304
|
+
}, async (ctx) => {
|
|
305
|
+
const session = ctx.context.session;
|
|
306
|
+
if (!hasPermission({
|
|
307
|
+
userId: ctx.context.session.user.id,
|
|
308
|
+
role: session.user.role,
|
|
309
|
+
options: opts,
|
|
310
|
+
permissions: { user: ["list"] }
|
|
311
|
+
})) throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_USERS });
|
|
312
|
+
const where = [];
|
|
313
|
+
if (ctx.query?.searchValue) where.push({
|
|
314
|
+
field: ctx.query.searchField || "email",
|
|
315
|
+
operator: ctx.query.searchOperator || "contains",
|
|
316
|
+
value: ctx.query.searchValue
|
|
317
|
+
});
|
|
318
|
+
if (ctx.query?.filterValue) where.push({
|
|
319
|
+
field: ctx.query.filterField || "email",
|
|
320
|
+
operator: ctx.query.filterOperator || "eq",
|
|
321
|
+
value: ctx.query.filterValue
|
|
322
|
+
});
|
|
323
|
+
try {
|
|
324
|
+
const users = await ctx.context.internalAdapter.listUsers(Number(ctx.query?.limit) || void 0, Number(ctx.query?.offset) || void 0, ctx.query?.sortBy ? {
|
|
325
|
+
field: ctx.query.sortBy,
|
|
326
|
+
direction: ctx.query.sortDirection || "asc"
|
|
327
|
+
} : void 0, where.length ? where : void 0);
|
|
328
|
+
const total = await ctx.context.internalAdapter.countTotalUsers(where.length ? where : void 0);
|
|
329
|
+
return ctx.json({
|
|
330
|
+
users,
|
|
331
|
+
total,
|
|
332
|
+
limit: Number(ctx.query?.limit) || void 0,
|
|
333
|
+
offset: Number(ctx.query?.offset) || void 0
|
|
334
|
+
});
|
|
335
|
+
} catch (e) {
|
|
336
|
+
return ctx.json({
|
|
337
|
+
users: [],
|
|
338
|
+
total: 0
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
const listUserSessionsBodySchema = z.object({ userId: z.coerce.string().meta({ description: "The user id" }) });
|
|
343
|
+
/**
|
|
344
|
+
* ### Endpoint
|
|
345
|
+
*
|
|
346
|
+
* POST `/admin/list-user-sessions`
|
|
347
|
+
*
|
|
348
|
+
* ### API Methods
|
|
349
|
+
*
|
|
350
|
+
* **server:**
|
|
351
|
+
* `auth.api.listUserSessions`
|
|
352
|
+
*
|
|
353
|
+
* **client:**
|
|
354
|
+
* `authClient.admin.listUserSessions`
|
|
355
|
+
*
|
|
356
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-list-user-sessions)
|
|
357
|
+
*/
|
|
358
|
+
const listUserSessions = (opts) => createAuthEndpoint("/admin/list-user-sessions", {
|
|
359
|
+
method: "POST",
|
|
360
|
+
use: [adminMiddleware],
|
|
361
|
+
body: listUserSessionsBodySchema,
|
|
362
|
+
metadata: { openapi: {
|
|
363
|
+
operationId: "listUserSessions",
|
|
364
|
+
summary: "List user sessions",
|
|
365
|
+
description: "List user sessions",
|
|
366
|
+
responses: { 200: {
|
|
367
|
+
description: "List of user sessions",
|
|
368
|
+
content: { "application/json": { schema: {
|
|
369
|
+
type: "object",
|
|
370
|
+
properties: { sessions: {
|
|
371
|
+
type: "array",
|
|
372
|
+
items: { $ref: "#/components/schemas/Session" }
|
|
373
|
+
} }
|
|
374
|
+
} } }
|
|
375
|
+
} }
|
|
376
|
+
} }
|
|
377
|
+
}, async (ctx) => {
|
|
378
|
+
const session = ctx.context.session;
|
|
379
|
+
if (!hasPermission({
|
|
380
|
+
userId: ctx.context.session.user.id,
|
|
381
|
+
role: session.user.role,
|
|
382
|
+
options: opts,
|
|
383
|
+
permissions: { session: ["list"] }
|
|
384
|
+
})) throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS });
|
|
385
|
+
return { sessions: await ctx.context.internalAdapter.listSessions(ctx.body.userId) };
|
|
386
|
+
});
|
|
387
|
+
const unbanUserBodySchema = z.object({ userId: z.coerce.string().meta({ description: "The user id" }) });
|
|
388
|
+
/**
|
|
389
|
+
* ### Endpoint
|
|
390
|
+
*
|
|
391
|
+
* POST `/admin/unban-user`
|
|
392
|
+
*
|
|
393
|
+
* ### API Methods
|
|
394
|
+
*
|
|
395
|
+
* **server:**
|
|
396
|
+
* `auth.api.unbanUser`
|
|
397
|
+
*
|
|
398
|
+
* **client:**
|
|
399
|
+
* `authClient.admin.unbanUser`
|
|
400
|
+
*
|
|
401
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-unban-user)
|
|
402
|
+
*/
|
|
403
|
+
const unbanUser = (opts) => createAuthEndpoint("/admin/unban-user", {
|
|
404
|
+
method: "POST",
|
|
405
|
+
body: unbanUserBodySchema,
|
|
406
|
+
use: [adminMiddleware],
|
|
407
|
+
metadata: { openapi: {
|
|
408
|
+
operationId: "unbanUser",
|
|
409
|
+
summary: "Unban a user",
|
|
410
|
+
description: "Unban a user",
|
|
411
|
+
responses: { 200: {
|
|
412
|
+
description: "User unbanned",
|
|
413
|
+
content: { "application/json": { schema: {
|
|
414
|
+
type: "object",
|
|
415
|
+
properties: { user: { $ref: "#/components/schemas/User" } }
|
|
416
|
+
} } }
|
|
417
|
+
} }
|
|
418
|
+
} }
|
|
419
|
+
}, async (ctx) => {
|
|
420
|
+
const session = ctx.context.session;
|
|
421
|
+
if (!hasPermission({
|
|
422
|
+
userId: ctx.context.session.user.id,
|
|
423
|
+
role: session.user.role,
|
|
424
|
+
options: opts,
|
|
425
|
+
permissions: { user: ["ban"] }
|
|
426
|
+
})) throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS });
|
|
427
|
+
const user = await ctx.context.internalAdapter.updateUser(ctx.body.userId, {
|
|
428
|
+
banned: false,
|
|
429
|
+
banExpires: null,
|
|
430
|
+
banReason: null,
|
|
431
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
432
|
+
});
|
|
433
|
+
return ctx.json({ user });
|
|
434
|
+
});
|
|
435
|
+
const banUserBodySchema = z.object({
|
|
436
|
+
userId: z.coerce.string().meta({ description: "The user id" }),
|
|
437
|
+
banReason: z.string().meta({ description: "The reason for the ban" }).optional(),
|
|
438
|
+
banExpiresIn: z.number().meta({ description: "The number of seconds until the ban expires" }).optional()
|
|
439
|
+
});
|
|
440
|
+
/**
|
|
441
|
+
* ### Endpoint
|
|
442
|
+
*
|
|
443
|
+
* POST `/admin/ban-user`
|
|
444
|
+
*
|
|
445
|
+
* ### API Methods
|
|
446
|
+
*
|
|
447
|
+
* **server:**
|
|
448
|
+
* `auth.api.banUser`
|
|
449
|
+
*
|
|
450
|
+
* **client:**
|
|
451
|
+
* `authClient.admin.banUser`
|
|
452
|
+
*
|
|
453
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-ban-user)
|
|
454
|
+
*/
|
|
455
|
+
const banUser = (opts) => createAuthEndpoint("/admin/ban-user", {
|
|
456
|
+
method: "POST",
|
|
457
|
+
body: banUserBodySchema,
|
|
458
|
+
use: [adminMiddleware],
|
|
459
|
+
metadata: { openapi: {
|
|
460
|
+
operationId: "banUser",
|
|
461
|
+
summary: "Ban a user",
|
|
462
|
+
description: "Ban a user",
|
|
463
|
+
responses: { 200: {
|
|
464
|
+
description: "User banned",
|
|
465
|
+
content: { "application/json": { schema: {
|
|
466
|
+
type: "object",
|
|
467
|
+
properties: { user: { $ref: "#/components/schemas/User" } }
|
|
468
|
+
} } }
|
|
469
|
+
} }
|
|
470
|
+
} }
|
|
471
|
+
}, async (ctx) => {
|
|
472
|
+
const session = ctx.context.session;
|
|
473
|
+
if (!hasPermission({
|
|
474
|
+
userId: ctx.context.session.user.id,
|
|
475
|
+
role: session.user.role,
|
|
476
|
+
options: opts,
|
|
477
|
+
permissions: { user: ["ban"] }
|
|
478
|
+
})) throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS });
|
|
479
|
+
if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.USER_NOT_FOUND });
|
|
480
|
+
if (ctx.body.userId === ctx.context.session.user.id) throw new APIError("BAD_REQUEST", { message: ADMIN_ERROR_CODES.YOU_CANNOT_BAN_YOURSELF });
|
|
481
|
+
const user = await ctx.context.internalAdapter.updateUser(ctx.body.userId, {
|
|
482
|
+
banned: true,
|
|
483
|
+
banReason: ctx.body.banReason || opts?.defaultBanReason || "No reason",
|
|
484
|
+
banExpires: ctx.body.banExpiresIn ? getDate(ctx.body.banExpiresIn, "sec") : opts?.defaultBanExpiresIn ? getDate(opts.defaultBanExpiresIn, "sec") : void 0,
|
|
485
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
486
|
+
});
|
|
487
|
+
await ctx.context.internalAdapter.deleteSessions(ctx.body.userId);
|
|
488
|
+
return ctx.json({ user });
|
|
489
|
+
});
|
|
490
|
+
const impersonateUserBodySchema = z.object({ userId: z.coerce.string().meta({ description: "The user id" }) });
|
|
491
|
+
/**
|
|
492
|
+
* ### Endpoint
|
|
493
|
+
*
|
|
494
|
+
* POST `/admin/impersonate-user`
|
|
495
|
+
*
|
|
496
|
+
* ### API Methods
|
|
497
|
+
*
|
|
498
|
+
* **server:**
|
|
499
|
+
* `auth.api.impersonateUser`
|
|
500
|
+
*
|
|
501
|
+
* **client:**
|
|
502
|
+
* `authClient.admin.impersonateUser`
|
|
503
|
+
*
|
|
504
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-impersonate-user)
|
|
505
|
+
*/
|
|
506
|
+
const impersonateUser = (opts) => createAuthEndpoint("/admin/impersonate-user", {
|
|
507
|
+
method: "POST",
|
|
508
|
+
body: impersonateUserBodySchema,
|
|
509
|
+
use: [adminMiddleware],
|
|
510
|
+
metadata: { openapi: {
|
|
511
|
+
operationId: "impersonateUser",
|
|
512
|
+
summary: "Impersonate a user",
|
|
513
|
+
description: "Impersonate a user",
|
|
514
|
+
responses: { 200: {
|
|
515
|
+
description: "Impersonation session created",
|
|
516
|
+
content: { "application/json": { schema: {
|
|
517
|
+
type: "object",
|
|
518
|
+
properties: {
|
|
519
|
+
session: { $ref: "#/components/schemas/Session" },
|
|
520
|
+
user: { $ref: "#/components/schemas/User" }
|
|
521
|
+
}
|
|
522
|
+
} } }
|
|
523
|
+
} }
|
|
524
|
+
} }
|
|
525
|
+
}, async (ctx) => {
|
|
526
|
+
if (!hasPermission({
|
|
527
|
+
userId: ctx.context.session.user.id,
|
|
528
|
+
role: ctx.context.session.user.role,
|
|
529
|
+
options: opts,
|
|
530
|
+
permissions: { user: ["impersonate"] }
|
|
531
|
+
})) throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS });
|
|
532
|
+
const targetUser = await ctx.context.internalAdapter.findUserById(ctx.body.userId);
|
|
533
|
+
if (!targetUser) throw new APIError("NOT_FOUND", { message: "User not found" });
|
|
534
|
+
const session = await ctx.context.internalAdapter.createSession(targetUser.id, true, {
|
|
535
|
+
impersonatedBy: ctx.context.session.user.id,
|
|
536
|
+
expiresAt: opts?.impersonationSessionDuration ? getDate(opts.impersonationSessionDuration, "sec") : getDate(3600, "sec")
|
|
537
|
+
}, true);
|
|
538
|
+
if (!session) throw new APIError("INTERNAL_SERVER_ERROR", { message: ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER });
|
|
539
|
+
const authCookies = ctx.context.authCookies;
|
|
540
|
+
deleteSessionCookie(ctx);
|
|
541
|
+
const dontRememberMeCookie = await ctx.getSignedCookie(ctx.context.authCookies.dontRememberToken.name, ctx.context.secret);
|
|
542
|
+
const adminCookieProp = ctx.context.createAuthCookie("admin_session");
|
|
543
|
+
await ctx.setSignedCookie(adminCookieProp.name, `${ctx.context.session.session.token}:${dontRememberMeCookie || ""}`, ctx.context.secret, authCookies.sessionToken.options);
|
|
544
|
+
await setSessionCookie(ctx, {
|
|
545
|
+
session,
|
|
546
|
+
user: targetUser
|
|
547
|
+
}, true);
|
|
548
|
+
return ctx.json({
|
|
549
|
+
session,
|
|
550
|
+
user: targetUser
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
/**
|
|
554
|
+
* ### Endpoint
|
|
555
|
+
*
|
|
556
|
+
* POST `/admin/stop-impersonating`
|
|
557
|
+
*
|
|
558
|
+
* ### API Methods
|
|
559
|
+
*
|
|
560
|
+
* **server:**
|
|
561
|
+
* `auth.api.stopImpersonating`
|
|
562
|
+
*
|
|
563
|
+
* **client:**
|
|
564
|
+
* `authClient.admin.stopImpersonating`
|
|
565
|
+
*
|
|
566
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-stop-impersonating)
|
|
567
|
+
*/
|
|
568
|
+
const stopImpersonating = () => createAuthEndpoint("/admin/stop-impersonating", {
|
|
569
|
+
method: "POST",
|
|
570
|
+
requireHeaders: true
|
|
571
|
+
}, async (ctx) => {
|
|
572
|
+
const session = await getSessionFromCtx(ctx);
|
|
573
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
574
|
+
if (!session.session.impersonatedBy) throw new APIError("BAD_REQUEST", { message: "You are not impersonating anyone" });
|
|
575
|
+
const user = await ctx.context.internalAdapter.findUserById(session.session.impersonatedBy);
|
|
576
|
+
if (!user) throw new APIError("INTERNAL_SERVER_ERROR", { message: "Failed to find user" });
|
|
577
|
+
const adminCookieName = ctx.context.createAuthCookie("admin_session").name;
|
|
578
|
+
const adminCookie = await ctx.getSignedCookie(adminCookieName, ctx.context.secret);
|
|
579
|
+
if (!adminCookie) throw new APIError("INTERNAL_SERVER_ERROR", { message: "Failed to find admin session" });
|
|
580
|
+
const [adminSessionToken, dontRememberMeCookie] = adminCookie?.split(":");
|
|
581
|
+
const adminSession = await ctx.context.internalAdapter.findSession(adminSessionToken);
|
|
582
|
+
if (!adminSession || adminSession.session.userId !== user.id) throw new APIError("INTERNAL_SERVER_ERROR", { message: "Failed to find admin session" });
|
|
583
|
+
await ctx.context.internalAdapter.deleteSession(session.session.token);
|
|
584
|
+
await setSessionCookie(ctx, adminSession, !!dontRememberMeCookie);
|
|
585
|
+
return ctx.json(adminSession);
|
|
586
|
+
});
|
|
587
|
+
const revokeUserSessionBodySchema = z.object({ sessionToken: z.string().meta({ description: "The session token" }) });
|
|
588
|
+
/**
|
|
589
|
+
* ### Endpoint
|
|
590
|
+
*
|
|
591
|
+
* POST `/admin/revoke-user-session`
|
|
592
|
+
*
|
|
593
|
+
* ### API Methods
|
|
594
|
+
*
|
|
595
|
+
* **server:**
|
|
596
|
+
* `auth.api.revokeUserSession`
|
|
597
|
+
*
|
|
598
|
+
* **client:**
|
|
599
|
+
* `authClient.admin.revokeUserSession`
|
|
600
|
+
*
|
|
601
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-revoke-user-session)
|
|
602
|
+
*/
|
|
603
|
+
const revokeUserSession = (opts) => createAuthEndpoint("/admin/revoke-user-session", {
|
|
604
|
+
method: "POST",
|
|
605
|
+
body: revokeUserSessionBodySchema,
|
|
606
|
+
use: [adminMiddleware],
|
|
607
|
+
metadata: { openapi: {
|
|
608
|
+
operationId: "revokeUserSession",
|
|
609
|
+
summary: "Revoke a user session",
|
|
610
|
+
description: "Revoke a user session",
|
|
611
|
+
responses: { 200: {
|
|
612
|
+
description: "Session revoked",
|
|
613
|
+
content: { "application/json": { schema: {
|
|
614
|
+
type: "object",
|
|
615
|
+
properties: { success: { type: "boolean" } }
|
|
616
|
+
} } }
|
|
617
|
+
} }
|
|
618
|
+
} }
|
|
619
|
+
}, async (ctx) => {
|
|
620
|
+
const session = ctx.context.session;
|
|
621
|
+
if (!hasPermission({
|
|
622
|
+
userId: ctx.context.session.user.id,
|
|
623
|
+
role: session.user.role,
|
|
624
|
+
options: opts,
|
|
625
|
+
permissions: { session: ["revoke"] }
|
|
626
|
+
})) throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS });
|
|
627
|
+
await ctx.context.internalAdapter.deleteSession(ctx.body.sessionToken);
|
|
628
|
+
return ctx.json({ success: true });
|
|
629
|
+
});
|
|
630
|
+
const revokeUserSessionsBodySchema = z.object({ userId: z.coerce.string().meta({ description: "The user id" }) });
|
|
631
|
+
/**
|
|
632
|
+
* ### Endpoint
|
|
633
|
+
*
|
|
634
|
+
* POST `/admin/revoke-user-sessions`
|
|
635
|
+
*
|
|
636
|
+
* ### API Methods
|
|
637
|
+
*
|
|
638
|
+
* **server:**
|
|
639
|
+
* `auth.api.revokeUserSessions`
|
|
640
|
+
*
|
|
641
|
+
* **client:**
|
|
642
|
+
* `authClient.admin.revokeUserSessions`
|
|
643
|
+
*
|
|
644
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-revoke-user-sessions)
|
|
645
|
+
*/
|
|
646
|
+
const revokeUserSessions = (opts) => createAuthEndpoint("/admin/revoke-user-sessions", {
|
|
647
|
+
method: "POST",
|
|
648
|
+
body: revokeUserSessionsBodySchema,
|
|
649
|
+
use: [adminMiddleware],
|
|
650
|
+
metadata: { openapi: {
|
|
651
|
+
operationId: "revokeUserSessions",
|
|
652
|
+
summary: "Revoke all user sessions",
|
|
653
|
+
description: "Revoke all user sessions",
|
|
654
|
+
responses: { 200: {
|
|
655
|
+
description: "Sessions revoked",
|
|
656
|
+
content: { "application/json": { schema: {
|
|
657
|
+
type: "object",
|
|
658
|
+
properties: { success: { type: "boolean" } }
|
|
659
|
+
} } }
|
|
660
|
+
} }
|
|
661
|
+
} }
|
|
662
|
+
}, async (ctx) => {
|
|
663
|
+
const session = ctx.context.session;
|
|
664
|
+
if (!hasPermission({
|
|
665
|
+
userId: ctx.context.session.user.id,
|
|
666
|
+
role: session.user.role,
|
|
667
|
+
options: opts,
|
|
668
|
+
permissions: { session: ["revoke"] }
|
|
669
|
+
})) throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS });
|
|
670
|
+
await ctx.context.internalAdapter.deleteSessions(ctx.body.userId);
|
|
671
|
+
return ctx.json({ success: true });
|
|
672
|
+
});
|
|
673
|
+
const removeUserBodySchema = z.object({ userId: z.coerce.string().meta({ description: "The user id" }) });
|
|
674
|
+
/**
|
|
675
|
+
* ### Endpoint
|
|
676
|
+
*
|
|
677
|
+
* POST `/admin/remove-user`
|
|
678
|
+
*
|
|
679
|
+
* ### API Methods
|
|
680
|
+
*
|
|
681
|
+
* **server:**
|
|
682
|
+
* `auth.api.removeUser`
|
|
683
|
+
*
|
|
684
|
+
* **client:**
|
|
685
|
+
* `authClient.admin.removeUser`
|
|
686
|
+
*
|
|
687
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-remove-user)
|
|
688
|
+
*/
|
|
689
|
+
const removeUser = (opts) => createAuthEndpoint("/admin/remove-user", {
|
|
690
|
+
method: "POST",
|
|
691
|
+
body: removeUserBodySchema,
|
|
692
|
+
use: [adminMiddleware],
|
|
693
|
+
metadata: { openapi: {
|
|
694
|
+
operationId: "removeUser",
|
|
695
|
+
summary: "Remove a user",
|
|
696
|
+
description: "Delete a user and all their sessions and accounts. Cannot be undone.",
|
|
697
|
+
responses: { 200: {
|
|
698
|
+
description: "User removed",
|
|
699
|
+
content: { "application/json": { schema: {
|
|
700
|
+
type: "object",
|
|
701
|
+
properties: { success: { type: "boolean" } }
|
|
702
|
+
} } }
|
|
703
|
+
} }
|
|
704
|
+
} }
|
|
705
|
+
}, async (ctx) => {
|
|
706
|
+
const session = ctx.context.session;
|
|
707
|
+
if (!hasPermission({
|
|
708
|
+
userId: ctx.context.session.user.id,
|
|
709
|
+
role: session.user.role,
|
|
710
|
+
options: opts,
|
|
711
|
+
permissions: { user: ["delete"] }
|
|
712
|
+
})) throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS });
|
|
713
|
+
if (ctx.body.userId === ctx.context.session.user.id) throw new APIError("BAD_REQUEST", { message: ADMIN_ERROR_CODES.YOU_CANNOT_REMOVE_YOURSELF });
|
|
714
|
+
if (!await ctx.context.internalAdapter.findUserById(ctx.body.userId)) throw new APIError("NOT_FOUND", { message: "User not found" });
|
|
715
|
+
await ctx.context.internalAdapter.deleteUser(ctx.body.userId);
|
|
716
|
+
return ctx.json({ success: true });
|
|
717
|
+
});
|
|
718
|
+
const setUserPasswordBodySchema = z.object({
|
|
719
|
+
newPassword: z.string().nonempty("newPassword cannot be empty").meta({ description: "The new password" }),
|
|
720
|
+
userId: z.coerce.string().nonempty("userId cannot be empty").meta({ description: "The user id" })
|
|
721
|
+
});
|
|
722
|
+
/**
|
|
723
|
+
* ### Endpoint
|
|
724
|
+
*
|
|
725
|
+
* POST `/admin/set-user-password`
|
|
726
|
+
*
|
|
727
|
+
* ### API Methods
|
|
728
|
+
*
|
|
729
|
+
* **server:**
|
|
730
|
+
* `auth.api.setUserPassword`
|
|
731
|
+
*
|
|
732
|
+
* **client:**
|
|
733
|
+
* `authClient.admin.setUserPassword`
|
|
734
|
+
*
|
|
735
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-set-user-password)
|
|
736
|
+
*/
|
|
737
|
+
const setUserPassword = (opts) => createAuthEndpoint("/admin/set-user-password", {
|
|
738
|
+
method: "POST",
|
|
739
|
+
body: setUserPasswordBodySchema,
|
|
740
|
+
use: [adminMiddleware],
|
|
741
|
+
metadata: { openapi: {
|
|
742
|
+
operationId: "setUserPassword",
|
|
743
|
+
summary: "Set a user's password",
|
|
744
|
+
description: "Set a user's password",
|
|
745
|
+
responses: { 200: {
|
|
746
|
+
description: "Password set",
|
|
747
|
+
content: { "application/json": { schema: {
|
|
748
|
+
type: "object",
|
|
749
|
+
properties: { status: { type: "boolean" } }
|
|
750
|
+
} } }
|
|
751
|
+
} }
|
|
752
|
+
} }
|
|
753
|
+
}, async (ctx) => {
|
|
754
|
+
if (!hasPermission({
|
|
755
|
+
userId: ctx.context.session.user.id,
|
|
756
|
+
role: ctx.context.session.user.role,
|
|
757
|
+
options: opts,
|
|
758
|
+
permissions: { user: ["set-password"] }
|
|
759
|
+
})) throw new APIError("FORBIDDEN", { message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD });
|
|
760
|
+
const { newPassword, userId } = ctx.body;
|
|
761
|
+
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
|
762
|
+
if (newPassword.length < minPasswordLength) {
|
|
763
|
+
ctx.context.logger.error("Password is too short");
|
|
764
|
+
throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT });
|
|
765
|
+
}
|
|
766
|
+
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
767
|
+
if (newPassword.length > maxPasswordLength) {
|
|
768
|
+
ctx.context.logger.error("Password is too long");
|
|
769
|
+
throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_LONG });
|
|
770
|
+
}
|
|
771
|
+
const hashedPassword = await ctx.context.password.hash(newPassword);
|
|
772
|
+
await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
|
|
773
|
+
return ctx.json({ status: true });
|
|
774
|
+
});
|
|
775
|
+
const userHasPermissionBodySchema = z.object({
|
|
776
|
+
userId: z.coerce.string().optional().meta({ description: `The user id. Eg: "user-id"` }),
|
|
777
|
+
role: z.string().optional().meta({ description: `The role to check permission for. Eg: "admin"` })
|
|
778
|
+
}).and(z.union([z.object({
|
|
779
|
+
permission: z.record(z.string(), z.array(z.string())),
|
|
780
|
+
permissions: z.undefined()
|
|
781
|
+
}), z.object({
|
|
782
|
+
permission: z.undefined(),
|
|
783
|
+
permissions: z.record(z.string(), z.array(z.string()))
|
|
784
|
+
})]));
|
|
785
|
+
/**
|
|
786
|
+
* ### Endpoint
|
|
787
|
+
*
|
|
788
|
+
* POST `/admin/has-permission`
|
|
789
|
+
*
|
|
790
|
+
* ### API Methods
|
|
791
|
+
*
|
|
792
|
+
* **server:**
|
|
793
|
+
* `auth.api.userHasPermission`
|
|
794
|
+
*
|
|
795
|
+
* **client:**
|
|
796
|
+
* `authClient.admin.hasPermission`
|
|
797
|
+
*
|
|
798
|
+
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-has-permission)
|
|
799
|
+
*/
|
|
800
|
+
const userHasPermission = (opts) => {
|
|
801
|
+
return createAuthEndpoint("/admin/has-permission", {
|
|
802
|
+
method: "POST",
|
|
803
|
+
body: userHasPermissionBodySchema,
|
|
804
|
+
metadata: {
|
|
805
|
+
openapi: {
|
|
806
|
+
description: "Check if the user has permission",
|
|
807
|
+
requestBody: { content: { "application/json": { schema: {
|
|
808
|
+
type: "object",
|
|
809
|
+
properties: {
|
|
810
|
+
permission: {
|
|
811
|
+
type: "object",
|
|
812
|
+
description: "The permission to check",
|
|
813
|
+
deprecated: true
|
|
814
|
+
},
|
|
815
|
+
permissions: {
|
|
816
|
+
type: "object",
|
|
817
|
+
description: "The permission to check"
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
required: ["permissions"]
|
|
821
|
+
} } } },
|
|
822
|
+
responses: { "200": {
|
|
823
|
+
description: "Success",
|
|
824
|
+
content: { "application/json": { schema: {
|
|
825
|
+
type: "object",
|
|
826
|
+
properties: {
|
|
827
|
+
error: { type: "string" },
|
|
828
|
+
success: { type: "boolean" }
|
|
829
|
+
},
|
|
830
|
+
required: ["success"]
|
|
831
|
+
} } }
|
|
832
|
+
} }
|
|
833
|
+
},
|
|
834
|
+
$Infer: { body: {} }
|
|
835
|
+
}
|
|
836
|
+
}, async (ctx) => {
|
|
837
|
+
if (!ctx.body?.permission && !ctx.body?.permissions) throw new APIError("BAD_REQUEST", { message: "invalid permission check. no permission(s) were passed." });
|
|
838
|
+
const session = await getSessionFromCtx(ctx);
|
|
839
|
+
if (!session && (ctx.request || ctx.headers)) throw new APIError("UNAUTHORIZED");
|
|
840
|
+
if (!session && !ctx.body.userId && !ctx.body.role) throw new APIError("BAD_REQUEST", { message: "user id or role is required" });
|
|
841
|
+
const user = session?.user || (ctx.body.role ? {
|
|
842
|
+
id: ctx.body.userId || "",
|
|
843
|
+
role: ctx.body.role
|
|
844
|
+
} : null) || await ctx.context.internalAdapter.findUserById(ctx.body.userId);
|
|
845
|
+
if (!user) throw new APIError("BAD_REQUEST", { message: "user not found" });
|
|
846
|
+
const result = hasPermission({
|
|
847
|
+
userId: user.id,
|
|
848
|
+
role: user.role,
|
|
849
|
+
options: opts,
|
|
850
|
+
permissions: ctx.body.permissions ?? ctx.body.permission
|
|
851
|
+
});
|
|
852
|
+
return ctx.json({
|
|
853
|
+
error: null,
|
|
854
|
+
success: result
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
//#endregion
|
|
860
|
+
//#region src/plugins/admin/schema.ts
|
|
861
|
+
const schema = {
|
|
862
|
+
user: { fields: {
|
|
863
|
+
role: {
|
|
864
|
+
type: "string",
|
|
865
|
+
required: false,
|
|
866
|
+
input: false
|
|
867
|
+
},
|
|
868
|
+
banned: {
|
|
869
|
+
type: "boolean",
|
|
870
|
+
defaultValue: false,
|
|
871
|
+
required: false,
|
|
872
|
+
input: false
|
|
873
|
+
},
|
|
874
|
+
banReason: {
|
|
875
|
+
type: "string",
|
|
876
|
+
required: false,
|
|
877
|
+
input: false
|
|
878
|
+
},
|
|
879
|
+
banExpires: {
|
|
880
|
+
type: "date",
|
|
881
|
+
required: false,
|
|
882
|
+
input: false
|
|
883
|
+
}
|
|
884
|
+
} },
|
|
885
|
+
session: { fields: { impersonatedBy: {
|
|
886
|
+
type: "string",
|
|
887
|
+
required: false
|
|
888
|
+
} } }
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
//#endregion
|
|
892
|
+
//#region src/plugins/admin/admin.ts
|
|
893
|
+
const admin = (options) => {
|
|
894
|
+
const opts = {
|
|
895
|
+
defaultRole: options?.defaultRole ?? "user",
|
|
896
|
+
adminRoles: options?.adminRoles ?? ["admin"],
|
|
897
|
+
bannedUserMessage: options?.bannedUserMessage ?? "You have been banned from this application. Please contact support if you believe this is an error.",
|
|
898
|
+
...options
|
|
899
|
+
};
|
|
900
|
+
return {
|
|
901
|
+
id: "admin",
|
|
902
|
+
init() {
|
|
903
|
+
return { options: { databaseHooks: {
|
|
904
|
+
user: { create: { async before(user) {
|
|
905
|
+
return { data: {
|
|
906
|
+
role: options?.defaultRole ?? "user",
|
|
907
|
+
...user
|
|
908
|
+
} };
|
|
909
|
+
} } },
|
|
910
|
+
session: { create: { async before(session, ctx) {
|
|
911
|
+
if (!ctx) return;
|
|
912
|
+
const user = await ctx.context.internalAdapter.findUserById(session.userId);
|
|
913
|
+
if (user.banned) {
|
|
914
|
+
if (user.banExpires && new Date(user.banExpires).getTime() < Date.now()) {
|
|
915
|
+
await ctx.context.internalAdapter.updateUser(session.userId, {
|
|
916
|
+
banned: false,
|
|
917
|
+
banReason: null,
|
|
918
|
+
banExpires: null
|
|
919
|
+
});
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (ctx && (ctx.path.startsWith("/callback") || ctx.path.startsWith("/oauth2/callback"))) {
|
|
923
|
+
const redirectURI = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
|
|
924
|
+
throw ctx.redirect(`${redirectURI}?error=banned&error_description=${opts.bannedUserMessage}`);
|
|
925
|
+
}
|
|
926
|
+
throw new APIError("FORBIDDEN", {
|
|
927
|
+
message: opts.bannedUserMessage,
|
|
928
|
+
code: "BANNED_USER"
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
} } }
|
|
932
|
+
} } };
|
|
933
|
+
},
|
|
934
|
+
hooks: { after: [{
|
|
935
|
+
matcher(context) {
|
|
936
|
+
return context.path === "/list-sessions";
|
|
937
|
+
},
|
|
938
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
939
|
+
const response = await getEndpointResponse(ctx);
|
|
940
|
+
if (!response) return;
|
|
941
|
+
const newJson = response.filter((session) => {
|
|
942
|
+
return !session.impersonatedBy;
|
|
943
|
+
});
|
|
944
|
+
return ctx.json(newJson);
|
|
945
|
+
})
|
|
946
|
+
}] },
|
|
947
|
+
endpoints: {
|
|
948
|
+
setRole: setRole(opts),
|
|
949
|
+
getUser: getUser(opts),
|
|
950
|
+
createUser: createUser(opts),
|
|
951
|
+
adminUpdateUser: adminUpdateUser(opts),
|
|
952
|
+
listUsers: listUsers(opts),
|
|
953
|
+
listUserSessions: listUserSessions(opts),
|
|
954
|
+
unbanUser: unbanUser(opts),
|
|
955
|
+
banUser: banUser(opts),
|
|
956
|
+
impersonateUser: impersonateUser(opts),
|
|
957
|
+
stopImpersonating: stopImpersonating(),
|
|
958
|
+
revokeUserSession: revokeUserSession(opts),
|
|
959
|
+
revokeUserSessions: revokeUserSessions(opts),
|
|
960
|
+
removeUser: removeUser(opts),
|
|
961
|
+
setUserPassword: setUserPassword(opts),
|
|
962
|
+
userHasPermission: userHasPermission(opts)
|
|
963
|
+
},
|
|
964
|
+
$ERROR_CODES: ADMIN_ERROR_CODES,
|
|
965
|
+
schema: mergeSchema(schema, opts.schema),
|
|
966
|
+
options
|
|
967
|
+
};
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
//#endregion
|
|
971
|
+
export { admin as t };
|