better-auth 1.6.15 → 1.6.17
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/api/index.d.mts +2 -2
- package/dist/api/index.mjs +3 -4
- package/dist/api/middlewares/origin-check.mjs +6 -1
- package/dist/api/rate-limiter/index.mjs +259 -73
- package/dist/api/routes/account.mjs +31 -11
- package/dist/api/routes/callback.mjs +3 -3
- package/dist/api/routes/index.d.mts +1 -1
- package/dist/api/routes/password.mjs +3 -4
- package/dist/api/routes/session.d.mts +12 -1
- package/dist/api/routes/session.mjs +16 -2
- package/dist/api/routes/sign-in.mjs +5 -5
- package/dist/api/routes/sign-up.mjs +2 -2
- package/dist/api/routes/update-session.mjs +9 -4
- package/dist/api/routes/update-user.mjs +10 -12
- package/dist/auth/base.mjs +11 -7
- package/dist/client/equality.d.mts +19 -0
- package/dist/client/equality.mjs +42 -0
- package/dist/client/index.d.mts +5 -4
- package/dist/client/index.mjs +2 -1
- package/dist/client/lynx/index.d.mts +6 -5
- package/dist/client/path-to-object.d.mts +5 -2
- package/dist/client/plugins/index.d.mts +4 -1
- package/dist/client/plugins/index.mjs +4 -1
- package/dist/client/query.d.mts +4 -3
- package/dist/client/query.mjs +27 -17
- package/dist/client/react/index.d.mts +6 -5
- package/dist/client/session-atom.mjs +129 -4
- package/dist/client/session-refresh.d.mts +3 -18
- package/dist/client/session-refresh.mjs +38 -49
- package/dist/client/solid/index.d.mts +6 -5
- package/dist/client/svelte/index.d.mts +6 -5
- package/dist/client/types.d.mts +2 -2
- package/dist/client/vanilla.d.mts +6 -5
- package/dist/client/vue/index.d.mts +6 -5
- package/dist/context/create-context.mjs +3 -2
- package/dist/context/store-capabilities.mjs +12 -0
- package/dist/cookies/index.mjs +30 -2
- package/dist/db/internal-adapter.mjs +56 -0
- package/dist/oauth2/link-account.d.mts +13 -0
- package/dist/oauth2/link-account.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.mjs +49 -19
- package/dist/plugins/admin/access/statement.d.mts +10 -10
- package/dist/plugins/admin/access/statement.mjs +2 -0
- package/dist/plugins/admin/admin.d.mts +6 -3
- package/dist/plugins/admin/client.d.mts +6 -4
- package/dist/plugins/admin/error-codes.d.mts +2 -0
- package/dist/plugins/admin/error-codes.mjs +3 -1
- package/dist/plugins/admin/routes.mjs +73 -5
- package/dist/plugins/admin/schema.d.mts +1 -0
- package/dist/plugins/admin/schema.mjs +2 -1
- package/dist/plugins/captcha/constants.mjs +8 -1
- package/dist/plugins/captcha/index.mjs +8 -2
- package/dist/plugins/captcha/types.d.mts +21 -0
- package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
- package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
- package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
- package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
- package/dist/plugins/device-authorization/routes.mjs +16 -9
- package/dist/plugins/email-otp/routes.mjs +23 -53
- package/dist/plugins/generic-oauth/index.mjs +7 -2
- package/dist/plugins/generic-oauth/routes.mjs +20 -9
- package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
- package/dist/plugins/haveibeenpwned/index.mjs +5 -1
- package/dist/plugins/index.d.mts +5 -1
- package/dist/plugins/index.mjs +4 -1
- package/dist/plugins/jwt/index.mjs +2 -2
- package/dist/plugins/mcp/client/index.mjs +1 -0
- package/dist/plugins/mcp/index.mjs +8 -0
- package/dist/plugins/multi-session/index.mjs +7 -5
- package/dist/plugins/oauth-popup/client.d.mts +82 -0
- package/dist/plugins/oauth-popup/client.mjs +203 -0
- package/dist/plugins/oauth-popup/constants.d.mts +11 -0
- package/dist/plugins/oauth-popup/constants.mjs +11 -0
- package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
- package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
- package/dist/plugins/oauth-popup/index.d.mts +67 -0
- package/dist/plugins/oauth-popup/index.mjs +227 -0
- package/dist/plugins/oauth-popup/types.d.mts +30 -0
- package/dist/plugins/oauth-proxy/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/utils.mjs +16 -2
- package/dist/plugins/oidc-provider/index.mjs +10 -0
- package/dist/plugins/one-tap/client.mjs +12 -6
- package/dist/plugins/one-tap/index.d.mts +1 -0
- package/dist/plugins/one-tap/index.mjs +9 -5
- package/dist/plugins/one-time-token/index.mjs +1 -3
- package/dist/plugins/open-api/generator.mjs +7 -4
- package/dist/plugins/organization/adapter.d.mts +29 -1
- package/dist/plugins/organization/adapter.mjs +66 -6
- package/dist/plugins/organization/organization.mjs +2 -0
- package/dist/plugins/organization/routes/crud-invites.mjs +55 -31
- package/dist/plugins/organization/routes/crud-members.mjs +42 -6
- package/dist/plugins/organization/routes/crud-team.mjs +51 -5
- package/dist/plugins/organization/schema.d.mts +2 -0
- package/dist/plugins/phone-number/routes.mjs +41 -36
- package/dist/plugins/siwe/index.mjs +30 -3
- package/dist/plugins/siwe/parse-message.mjs +60 -0
- package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
- package/dist/plugins/two-factor/index.mjs +9 -1
- package/dist/plugins/two-factor/otp/index.mjs +11 -13
- package/dist/plugins/two-factor/totp/index.mjs +1 -1
- package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
- package/dist/plugins/username/index.mjs +6 -6
- package/dist/test-utils/test-instance.d.mts +6 -5
- package/package.json +10 -10
|
@@ -8,7 +8,10 @@ import { createAuthEndpoint } from "@better-auth/core/api";
|
|
|
8
8
|
import * as z from "zod";
|
|
9
9
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
10
10
|
//#region src/plugins/one-tap/index.ts
|
|
11
|
-
const oneTapCallbackBodySchema = z.object({
|
|
11
|
+
const oneTapCallbackBodySchema = z.object({
|
|
12
|
+
idToken: z.string().meta({ description: "Google ID token, which the client obtains from the One Tap API" }),
|
|
13
|
+
callbackURL: z.string().meta({ description: "URL to redirect to after a successful sign-in" }).optional()
|
|
14
|
+
});
|
|
12
15
|
const oneTap = (options) => ({
|
|
13
16
|
id: "one-tap",
|
|
14
17
|
version: PACKAGE_VERSION,
|
|
@@ -34,13 +37,14 @@ const oneTap = (options) => ({
|
|
|
34
37
|
} }
|
|
35
38
|
}, async (ctx) => {
|
|
36
39
|
const { idToken } = ctx.body;
|
|
40
|
+
const googleProvider = typeof ctx.context.options.socialProviders?.google === "function" ? await ctx.context.options.socialProviders?.google() : ctx.context.options.socialProviders?.google;
|
|
41
|
+
const audience = options?.clientId || googleProvider?.clientId;
|
|
42
|
+
if (!audience || Array.isArray(audience) && audience.length === 0) throw new APIError("BAD_REQUEST", { message: "Google client ID is required for One Tap. Set it on the oneTap plugin (clientId) or on socialProviders.google." });
|
|
37
43
|
let payload;
|
|
38
44
|
try {
|
|
39
|
-
const
|
|
40
|
-
const googleProvider = typeof ctx.context.options.socialProviders?.google === "function" ? await ctx.context.options.socialProviders?.google() : ctx.context.options.socialProviders?.google;
|
|
41
|
-
const { payload: verifiedPayload } = await jwtVerify(idToken, JWKS, {
|
|
45
|
+
const { payload: verifiedPayload } = await jwtVerify(idToken, createRemoteJWKSet(new URL("https://www.googleapis.com/oauth2/v3/certs")), {
|
|
42
46
|
issuer: ["https://accounts.google.com", "accounts.google.com"],
|
|
43
|
-
audience
|
|
47
|
+
audience
|
|
44
48
|
});
|
|
45
49
|
payload = verifiedPayload;
|
|
46
50
|
} catch {
|
|
@@ -47,10 +47,8 @@ const oneTimeToken = (options) => {
|
|
|
47
47
|
}, async (c) => {
|
|
48
48
|
const { token } = c.body;
|
|
49
49
|
const storedToken = await storeToken(c, token);
|
|
50
|
-
const verificationValue = await c.context.internalAdapter.
|
|
50
|
+
const verificationValue = await c.context.internalAdapter.consumeVerificationValue(`one-time-token:${storedToken}`);
|
|
51
51
|
if (!verificationValue) throw c.error("BAD_REQUEST", { message: "Invalid token" });
|
|
52
|
-
await c.context.internalAdapter.deleteVerificationByIdentifier(`one-time-token:${storedToken}`);
|
|
53
|
-
if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) throw c.error("BAD_REQUEST", { message: "Token expired" });
|
|
54
52
|
const session = await c.context.internalAdapter.findSession(verificationValue.value);
|
|
55
53
|
if (!session) throw c.error("BAD_REQUEST", { message: "Session not found" });
|
|
56
54
|
if (!opts?.disableSetSessionCookie) await setSessionCookie(c, session);
|
|
@@ -178,12 +178,15 @@ async function generator(ctx, options) {
|
|
|
178
178
|
const components = { schemas: { ...Object.entries(tables).reduce((acc, [key, value]) => {
|
|
179
179
|
const modelName = key.charAt(0).toUpperCase() + key.slice(1);
|
|
180
180
|
const fields = value.fields;
|
|
181
|
-
const required = [];
|
|
182
|
-
const properties = { id: {
|
|
181
|
+
const required = new Set(["id"]);
|
|
182
|
+
const properties = { id: {
|
|
183
|
+
type: "string",
|
|
184
|
+
readOnly: true
|
|
185
|
+
} };
|
|
183
186
|
Object.entries(fields).forEach(([fieldKey, fieldValue]) => {
|
|
184
187
|
if (!fieldValue) return;
|
|
185
188
|
properties[fieldKey] = getFieldSchema(fieldValue);
|
|
186
|
-
if (fieldValue.required && fieldValue.
|
|
189
|
+
if (fieldValue.required && fieldValue.returned !== false) required.add(fieldKey);
|
|
187
190
|
});
|
|
188
191
|
Object.entries(properties).forEach(([key, prop]) => {
|
|
189
192
|
const field = value.fields[key];
|
|
@@ -192,7 +195,7 @@ async function generator(ctx, options) {
|
|
|
192
195
|
acc[modelName] = {
|
|
193
196
|
type: "object",
|
|
194
197
|
properties,
|
|
195
|
-
required
|
|
198
|
+
required: Array.from(required)
|
|
196
199
|
};
|
|
197
200
|
return acc;
|
|
198
201
|
}, {}) } };
|
|
@@ -536,6 +536,28 @@ declare const getOrgAdapter: <O extends OrganizationOptions>(context: AuthContex
|
|
|
536
536
|
userId: string;
|
|
537
537
|
createdAt: Date;
|
|
538
538
|
}>;
|
|
539
|
+
/**
|
|
540
|
+
* Adds a user to a team only when the team is below its member limit,
|
|
541
|
+
* reading the count and creating the membership in one transaction.
|
|
542
|
+
* Returns the existing membership unchanged (no capacity charge) when the
|
|
543
|
+
* user already belongs to the team.
|
|
544
|
+
*
|
|
545
|
+
* FIXME(team-cap-race): the count-then-create is not atomic under READ
|
|
546
|
+
* COMMITTED, so two concurrent adds can both pass the count check and
|
|
547
|
+
* exceed maximumMembersPerTeam. A durable fix needs a unique constraint on
|
|
548
|
+
* teamMember(teamId, userId) or serializable isolation. Affects every
|
|
549
|
+
* caller (acceptInvitation, addMember, addTeamMember).
|
|
550
|
+
*/
|
|
551
|
+
addTeamMemberWithLimit: (data: {
|
|
552
|
+
teamId: string;
|
|
553
|
+
userId: string;
|
|
554
|
+
maximumMembersPerTeam: number;
|
|
555
|
+
}) => Promise<{
|
|
556
|
+
status: "added";
|
|
557
|
+
member: TeamMember;
|
|
558
|
+
} | {
|
|
559
|
+
status: "limitReached";
|
|
560
|
+
}>;
|
|
539
561
|
removeTeamMember: (data: {
|
|
540
562
|
teamId: string;
|
|
541
563
|
userId: string;
|
|
@@ -757,7 +779,13 @@ declare const getOrgAdapter: <O extends OrganizationOptions>(context: AuthContex
|
|
|
757
779
|
} ? FieldAttributeToObject<Field> : {}) extends infer T ? { [K in keyof T]: T[K] } : never)[]>;
|
|
758
780
|
updateInvitation: (data: {
|
|
759
781
|
invitationId: string;
|
|
760
|
-
status: "accepted" | "canceled" | "rejected";
|
|
782
|
+
status: "pending" | "accepted" | "canceled" | "rejected";
|
|
783
|
+
/**
|
|
784
|
+
* Only transition when the invitation is currently in this status. The
|
|
785
|
+
* guarded update is atomic, so a concurrent caller racing the same
|
|
786
|
+
* transition gets `null` instead of both proceeding.
|
|
787
|
+
*/
|
|
788
|
+
fromStatus?: "pending";
|
|
761
789
|
}) => Promise<((O["teams"] extends {
|
|
762
790
|
enabled: true;
|
|
763
791
|
} ? {
|
|
@@ -4,6 +4,23 @@ import { getCurrentAdapter, runWithTransaction } from "@better-auth/core/context
|
|
|
4
4
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
5
5
|
import { filterOutputFields } from "@better-auth/core/utils/db";
|
|
6
6
|
//#region src/plugins/organization/adapter.ts
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the configured per-team member cap to a concrete number for a given
|
|
9
|
+
* team-add. Returns `undefined` only when no cap is configured. Throws when the
|
|
10
|
+
* cap is a function but no session is available to evaluate it, so a sessionless
|
|
11
|
+
* server-side add fails closed instead of silently bypassing the limit.
|
|
12
|
+
*/
|
|
13
|
+
async function resolveMaximumMembersPerTeam(teams, context) {
|
|
14
|
+
const maximumMembersPerTeam = teams?.maximumMembersPerTeam;
|
|
15
|
+
if (maximumMembersPerTeam === void 0) return void 0;
|
|
16
|
+
if (typeof maximumMembersPerTeam === "number") return maximumMembersPerTeam;
|
|
17
|
+
if (!context.session) throw new BetterAuthError("`teams.maximumMembersPerTeam` is configured as a function but no session is available to evaluate it. Provide a session-bearing request or configure a numeric limit.");
|
|
18
|
+
return await maximumMembersPerTeam({
|
|
19
|
+
teamId: context.teamId,
|
|
20
|
+
session: context.session,
|
|
21
|
+
organizationId: context.organizationId
|
|
22
|
+
});
|
|
23
|
+
}
|
|
7
24
|
const getOrgAdapter = (context, options) => {
|
|
8
25
|
const baseAdapter = context.adapter;
|
|
9
26
|
const orgAdditionalFields = options?.schema?.organization?.additionalFields;
|
|
@@ -513,6 +530,43 @@ const getOrgAdapter = (context, options) => {
|
|
|
513
530
|
}
|
|
514
531
|
});
|
|
515
532
|
},
|
|
533
|
+
addTeamMemberWithLimit: async (data) => {
|
|
534
|
+
return runWithTransaction(baseAdapter, async () => {
|
|
535
|
+
const adapter = await getCurrentAdapter(baseAdapter);
|
|
536
|
+
const existing = await adapter.findOne({
|
|
537
|
+
model: "teamMember",
|
|
538
|
+
where: [{
|
|
539
|
+
field: "teamId",
|
|
540
|
+
value: data.teamId
|
|
541
|
+
}, {
|
|
542
|
+
field: "userId",
|
|
543
|
+
value: data.userId
|
|
544
|
+
}]
|
|
545
|
+
});
|
|
546
|
+
if (existing) return {
|
|
547
|
+
status: "added",
|
|
548
|
+
member: existing
|
|
549
|
+
};
|
|
550
|
+
if (await adapter.count({
|
|
551
|
+
model: "teamMember",
|
|
552
|
+
where: [{
|
|
553
|
+
field: "teamId",
|
|
554
|
+
value: data.teamId
|
|
555
|
+
}]
|
|
556
|
+
}) >= data.maximumMembersPerTeam) return { status: "limitReached" };
|
|
557
|
+
return {
|
|
558
|
+
status: "added",
|
|
559
|
+
member: await adapter.create({
|
|
560
|
+
model: "teamMember",
|
|
561
|
+
data: {
|
|
562
|
+
teamId: data.teamId,
|
|
563
|
+
userId: data.userId,
|
|
564
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
565
|
+
}
|
|
566
|
+
})
|
|
567
|
+
};
|
|
568
|
+
});
|
|
569
|
+
},
|
|
516
570
|
removeTeamMember: async (data) => {
|
|
517
571
|
await (await getCurrentAdapter(baseAdapter)).deleteMany({
|
|
518
572
|
model: "teamMember",
|
|
@@ -615,16 +669,22 @@ const getOrgAdapter = (context, options) => {
|
|
|
615
669
|
});
|
|
616
670
|
},
|
|
617
671
|
updateInvitation: async (data) => {
|
|
618
|
-
|
|
672
|
+
const adapter = await getCurrentAdapter(baseAdapter);
|
|
673
|
+
const where = [{
|
|
674
|
+
field: "id",
|
|
675
|
+
value: data.invitationId
|
|
676
|
+
}];
|
|
677
|
+
if (data.fromStatus) where.push({
|
|
678
|
+
field: "status",
|
|
679
|
+
value: data.fromStatus
|
|
680
|
+
});
|
|
681
|
+
return await adapter.update({
|
|
619
682
|
model: "invitation",
|
|
620
|
-
where
|
|
621
|
-
field: "id",
|
|
622
|
-
value: data.invitationId
|
|
623
|
-
}],
|
|
683
|
+
where,
|
|
624
684
|
update: { status: data.status }
|
|
625
685
|
});
|
|
626
686
|
}
|
|
627
687
|
};
|
|
628
688
|
};
|
|
629
689
|
//#endregion
|
|
630
|
-
export { getOrgAdapter };
|
|
690
|
+
export { getOrgAdapter, resolveMaximumMembersPerTeam };
|
|
@@ -394,11 +394,13 @@ function organization(options) {
|
|
|
394
394
|
activeOrganizationId: {
|
|
395
395
|
type: "string",
|
|
396
396
|
required: false,
|
|
397
|
+
input: false,
|
|
397
398
|
fieldName: opts.schema?.session?.fields?.activeOrganizationId
|
|
398
399
|
},
|
|
399
400
|
...teamSupport ? { activeTeamId: {
|
|
400
401
|
type: "string",
|
|
401
402
|
required: false,
|
|
403
|
+
input: false,
|
|
402
404
|
fieldName: opts.schema?.session?.fields?.activeTeamId
|
|
403
405
|
} } : {}
|
|
404
406
|
} }
|
|
@@ -4,10 +4,11 @@ import { toZodSchema } from "../../../db/to-zod.mjs";
|
|
|
4
4
|
import { getSessionFromCtx } from "../../../api/routes/session.mjs";
|
|
5
5
|
import { defaultRoles } from "../access/statement.mjs";
|
|
6
6
|
import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs";
|
|
7
|
-
import { getOrgAdapter } from "../adapter.mjs";
|
|
7
|
+
import { getOrgAdapter, resolveMaximumMembersPerTeam } from "../adapter.mjs";
|
|
8
8
|
import { orgMiddleware, orgSessionMiddleware } from "../call.mjs";
|
|
9
9
|
import { hasPermission } from "../has-permission.mjs";
|
|
10
10
|
import { parseRoles } from "../organization.mjs";
|
|
11
|
+
import { runWithTransaction } from "@better-auth/core/context";
|
|
11
12
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
12
13
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
13
14
|
import * as z from "zod";
|
|
@@ -170,7 +171,12 @@ const createInvitation = (option) => {
|
|
|
170
171
|
}, ctx.context) : ctx.context.orgOptions.invitationLimit ?? 100;
|
|
171
172
|
if ((await adapter.findPendingInvitations({ organizationId })).length >= invitationLimit) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED);
|
|
172
173
|
if (ctx.context.orgOptions.teams?.enabled && "teamId" in ctx.body && ctx.body.teamId) {
|
|
173
|
-
|
|
174
|
+
const requestedTeamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId;
|
|
175
|
+
if (requestedTeamIds.some((id) => id.includes(","))) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVALID_TEAM_ID);
|
|
176
|
+
for (const teamId of requestedTeamIds) if (!await adapter.findTeamById({
|
|
177
|
+
teamId,
|
|
178
|
+
organizationId
|
|
179
|
+
})) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
|
|
174
180
|
}
|
|
175
181
|
if (ctx.context.orgOptions.teams && ctx.context.orgOptions.teams.enabled && typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !== "undefined" && "teamId" in ctx.body && ctx.body.teamId) {
|
|
176
182
|
const teamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId;
|
|
@@ -278,40 +284,58 @@ const acceptInvitation = (options) => createAuthEndpoint("/organization/accept-i
|
|
|
278
284
|
});
|
|
279
285
|
const acceptedI = await adapter.updateInvitation({
|
|
280
286
|
invitationId: ctx.body.invitationId,
|
|
281
|
-
status: "accepted"
|
|
287
|
+
status: "accepted",
|
|
288
|
+
fromStatus: "pending"
|
|
282
289
|
});
|
|
283
|
-
if (!acceptedI) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
userId: session.user.id
|
|
291
|
-
});
|
|
292
|
-
if (typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !== "undefined") {
|
|
293
|
-
if (await adapter.countTeamMembers({ teamId }) >= (typeof ctx.context.orgOptions.teams.maximumMembersPerTeam === "function" ? await ctx.context.orgOptions.teams.maximumMembersPerTeam({
|
|
290
|
+
if (!acceptedI) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND);
|
|
291
|
+
const member = await runWithTransaction(ctx.context.adapter, async () => {
|
|
292
|
+
if (ctx.context.orgOptions.teams && ctx.context.orgOptions.teams.enabled && "teamId" in acceptedI && acceptedI.teamId) {
|
|
293
|
+
const teamIds = acceptedI.teamId.split(",");
|
|
294
|
+
const onlyOne = teamIds.length === 1;
|
|
295
|
+
for (const teamId of teamIds) {
|
|
296
|
+
if (!await adapter.findTeamById({
|
|
294
297
|
teamId,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
+
organizationId: acceptedI.organizationId
|
|
299
|
+
})) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
|
|
300
|
+
const maximumMembersPerTeam = await resolveMaximumMembersPerTeam(ctx.context.orgOptions.teams, {
|
|
301
|
+
teamId,
|
|
302
|
+
organizationId: acceptedI.organizationId,
|
|
303
|
+
session
|
|
304
|
+
});
|
|
305
|
+
if (maximumMembersPerTeam !== void 0) {
|
|
306
|
+
if ((await adapter.addTeamMemberWithLimit({
|
|
307
|
+
teamId,
|
|
308
|
+
userId: session.user.id,
|
|
309
|
+
maximumMembersPerTeam
|
|
310
|
+
})).status === "limitReached") throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED);
|
|
311
|
+
} else await adapter.findOrCreateTeamMember({
|
|
312
|
+
teamId,
|
|
313
|
+
userId: session.user.id
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (onlyOne) {
|
|
317
|
+
const teamId = teamIds[0];
|
|
318
|
+
await setSessionCookie(ctx, {
|
|
319
|
+
session: await adapter.setActiveTeam(session.session.token, teamId, ctx),
|
|
320
|
+
user: session.user
|
|
321
|
+
});
|
|
298
322
|
}
|
|
299
323
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
324
|
+
const createdMember = await adapter.createMember({
|
|
325
|
+
organizationId: acceptedI.organizationId,
|
|
326
|
+
userId: session.user.id,
|
|
327
|
+
role: acceptedI.role,
|
|
328
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
329
|
+
});
|
|
330
|
+
await adapter.setActiveOrganization(session.session.token, acceptedI.organizationId, ctx);
|
|
331
|
+
return createdMember;
|
|
332
|
+
}).catch(async (error) => {
|
|
333
|
+
await adapter.updateInvitation({
|
|
334
|
+
invitationId: ctx.body.invitationId,
|
|
335
|
+
status: "pending"
|
|
336
|
+
});
|
|
337
|
+
throw error;
|
|
313
338
|
});
|
|
314
|
-
await adapter.setActiveOrganization(session.session.token, invitation.organizationId, ctx);
|
|
315
339
|
if (options?.organizationHooks?.afterAcceptInvitation) await options?.organizationHooks.afterAcceptInvitation({
|
|
316
340
|
invitation: acceptedI,
|
|
317
341
|
member,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { toZodSchema } from "../../../db/to-zod.mjs";
|
|
2
2
|
import { getSessionFromCtx, sessionMiddleware } from "../../../api/routes/session.mjs";
|
|
3
|
+
import { defaultRoles } from "../access/statement.mjs";
|
|
3
4
|
import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs";
|
|
4
|
-
import { getOrgAdapter } from "../adapter.mjs";
|
|
5
|
+
import { getOrgAdapter, resolveMaximumMembersPerTeam } from "../adapter.mjs";
|
|
5
6
|
import { orgMiddleware, orgSessionMiddleware } from "../call.mjs";
|
|
6
7
|
import { hasPermission } from "../has-permission.mjs";
|
|
7
8
|
import { parseRoles } from "../organization.mjs";
|
|
@@ -21,7 +22,7 @@ const addMember = (option) => {
|
|
|
21
22
|
fields: option?.schema?.member?.additionalFields || {},
|
|
22
23
|
isClientSide: true
|
|
23
24
|
});
|
|
24
|
-
return createAuthEndpoint({
|
|
25
|
+
return createAuthEndpoint.serverOnly({
|
|
25
26
|
method: "POST",
|
|
26
27
|
body: z.object({
|
|
27
28
|
...baseMemberSchema.shape,
|
|
@@ -87,11 +88,22 @@ const addMember = (option) => {
|
|
|
87
88
|
...response.data
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
91
|
+
const maximumMembersPerTeam = teamId ? await resolveMaximumMembersPerTeam(ctx.context.orgOptions.teams, {
|
|
92
|
+
teamId,
|
|
93
|
+
organizationId: orgId,
|
|
94
|
+
session
|
|
95
|
+
}) : void 0;
|
|
96
|
+
if (teamId) if (maximumMembersPerTeam !== void 0) {
|
|
97
|
+
if ((await adapter.addTeamMemberWithLimit({
|
|
98
|
+
teamId,
|
|
99
|
+
userId: user.id,
|
|
100
|
+
maximumMembersPerTeam
|
|
101
|
+
})).status === "limitReached") throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED);
|
|
102
|
+
} else await adapter.findOrCreateTeamMember({
|
|
92
103
|
userId: user.id,
|
|
93
104
|
teamId
|
|
94
105
|
});
|
|
106
|
+
const createdMember = await adapter.createMember(memberData);
|
|
95
107
|
if (option?.organizationHooks?.afterAddMember) await option?.organizationHooks.afterAddMember({
|
|
96
108
|
member: createdMember,
|
|
97
109
|
user,
|
|
@@ -241,7 +253,31 @@ const updateMemberRole = (option) => createAuthEndpoint("/organization/update-me
|
|
|
241
253
|
const organizationId = ctx.body.organizationId || session.session.activeOrganizationId;
|
|
242
254
|
if (!organizationId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION);
|
|
243
255
|
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
|
|
244
|
-
const roleToSet = Array.isArray(ctx.body.role) ? ctx.body.role : ctx.body.role
|
|
256
|
+
const roleToSet = (Array.isArray(ctx.body.role) ? ctx.body.role : [ctx.body.role]).flatMap((role) => role.split(",")).map((role) => role.trim()).filter(Boolean);
|
|
257
|
+
if (roleToSet.length === 0) throw APIError.fromStatus("BAD_REQUEST");
|
|
258
|
+
const validStaticRoles = new Set([...Object.keys(defaultRoles), ...Object.keys(ctx.context.orgOptions.roles || {})]);
|
|
259
|
+
const unknownRoles = roleToSet.filter((role) => !validStaticRoles.has(role));
|
|
260
|
+
if (unknownRoles.length > 0) if (ctx.context.orgOptions.dynamicAccessControl?.enabled) {
|
|
261
|
+
const foundRoleNames = (await ctx.context.adapter.findMany({
|
|
262
|
+
model: "organizationRole",
|
|
263
|
+
where: [{
|
|
264
|
+
field: "organizationId",
|
|
265
|
+
value: organizationId
|
|
266
|
+
}, {
|
|
267
|
+
field: "role",
|
|
268
|
+
value: unknownRoles,
|
|
269
|
+
operator: "in"
|
|
270
|
+
}]
|
|
271
|
+
})).map((r) => r.role);
|
|
272
|
+
const stillInvalid = unknownRoles.filter((r) => !foundRoleNames.includes(r));
|
|
273
|
+
if (stillInvalid.length > 0) throw new APIError("BAD_REQUEST", {
|
|
274
|
+
code: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.code,
|
|
275
|
+
message: `${ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.code}: ${stillInvalid.join(", ")}`
|
|
276
|
+
});
|
|
277
|
+
} else throw new APIError("BAD_REQUEST", {
|
|
278
|
+
code: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.code,
|
|
279
|
+
message: `${ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.code}: ${unknownRoles.join(", ")}`
|
|
280
|
+
});
|
|
245
281
|
const member = await adapter.findMemberByOrgId({
|
|
246
282
|
userId: session.user.id,
|
|
247
283
|
organizationId
|
|
@@ -280,7 +316,7 @@ const updateMemberRole = (option) => createAuthEndpoint("/organization/update-me
|
|
|
280
316
|
const userBeingUpdated = await ctx.context.internalAdapter.findUserById(toBeUpdatedMember.userId);
|
|
281
317
|
if (!userBeingUpdated) throw APIError.fromStatus("BAD_REQUEST", { message: "User not found" });
|
|
282
318
|
const previousRole = toBeUpdatedMember.role;
|
|
283
|
-
const newRole = parseRoles(
|
|
319
|
+
const newRole = parseRoles(roleToSet);
|
|
284
320
|
if (option?.organizationHooks?.beforeUpdateMemberRole) {
|
|
285
321
|
const response = await option?.organizationHooks.beforeUpdateMemberRole({
|
|
286
322
|
member: toBeUpdatedMember,
|
|
@@ -2,10 +2,11 @@ import { setSessionCookie } from "../../../cookies/index.mjs";
|
|
|
2
2
|
import { toZodSchema } from "../../../db/to-zod.mjs";
|
|
3
3
|
import { getSessionFromCtx } from "../../../api/routes/session.mjs";
|
|
4
4
|
import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs";
|
|
5
|
-
import { getOrgAdapter } from "../adapter.mjs";
|
|
5
|
+
import { getOrgAdapter, resolveMaximumMembersPerTeam } from "../adapter.mjs";
|
|
6
6
|
import { orgMiddleware, orgSessionMiddleware } from "../call.mjs";
|
|
7
7
|
import { hasPermission } from "../has-permission.mjs";
|
|
8
8
|
import { teamSchema } from "../schema.mjs";
|
|
9
|
+
import { getCurrentAdapter, runWithTransaction } from "@better-auth/core/context";
|
|
9
10
|
import { APIError } from "@better-auth/core/error";
|
|
10
11
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
11
12
|
import * as z from "zod";
|
|
@@ -185,7 +186,25 @@ const removeTeam = (options) => createAuthEndpoint("/organization/remove-team",
|
|
|
185
186
|
user: session?.user,
|
|
186
187
|
organization
|
|
187
188
|
});
|
|
188
|
-
await adapter
|
|
189
|
+
await runWithTransaction(ctx.context.adapter, async () => {
|
|
190
|
+
await adapter.deleteTeam(team.id);
|
|
191
|
+
const pendingInvitations = await adapter.findPendingInvitations({ organizationId });
|
|
192
|
+
const trx = await getCurrentAdapter(ctx.context.adapter);
|
|
193
|
+
for (const invitation of pendingInvitations) {
|
|
194
|
+
if (!("teamId" in invitation) || !invitation.teamId) continue;
|
|
195
|
+
const teamIds = invitation.teamId.split(",");
|
|
196
|
+
if (!teamIds.includes(team.id)) continue;
|
|
197
|
+
const remainingTeamIds = teamIds.filter((id) => id !== team.id);
|
|
198
|
+
await trx.update({
|
|
199
|
+
model: "invitation",
|
|
200
|
+
where: [{
|
|
201
|
+
field: "id",
|
|
202
|
+
value: invitation.id
|
|
203
|
+
}],
|
|
204
|
+
update: { teamId: remainingTeamIds.length > 0 ? remainingTeamIds.join(",") : null }
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
});
|
|
189
208
|
if (options?.organizationHooks?.afterDeleteTeam) await options?.organizationHooks.afterDeleteTeam({
|
|
190
209
|
team,
|
|
191
210
|
user: session?.user,
|
|
@@ -441,8 +460,15 @@ const listUserTeams = (options) => createAuthEndpoint("/organization/list-user-t
|
|
|
441
460
|
use: [orgMiddleware, orgSessionMiddleware]
|
|
442
461
|
}, async (ctx) => {
|
|
443
462
|
const session = ctx.context.session;
|
|
444
|
-
const
|
|
445
|
-
|
|
463
|
+
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
|
|
464
|
+
const teams = await adapter.listTeamsByUser({ userId: session.user.id });
|
|
465
|
+
const orgIds = [...new Set(teams.map((team) => team.organizationId))];
|
|
466
|
+
const memberships = await Promise.all(orgIds.map((organizationId) => adapter.checkMembership({
|
|
467
|
+
userId: session.user.id,
|
|
468
|
+
organizationId
|
|
469
|
+
})));
|
|
470
|
+
const memberOrgIds = new Set(orgIds.filter((_, index) => memberships[index]));
|
|
471
|
+
return ctx.json(teams.filter((team) => memberOrgIds.has(team.organizationId)));
|
|
446
472
|
});
|
|
447
473
|
const listTeamMembersQuerySchema = z.optional(z.object({ teamId: z.string().optional().meta({ description: "The team whose members we should return. If this is not provided the members of the current active team get returned." }) }));
|
|
448
474
|
const listTeamMembers = (options) => createAuthEndpoint("/organization/list-team-members", {
|
|
@@ -494,6 +520,12 @@ const listTeamMembers = (options) => createAuthEndpoint("/organization/list-team
|
|
|
494
520
|
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
|
|
495
521
|
const teamId = ctx.query?.teamId || session?.session.activeTeamId;
|
|
496
522
|
if (!teamId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM);
|
|
523
|
+
const team = await adapter.findTeamById({ teamId });
|
|
524
|
+
if (!team) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
|
|
525
|
+
if (!await adapter.checkMembership({
|
|
526
|
+
userId: session.user.id,
|
|
527
|
+
organizationId: team.organizationId
|
|
528
|
+
})) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM);
|
|
497
529
|
if (!await adapter.findTeamMember({
|
|
498
530
|
userId: session.user.id,
|
|
499
531
|
teamId
|
|
@@ -587,7 +619,21 @@ const addTeamMember = (options) => createAuthEndpoint("/organization/add-team-me
|
|
|
587
619
|
});
|
|
588
620
|
if (response && typeof response === "object" && "data" in response) {}
|
|
589
621
|
}
|
|
590
|
-
const
|
|
622
|
+
const maximumMembersPerTeam = await resolveMaximumMembersPerTeam(ctx.context.orgOptions.teams, {
|
|
623
|
+
teamId: ctx.body.teamId,
|
|
624
|
+
organizationId,
|
|
625
|
+
session
|
|
626
|
+
});
|
|
627
|
+
let teamMember;
|
|
628
|
+
if (maximumMembersPerTeam !== void 0) {
|
|
629
|
+
const result = await adapter.addTeamMemberWithLimit({
|
|
630
|
+
teamId: ctx.body.teamId,
|
|
631
|
+
userId: ctx.body.userId,
|
|
632
|
+
maximumMembersPerTeam
|
|
633
|
+
});
|
|
634
|
+
if (result.status === "limitReached") throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED);
|
|
635
|
+
teamMember = result.member;
|
|
636
|
+
} else teamMember = await adapter.findOrCreateTeamMember({
|
|
591
637
|
teamId: ctx.body.teamId,
|
|
592
638
|
userId: ctx.body.userId
|
|
593
639
|
});
|
|
@@ -183,6 +183,7 @@ interface SessionDefaultFields {
|
|
|
183
183
|
activeOrganizationId: {
|
|
184
184
|
type: "string";
|
|
185
185
|
required: false;
|
|
186
|
+
input: false;
|
|
186
187
|
};
|
|
187
188
|
}
|
|
188
189
|
type OrganizationSchema<O extends OrganizationOptions> = (O["dynamicAccessControl"] extends {
|
|
@@ -222,6 +223,7 @@ type OrganizationSchema<O extends OrganizationOptions> = (O["dynamicAccessContro
|
|
|
222
223
|
activeTeamId: {
|
|
223
224
|
type: "string";
|
|
224
225
|
required: false;
|
|
226
|
+
input: false;
|
|
225
227
|
};
|
|
226
228
|
} : {});
|
|
227
229
|
};
|