better-auth 1.7.14 → 1.7.16
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/dispatch.mjs +1 -1
- package/dist/api/index.d.mts +2 -1
- package/dist/api/index.mjs +3 -2
- package/dist/api/routes/account.mjs +14 -6
- package/dist/api/routes/email-verification.mjs +29 -37
- package/dist/api/routes/sign-in.mjs +2 -2
- package/dist/api/routes/sign-up.mjs +1 -1
- package/dist/api/routes/update-user.mjs +10 -4
- package/dist/api/state/session-source.d.mts +10 -0
- package/dist/api/state/session-source.mjs +28 -0
- package/dist/auth/trusted-origins.mjs +31 -8
- package/dist/client/index.d.mts +2 -1
- package/dist/client/parser.mjs +1 -11
- package/dist/client/vue/index.mjs +13 -7
- package/dist/context/secret-utils.mjs +3 -3
- package/dist/db/internal-adapter.mjs +24 -6
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/oauth2/link-account.d.mts +3 -5
- package/dist/oauth2/link-account.mjs +33 -3
- package/dist/oauth2/state.d.mts +6 -5
- package/dist/package.mjs +1 -1
- 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 -4
- package/dist/plugins/admin/admin.mjs +4 -4
- 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 +9 -0
- package/dist/plugins/anonymous/index.mjs +2 -1
- package/dist/plugins/device-authorization/routes.mjs +3 -3
- package/dist/plugins/email-otp/routes.mjs +33 -50
- package/dist/plugins/generic-oauth/index.mjs +54 -30
- package/dist/plugins/generic-oauth/providers/hubspot.mjs +5 -2
- package/dist/plugins/index.d.mts +2 -1
- package/dist/plugins/index.mjs +2 -1
- package/dist/plugins/oauth-proxy/index.mjs +1 -1
- package/dist/plugins/one-tap/index.mjs +1 -1
- package/dist/plugins/organization/adapter.mjs +1 -0
- package/dist/plugins/organization/client.d.mts +1 -0
- package/dist/plugins/organization/error-codes.d.mts +1 -0
- package/dist/plugins/organization/error-codes.mjs +1 -0
- package/dist/plugins/organization/has-permission.d.mts +2 -3
- package/dist/plugins/organization/has-permission.mjs +62 -23
- package/dist/plugins/organization/index.d.mts +2 -1
- package/dist/plugins/organization/index.mjs +2 -1
- package/dist/plugins/organization/organization.d.mts +2 -2
- package/dist/plugins/organization/organization.mjs +1 -3
- package/dist/plugins/organization/permission.mjs +3 -3
- package/dist/plugins/organization/roles.d.mts +8 -0
- package/dist/plugins/organization/roles.mjs +26 -0
- package/dist/plugins/organization/routes/crud-access-control.mjs +2 -1
- package/dist/plugins/organization/routes/crud-invites.mjs +22 -11
- package/dist/plugins/organization/routes/crud-members.mjs +10 -14
- package/dist/plugins/organization/routes/crud-org.mjs +12 -3
- package/dist/plugins/phone-number/client.d.mts +2 -0
- package/dist/plugins/phone-number/error-codes.d.mts +2 -0
- package/dist/plugins/phone-number/error-codes.mjs +2 -0
- package/dist/plugins/phone-number/index.d.mts +25 -1
- package/dist/plugins/phone-number/index.mjs +22 -6
- package/dist/plugins/phone-number/routes.mjs +34 -35
- package/dist/plugins/two-factor/index.d.mts +9 -2
- package/dist/plugins/two-factor/index.mjs +73 -26
- package/dist/plugins/two-factor/verify-two-factor.mjs +2 -2
- package/dist/plugins/username/index.mjs +6 -19
- package/dist/state.d.mts +6 -0
- package/dist/state.mjs +5 -2
- package/dist/utils/index.d.mts +2 -2
- package/package.json +9 -9
|
@@ -2,10 +2,10 @@ import { toZodSchema } from "../../../db/to-zod.mjs";
|
|
|
2
2
|
import { getSessionFromCtx, sessionMiddleware } from "../../../api/routes/session.mjs";
|
|
3
3
|
import { defaultRoles } from "../access/statement.mjs";
|
|
4
4
|
import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs";
|
|
5
|
+
import { parseRoleList, parseRoles } from "../roles.mjs";
|
|
5
6
|
import { getOrgAdapter } from "../adapter.mjs";
|
|
6
7
|
import { orgMiddleware, orgSessionMiddleware } from "../call.mjs";
|
|
7
8
|
import { hasPermission } from "../has-permission.mjs";
|
|
8
|
-
import { parseRoles } from "../organization.mjs";
|
|
9
9
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
10
10
|
import { whereOperators } from "@better-auth/core/db/adapter";
|
|
11
11
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -190,14 +190,12 @@ const removeMember = (options) => createAuthEndpoint("/organization/remove-membe
|
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
if (!toBeRemovedMember) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND);
|
|
193
|
-
const roles = toBeRemovedMember.role
|
|
193
|
+
const roles = parseRoleList(toBeRemovedMember.role);
|
|
194
194
|
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
|
|
195
195
|
if (roles.includes(creatorRole)) {
|
|
196
|
-
if (!member.role
|
|
196
|
+
if (!parseRoleList(member.role).includes(creatorRole)) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER);
|
|
197
197
|
const { members } = await adapter.listMembers({ organizationId });
|
|
198
|
-
if (members.filter((member) =>
|
|
199
|
-
return member.role.split(",").includes(creatorRole);
|
|
200
|
-
}).length <= 1) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER);
|
|
198
|
+
if (members.filter((member) => parseRoleList(member.role).includes(creatorRole)).length <= 1) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER);
|
|
201
199
|
}
|
|
202
200
|
if (!await hasPermission({
|
|
203
201
|
role: member.role,
|
|
@@ -273,7 +271,7 @@ const updateMemberRole = (option) => createAuthEndpoint("/organization/update-me
|
|
|
273
271
|
const organizationId = ctx.body.organizationId || session.session.activeOrganizationId;
|
|
274
272
|
if (!organizationId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION);
|
|
275
273
|
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
|
|
276
|
-
const roleToSet = (
|
|
274
|
+
const roleToSet = parseRoleList(ctx.body.role);
|
|
277
275
|
if (roleToSet.length === 0) throw APIError.fromStatus("BAD_REQUEST");
|
|
278
276
|
const member = await adapter.findMemberByOrgId({
|
|
279
277
|
userId: session.user.id,
|
|
@@ -284,8 +282,8 @@ const updateMemberRole = (option) => createAuthEndpoint("/organization/update-me
|
|
|
284
282
|
if (!toBeUpdatedMember) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND);
|
|
285
283
|
if (!(toBeUpdatedMember.organizationId === organizationId)) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER);
|
|
286
284
|
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
|
|
287
|
-
const updatingMemberRoles = member.role
|
|
288
|
-
const isUpdatingCreator = toBeUpdatedMember.role
|
|
285
|
+
const updatingMemberRoles = parseRoleList(member.role);
|
|
286
|
+
const isUpdatingCreator = parseRoleList(toBeUpdatedMember.role).includes(creatorRole);
|
|
289
287
|
const updaterIsCreator = updatingMemberRoles.includes(creatorRole);
|
|
290
288
|
const isSettingCreatorRole = roleToSet.includes(creatorRole);
|
|
291
289
|
const memberIsUpdatingThemselves = member.id === toBeUpdatedMember.id;
|
|
@@ -297,9 +295,7 @@ const updateMemberRole = (option) => createAuthEndpoint("/organization/update-me
|
|
|
297
295
|
field: "organizationId",
|
|
298
296
|
value: organizationId
|
|
299
297
|
}]
|
|
300
|
-
})).filter((member) =>
|
|
301
|
-
return member.role.split(",").includes(creatorRole);
|
|
302
|
-
}).length <= 1 && !isSettingCreatorRole) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER);
|
|
298
|
+
})).filter((member) => parseRoleList(member.role).includes(creatorRole)).length <= 1 && !isSettingCreatorRole) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER);
|
|
303
299
|
}
|
|
304
300
|
if (!await hasPermission({
|
|
305
301
|
role: member.role,
|
|
@@ -419,14 +415,14 @@ const leaveOrganization = (options) => createAuthEndpoint("/organization/leave",
|
|
|
419
415
|
});
|
|
420
416
|
if (!member) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND);
|
|
421
417
|
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
|
|
422
|
-
if (member.role
|
|
418
|
+
if (parseRoleList(member.role).includes(creatorRole)) {
|
|
423
419
|
if ((await ctx.context.adapter.findMany({
|
|
424
420
|
model: "member",
|
|
425
421
|
where: [{
|
|
426
422
|
field: "organizationId",
|
|
427
423
|
value: ctx.body.organizationId
|
|
428
424
|
}]
|
|
429
|
-
})).filter((member) => member.role
|
|
425
|
+
})).filter((member) => parseRoleList(member.role).includes(creatorRole)).length <= 1) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER);
|
|
430
426
|
}
|
|
431
427
|
await adapter.deleteMember({
|
|
432
428
|
memberId: member.id,
|
|
@@ -4,7 +4,7 @@ import { getSessionFromCtx, requestOnlySessionMiddleware } from "../../../api/ro
|
|
|
4
4
|
import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs";
|
|
5
5
|
import { getOrgAdapter } from "../adapter.mjs";
|
|
6
6
|
import { orgMiddleware, orgSessionMiddleware } from "../call.mjs";
|
|
7
|
-
import { hasPermission } from "../has-permission.mjs";
|
|
7
|
+
import { canViewInvitations, hasPermission, withoutCreatorRoleInvitations } from "../has-permission.mjs";
|
|
8
8
|
import { APIError } from "@better-auth/core/error";
|
|
9
9
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
10
10
|
import * as z from "zod";
|
|
@@ -362,13 +362,22 @@ const getFullOrganization = (options) => createAuthEndpoint("/organization/get-f
|
|
|
362
362
|
membersLimit: ctx.query?.membersLimit
|
|
363
363
|
});
|
|
364
364
|
if (!organization) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND);
|
|
365
|
-
|
|
365
|
+
const isMember = await adapter.checkMembership({
|
|
366
366
|
userId: session.user.id,
|
|
367
367
|
organizationId: organization.id
|
|
368
|
-
})
|
|
368
|
+
});
|
|
369
|
+
if (!isMember) {
|
|
369
370
|
await adapter.setActiveOrganization(session.session.token, null, ctx);
|
|
370
371
|
throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION);
|
|
371
372
|
}
|
|
373
|
+
organization.invitations = organization.invitations.length > 0 && await canViewInvitations({
|
|
374
|
+
role: isMember.role,
|
|
375
|
+
options: ctx.context.orgOptions,
|
|
376
|
+
organizationId: organization.id
|
|
377
|
+
}, ctx) ? withoutCreatorRoleInvitations(organization.invitations, {
|
|
378
|
+
role: isMember.role,
|
|
379
|
+
options: ctx.context.orgOptions
|
|
380
|
+
}) : [];
|
|
372
381
|
return ctx.json(organization);
|
|
373
382
|
});
|
|
374
383
|
const setActiveOrganizationBodySchema = z.object({
|
|
@@ -21,7 +21,9 @@ export declare const phoneNumberClient: () => {
|
|
|
21
21
|
UNEXPECTED_ERROR: import("@better-auth/core/utils/error-codes").RawError<"UNEXPECTED_ERROR">;
|
|
22
22
|
OTP_NOT_FOUND: import("@better-auth/core/utils/error-codes").RawError<"OTP_NOT_FOUND">;
|
|
23
23
|
PHONE_NUMBER_NOT_VERIFIED: import("@better-auth/core/utils/error-codes").RawError<"PHONE_NUMBER_NOT_VERIFIED">;
|
|
24
|
+
PHONE_NUMBER_PENDING_VERIFICATION: import("@better-auth/core/utils/error-codes").RawError<"PHONE_NUMBER_PENDING_VERIFICATION">;
|
|
24
25
|
PHONE_NUMBER_CANNOT_BE_UPDATED: import("@better-auth/core/utils/error-codes").RawError<"PHONE_NUMBER_CANNOT_BE_UPDATED">;
|
|
26
|
+
PHONE_NUMBER_CANNOT_BE_SET: import("@better-auth/core/utils/error-codes").RawError<"PHONE_NUMBER_CANNOT_BE_SET">;
|
|
25
27
|
SEND_OTP_NOT_IMPLEMENTED: import("@better-auth/core/utils/error-codes").RawError<"SEND_OTP_NOT_IMPLEMENTED">;
|
|
26
28
|
};
|
|
27
29
|
};
|
|
@@ -10,7 +10,9 @@ export declare const PHONE_NUMBER_ERROR_CODES: {
|
|
|
10
10
|
UNEXPECTED_ERROR: import("@better-auth/core/utils/error-codes").RawError<"UNEXPECTED_ERROR">;
|
|
11
11
|
OTP_NOT_FOUND: import("@better-auth/core/utils/error-codes").RawError<"OTP_NOT_FOUND">;
|
|
12
12
|
PHONE_NUMBER_NOT_VERIFIED: import("@better-auth/core/utils/error-codes").RawError<"PHONE_NUMBER_NOT_VERIFIED">;
|
|
13
|
+
PHONE_NUMBER_PENDING_VERIFICATION: import("@better-auth/core/utils/error-codes").RawError<"PHONE_NUMBER_PENDING_VERIFICATION">;
|
|
13
14
|
PHONE_NUMBER_CANNOT_BE_UPDATED: import("@better-auth/core/utils/error-codes").RawError<"PHONE_NUMBER_CANNOT_BE_UPDATED">;
|
|
15
|
+
PHONE_NUMBER_CANNOT_BE_SET: import("@better-auth/core/utils/error-codes").RawError<"PHONE_NUMBER_CANNOT_BE_SET">;
|
|
14
16
|
SEND_OTP_NOT_IMPLEMENTED: import("@better-auth/core/utils/error-codes").RawError<"SEND_OTP_NOT_IMPLEMENTED">;
|
|
15
17
|
};
|
|
16
18
|
//#endregion
|
|
@@ -10,7 +10,9 @@ const PHONE_NUMBER_ERROR_CODES = defineErrorCodes({
|
|
|
10
10
|
OTP_EXPIRED: "OTP expired",
|
|
11
11
|
INVALID_OTP: "Invalid OTP",
|
|
12
12
|
PHONE_NUMBER_NOT_VERIFIED: "Phone number not verified",
|
|
13
|
+
PHONE_NUMBER_PENDING_VERIFICATION: "Phone number requires account ownership proof",
|
|
13
14
|
PHONE_NUMBER_CANNOT_BE_UPDATED: "Phone number cannot be updated",
|
|
15
|
+
PHONE_NUMBER_CANNOT_BE_SET: "Phone number can only be set by verifying it",
|
|
14
16
|
SEND_OTP_NOT_IMPLEMENTED: "sendOTP not implemented",
|
|
15
17
|
TOO_MANY_ATTEMPTS: "Too many attempts"
|
|
16
18
|
});
|
|
@@ -14,6 +14,28 @@ export declare const phoneNumber: (options?: PhoneNumberOptions | undefined) =>
|
|
|
14
14
|
options: {
|
|
15
15
|
databaseHooks: {
|
|
16
16
|
user: {
|
|
17
|
+
create: {
|
|
18
|
+
before(data: {
|
|
19
|
+
id: string;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
updatedAt: Date;
|
|
22
|
+
email: string;
|
|
23
|
+
emailVerified: boolean;
|
|
24
|
+
name: string;
|
|
25
|
+
image?: string | null | undefined;
|
|
26
|
+
} & Record<string, unknown>, ctx: import("@better-auth/core").GenericEndpointContext | null): Promise<{
|
|
27
|
+
data: {
|
|
28
|
+
[x: string]: unknown;
|
|
29
|
+
id: string;
|
|
30
|
+
createdAt: Date;
|
|
31
|
+
updatedAt: Date;
|
|
32
|
+
email: string;
|
|
33
|
+
emailVerified: boolean;
|
|
34
|
+
name: string;
|
|
35
|
+
image?: string | null | undefined;
|
|
36
|
+
};
|
|
37
|
+
} | undefined>;
|
|
38
|
+
};
|
|
17
39
|
update: {
|
|
18
40
|
before(data: Partial<{
|
|
19
41
|
id: string;
|
|
@@ -23,7 +45,7 @@ export declare const phoneNumber: (options?: PhoneNumberOptions | undefined) =>
|
|
|
23
45
|
emailVerified: boolean;
|
|
24
46
|
name: string;
|
|
25
47
|
image?: string | null | undefined;
|
|
26
|
-
}> & Record<string, unknown
|
|
48
|
+
}> & Record<string, unknown>, ctx: import("@better-auth/core").GenericEndpointContext | null): Promise<{
|
|
27
49
|
data: {
|
|
28
50
|
[x: string]: unknown;
|
|
29
51
|
id?: string | undefined;
|
|
@@ -346,7 +368,9 @@ export declare const phoneNumber: (options?: PhoneNumberOptions | undefined) =>
|
|
|
346
368
|
UNEXPECTED_ERROR: import("@better-auth/core/utils/error-codes").RawError<"UNEXPECTED_ERROR">;
|
|
347
369
|
OTP_NOT_FOUND: import("@better-auth/core/utils/error-codes").RawError<"OTP_NOT_FOUND">;
|
|
348
370
|
PHONE_NUMBER_NOT_VERIFIED: import("@better-auth/core/utils/error-codes").RawError<"PHONE_NUMBER_NOT_VERIFIED">;
|
|
371
|
+
PHONE_NUMBER_PENDING_VERIFICATION: import("@better-auth/core/utils/error-codes").RawError<"PHONE_NUMBER_PENDING_VERIFICATION">;
|
|
349
372
|
PHONE_NUMBER_CANNOT_BE_UPDATED: import("@better-auth/core/utils/error-codes").RawError<"PHONE_NUMBER_CANNOT_BE_UPDATED">;
|
|
373
|
+
PHONE_NUMBER_CANNOT_BE_SET: import("@better-auth/core/utils/error-codes").RawError<"PHONE_NUMBER_CANNOT_BE_SET">;
|
|
350
374
|
SEND_OTP_NOT_IMPLEMENTED: import("@better-auth/core/utils/error-codes").RawError<"SEND_OTP_NOT_IMPLEMENTED">;
|
|
351
375
|
};
|
|
352
376
|
};
|
|
@@ -20,12 +20,28 @@ const phoneNumber = (options) => {
|
|
|
20
20
|
id: "phone-number",
|
|
21
21
|
version: PACKAGE_VERSION,
|
|
22
22
|
init() {
|
|
23
|
-
return { options: { databaseHooks: { user: {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
[opts.
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
return { options: { databaseHooks: { user: {
|
|
24
|
+
create: { async before(data, ctx) {
|
|
25
|
+
if (!data[opts.phoneNumber] || data[opts.phoneNumberVerified] === true) return;
|
|
26
|
+
if (ctx?.body?.[opts.phoneNumber]) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.PHONE_NUMBER_CANNOT_BE_SET);
|
|
27
|
+
return { data: {
|
|
28
|
+
...data,
|
|
29
|
+
[opts.phoneNumber]: null,
|
|
30
|
+
[opts.phoneNumberVerified]: false
|
|
31
|
+
} };
|
|
32
|
+
} },
|
|
33
|
+
update: { async before(data, ctx) {
|
|
34
|
+
if (opts.phoneNumber in data && data[opts.phoneNumber] === null) return { data: {
|
|
35
|
+
...data,
|
|
36
|
+
[opts.phoneNumberVerified]: false
|
|
37
|
+
} };
|
|
38
|
+
if (!ctx || !data[opts.phoneNumber] || data[opts.phoneNumberVerified] === true || ctx.path === "/admin/update-user") return;
|
|
39
|
+
return { data: {
|
|
40
|
+
...data,
|
|
41
|
+
[opts.phoneNumber]: void 0
|
|
42
|
+
} };
|
|
43
|
+
} }
|
|
44
|
+
} } } };
|
|
29
45
|
},
|
|
30
46
|
hooks: { before: [{
|
|
31
47
|
matcher: (ctx) => ctx.path === "/update-user" && "phoneNumber" in ctx.body && ctx.body.phoneNumber !== null,
|
|
@@ -2,9 +2,10 @@ import { parseUserInput, parseUserOutput } from "../../db/schema.mjs";
|
|
|
2
2
|
import { getDate } from "../../utils/date.mjs";
|
|
3
3
|
import { generateRandomString } from "../../crypto/random.mjs";
|
|
4
4
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { getAuthoritativeSessionFromCtx } from "../../api/routes/session.mjs";
|
|
6
6
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
7
7
|
import { assertPasswordNotTooLong, assertPasswordNotTooShort, resetCredentialPassword } from "../../utils/password.mjs";
|
|
8
|
+
import { isSessionOutsideStore } from "../../api/state/session-source.mjs";
|
|
8
9
|
import { PHONE_NUMBER_ERROR_CODES } from "./error-codes.mjs";
|
|
9
10
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
10
11
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -69,7 +70,7 @@ const signInPhoneNumber = (opts) => createAuthEndpoint("/sign-in/phone-number",
|
|
|
69
70
|
const otp = generateOTP(opts.otpLength);
|
|
70
71
|
await ctx.context.internalAdapter.createVerificationValue({
|
|
71
72
|
value: otp,
|
|
72
|
-
identifier: phoneNumber,
|
|
73
|
+
identifier: phoneNumberOTPIdentifier(phoneNumber),
|
|
73
74
|
expiresAt: getDate(opts.expiresIn, "sec")
|
|
74
75
|
});
|
|
75
76
|
if (opts.sendOTP) await ctx.context.runInBackgroundOrAwait(opts.sendOTP({
|
|
@@ -155,7 +156,7 @@ const sendPhoneNumberOTP = (opts) => createAuthEndpoint("/phone-number/send-otp"
|
|
|
155
156
|
const code = generateOTP(opts.otpLength);
|
|
156
157
|
await ctx.context.internalAdapter.createVerificationValue({
|
|
157
158
|
value: `${code}:0`,
|
|
158
|
-
identifier: ctx.body.phoneNumber,
|
|
159
|
+
identifier: phoneNumberOTPIdentifier(ctx.body.phoneNumber),
|
|
159
160
|
expiresAt: getDate(opts.expiresIn, "sec")
|
|
160
161
|
});
|
|
161
162
|
const sendOTPResult = opts.sendOTP({
|
|
@@ -174,11 +175,12 @@ const sendPhoneNumberOTP = (opts) => createAuthEndpoint("/phone-number/send-otp"
|
|
|
174
175
|
});
|
|
175
176
|
async function verifyAndConsumePhoneNumberOTP(ctx, opts, body) {
|
|
176
177
|
const { phoneNumber, code } = body;
|
|
178
|
+
const identifier = phoneNumberOTPIdentifier(phoneNumber);
|
|
177
179
|
if (opts.verifyOTP) {
|
|
178
|
-
await verifyWithCustomVerifier(ctx, opts.verifyOTP, phoneNumber, code,
|
|
180
|
+
await verifyWithCustomVerifier(ctx, opts.verifyOTP, phoneNumber, code, identifier);
|
|
179
181
|
return;
|
|
180
182
|
}
|
|
181
|
-
await verifyPhoneNumberOTP(ctx, opts,
|
|
183
|
+
await verifyPhoneNumberOTP(ctx, opts, identifier, code);
|
|
182
184
|
}
|
|
183
185
|
/** Checks the code with `verifyOTP`, then drops the code stored under `identifier`. */
|
|
184
186
|
async function verifyWithCustomVerifier(ctx, verifyOTP, phoneNumber, code, identifier) {
|
|
@@ -330,7 +332,8 @@ const verifyPhoneNumber = (opts) => createAuthEndpoint("/phone-number/verify", {
|
|
|
330
332
|
}, async (ctx) => {
|
|
331
333
|
await verifyAndConsumePhoneNumberOTP(ctx, opts, ctx.body);
|
|
332
334
|
if (ctx.body.updatePhoneNumber) {
|
|
333
|
-
const
|
|
335
|
+
const requestSession = ctx.context.session;
|
|
336
|
+
const session = requestSession && await isSessionOutsideStore(requestSession.session.token) ? requestSession : await getAuthoritativeSessionFromCtx(ctx);
|
|
334
337
|
if (!session) throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
335
338
|
if ((await ctx.context.adapter.findMany({
|
|
336
339
|
model: "user",
|
|
@@ -429,10 +432,10 @@ const requestPasswordResetPhoneNumber = (opts) => createAuthEndpoint("/phone-num
|
|
|
429
432
|
const code = generateOTP(opts.otpLength);
|
|
430
433
|
await ctx.context.internalAdapter.createVerificationValue({
|
|
431
434
|
value: `${code}:0`,
|
|
432
|
-
identifier:
|
|
435
|
+
identifier: phoneNumberResetIdentifier(ctx.body.phoneNumber),
|
|
433
436
|
expiresAt: getDate(opts.expiresIn, "sec")
|
|
434
437
|
});
|
|
435
|
-
if (!user) return ctx.json({ status: true });
|
|
438
|
+
if (!user || opts.requireVerification && !user.phoneNumberVerified) return ctx.json({ status: true });
|
|
436
439
|
if (opts.sendPasswordResetOTP) await ctx.context.runInBackgroundOrAwait(opts.sendPasswordResetOTP({
|
|
437
440
|
phoneNumber: ctx.body.phoneNumber,
|
|
438
441
|
code
|
|
@@ -463,17 +466,11 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
|
|
|
463
466
|
} }
|
|
464
467
|
} }
|
|
465
468
|
}, async (ctx) => {
|
|
466
|
-
const phoneResetIdentifier =
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
await verifyWithCustomVerifier(ctx, opts.verifyOTP, ctx.body.phoneNumber, ctx.body.otp, phoneResetIdentifier);
|
|
472
|
-
otpConsumed = true;
|
|
473
|
-
} else if (!await matchesUsableStoredPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp)) {
|
|
474
|
-
await verifyPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp);
|
|
475
|
-
otpConsumed = true;
|
|
476
|
-
}
|
|
469
|
+
const phoneResetIdentifier = phoneNumberResetIdentifier(ctx.body.phoneNumber);
|
|
470
|
+
assertPasswordNotTooShort(ctx, ctx.body.newPassword);
|
|
471
|
+
assertPasswordNotTooLong(ctx, ctx.body.newPassword);
|
|
472
|
+
if (opts.verifyOTP) await verifyWithCustomVerifier(ctx, opts.verifyOTP, ctx.body.phoneNumber, ctx.body.otp, phoneResetIdentifier);
|
|
473
|
+
else await verifyPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp, "restore");
|
|
477
474
|
const user = await ctx.context.adapter.findOne({
|
|
478
475
|
model: "user",
|
|
479
476
|
where: [{
|
|
@@ -482,10 +479,9 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
|
|
|
482
479
|
}]
|
|
483
480
|
});
|
|
484
481
|
if (!user) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.UNEXPECTED_ERROR);
|
|
485
|
-
|
|
486
|
-
assertPasswordNotTooLong(ctx, ctx.body.newPassword);
|
|
482
|
+
if (opts.requireVerification && !user.phoneNumberVerified) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.PHONE_NUMBER_PENDING_VERIFICATION);
|
|
487
483
|
const hashedPassword = await ctx.context.password.hash(ctx.body.newPassword);
|
|
488
|
-
await resetCredentialPassword(ctx, user.id, hashedPassword,
|
|
484
|
+
await resetCredentialPassword(ctx, user.id, hashedPassword, opts.verifyOTP ? void 0 : () => verifyPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp));
|
|
489
485
|
if (ctx.context.options.emailAndPassword?.onPasswordReset) await ctx.context.options.emailAndPassword.onPasswordReset({ user }, ctx.request);
|
|
490
486
|
return ctx.json({ status: true });
|
|
491
487
|
});
|
|
@@ -497,9 +493,10 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
|
|
|
497
493
|
* never satisfy two simultaneous verifications. On a wrong code that is still
|
|
498
494
|
* within the attempt budget, the row is recreated with the same value and
|
|
499
495
|
* expiry and an incremented attempt counter. Once the budget is exhausted the
|
|
500
|
-
* row is not recreated.
|
|
496
|
+
* row is not recreated. With `"restore"`, a correct code is written back
|
|
497
|
+
* unchanged so a follow-up call can spend it.
|
|
501
498
|
*/
|
|
502
|
-
async function verifyPhoneNumberOTP(ctx, opts, identifier, providedCode) {
|
|
499
|
+
async function verifyPhoneNumberOTP(ctx, opts, identifier, providedCode, successDisposition = "consume") {
|
|
503
500
|
const existing = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
504
501
|
if (!existing) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.OTP_NOT_FOUND);
|
|
505
502
|
if (existing.expiresAt < /* @__PURE__ */ new Date()) {
|
|
@@ -524,22 +521,24 @@ async function verifyPhoneNumberOTP(ctx, opts, identifier, providedCode) {
|
|
|
524
521
|
});
|
|
525
522
|
throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_OTP);
|
|
526
523
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (!existing || existing.expiresAt < /* @__PURE__ */ new Date()) return false;
|
|
535
|
-
const [otpValue, rawAttempts] = existing.value.split(":");
|
|
536
|
-
if (parseVerificationAttempts(rawAttempts) >= (opts?.allowedAttempts ?? 3)) return false;
|
|
537
|
-
return otpValue === providedCode;
|
|
524
|
+
if (successDisposition === "consume") return;
|
|
525
|
+
if (await ctx.context.internalAdapter.findVerificationValue(identifier)) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_OTP);
|
|
526
|
+
if (!await ctx.context.internalAdapter.createVerificationValue({
|
|
527
|
+
value: consumed.value,
|
|
528
|
+
identifier,
|
|
529
|
+
expiresAt: consumed.expiresAt
|
|
530
|
+
})) throw APIError.fromStatus("INTERNAL_SERVER_ERROR", { message: "Unable to complete OTP verification" });
|
|
538
531
|
}
|
|
539
532
|
function parseVerificationAttempts(value) {
|
|
540
533
|
const attempts = Number(value ?? 0);
|
|
541
534
|
return Number.isSafeInteger(attempts) && attempts > 0 ? attempts : 0;
|
|
542
535
|
}
|
|
536
|
+
function phoneNumberOTPIdentifier(phoneNumber) {
|
|
537
|
+
return `phone-number-otp-${phoneNumber}`;
|
|
538
|
+
}
|
|
539
|
+
function phoneNumberResetIdentifier(phoneNumber) {
|
|
540
|
+
return `phone-number-reset-${phoneNumber}`;
|
|
541
|
+
}
|
|
543
542
|
function generateOTP(size) {
|
|
544
543
|
return generateRandomString(size, "0-9");
|
|
545
544
|
}
|
|
@@ -4,6 +4,7 @@ import { TOTPOptions, totp2fa } from "./totp/index.mjs";
|
|
|
4
4
|
import { TwoFactorOptions, TwoFactorProvider, TwoFactorTable, UserWithTwoFactor } from "./types.mjs";
|
|
5
5
|
import { TWO_FACTOR_ERROR_CODES } from "./error-code.mjs";
|
|
6
6
|
import { twoFactorClient } from "./client.mjs";
|
|
7
|
+
import { GenericEndpointContext } from "@better-auth/core";
|
|
7
8
|
import * as z from "zod";
|
|
8
9
|
//#region src/plugins/two-factor/index.d.ts
|
|
9
10
|
declare module "@better-auth/core" {
|
|
@@ -16,6 +17,12 @@ declare module "@better-auth/core" {
|
|
|
16
17
|
export declare const twoFactor: <O extends TwoFactorOptions>(options?: O) => {
|
|
17
18
|
id: "two-factor";
|
|
18
19
|
version: string;
|
|
20
|
+
/**
|
|
21
|
+
* Whether this sign-in still requires a second factor.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
shouldRequireTwoFactor: (ctx: GenericEndpointContext) => Promise<boolean>;
|
|
19
26
|
endpoints: {
|
|
20
27
|
/**
|
|
21
28
|
* ### Endpoint
|
|
@@ -37,15 +44,15 @@ export declare const twoFactor: <O extends TwoFactorOptions>(options?: O) => {
|
|
|
37
44
|
body: z.ZodObject<{
|
|
38
45
|
password: z.ZodOptional<z.ZodString>;
|
|
39
46
|
method: z.ZodDefault<z.ZodEnum<{
|
|
40
|
-
otp: "otp";
|
|
41
47
|
totp: "totp";
|
|
48
|
+
otp: "otp";
|
|
42
49
|
}>>;
|
|
43
50
|
issuer: z.ZodOptional<z.ZodString>;
|
|
44
51
|
}, z.core.$strip> | z.ZodObject<{
|
|
45
52
|
password: z.ZodString;
|
|
46
53
|
method: z.ZodDefault<z.ZodEnum<{
|
|
47
|
-
otp: "otp";
|
|
48
54
|
totp: "totp";
|
|
55
|
+
otp: "otp";
|
|
49
56
|
}>>;
|
|
50
57
|
issuer: z.ZodOptional<z.ZodString>;
|
|
51
58
|
}, z.core.$strip>;
|
|
@@ -2,8 +2,9 @@ import { mergeSchema } from "../../db/schema.mjs";
|
|
|
2
2
|
import { generateRandomString } from "../../crypto/random.mjs";
|
|
3
3
|
import { symmetricEncrypt } from "../../crypto/index.mjs";
|
|
4
4
|
import { deleteSessionCookie, expireCookie, setSessionCookie } from "../../cookies/index.mjs";
|
|
5
|
-
import { sensitiveSessionMiddleware, sessionMiddleware } from "../../api/routes/session.mjs";
|
|
5
|
+
import { getAuthoritativeSessionFromCtx, sensitiveSessionMiddleware, sessionMiddleware } from "../../api/routes/session.mjs";
|
|
6
6
|
import { shouldRequirePassword, validatePassword } from "../../utils/password.mjs";
|
|
7
|
+
import { isSessionOutsideStore } from "../../api/state/session-source.mjs";
|
|
7
8
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
8
9
|
import { TWO_FACTOR_ERROR_CODES } from "./error-code.mjs";
|
|
9
10
|
import { twoFactorClient } from "./client.mjs";
|
|
@@ -18,6 +19,39 @@ import * as z from "zod";
|
|
|
18
19
|
import { createHMAC } from "@better-auth/utils/hmac";
|
|
19
20
|
import { createOTP } from "@better-auth/utils/otp";
|
|
20
21
|
//#region src/plugins/two-factor/index.ts
|
|
22
|
+
const signInPaths = /* @__PURE__ */ new Set([
|
|
23
|
+
"/sign-in/email",
|
|
24
|
+
"/sign-in/username",
|
|
25
|
+
"/sign-in/phone-number"
|
|
26
|
+
]);
|
|
27
|
+
const signInTwoFactorStates = /* @__PURE__ */ new WeakMap();
|
|
28
|
+
async function readSignInTwoFactorState(ctx, trustDeviceMaxAge) {
|
|
29
|
+
const data = ctx.context.newSession;
|
|
30
|
+
if (!signInPaths.has(ctx.path) || !data?.user.twoFactorEnabled) return {
|
|
31
|
+
requiresTwoFactor: false,
|
|
32
|
+
trustDeviceCookie: null
|
|
33
|
+
};
|
|
34
|
+
const cookie = ctx.context.createAuthCookie(TRUST_DEVICE_COOKIE_NAME, { maxAge: trustDeviceMaxAge });
|
|
35
|
+
const trustDeviceCookie = await ctx.getSignedCookie(cookie.name, ctx.context.secret);
|
|
36
|
+
if (!trustDeviceCookie) return {
|
|
37
|
+
requiresTwoFactor: true,
|
|
38
|
+
trustDeviceCookie
|
|
39
|
+
};
|
|
40
|
+
const [token, trustIdentifier] = trustDeviceCookie.split("!");
|
|
41
|
+
if (!token || !trustIdentifier) return {
|
|
42
|
+
requiresTwoFactor: true,
|
|
43
|
+
trustDeviceCookie
|
|
44
|
+
};
|
|
45
|
+
if (token !== await createHMAC("SHA-256", "base64urlnopad").sign(ctx.context.secret, `${data.user.id}!${trustIdentifier}`)) return {
|
|
46
|
+
requiresTwoFactor: true,
|
|
47
|
+
trustDeviceCookie
|
|
48
|
+
};
|
|
49
|
+
const verificationRecord = await ctx.context.internalAdapter.findVerificationValue(trustIdentifier);
|
|
50
|
+
return {
|
|
51
|
+
requiresTwoFactor: !verificationRecord || verificationRecord.value !== data.user.id || !(verificationRecord.expiresAt > /* @__PURE__ */ new Date()),
|
|
52
|
+
trustDeviceCookie
|
|
53
|
+
};
|
|
54
|
+
}
|
|
21
55
|
const twoFactor = (options) => {
|
|
22
56
|
const opts = { twoFactorTable: "twoFactor" };
|
|
23
57
|
const trustDeviceMaxAge = options?.trustDeviceMaxAge ?? 2592e3;
|
|
@@ -48,9 +82,25 @@ const twoFactor = (options) => {
|
|
|
48
82
|
issuer: issuerField
|
|
49
83
|
});
|
|
50
84
|
const disableTwoFactorBodySchema = allowPasswordless ? z.object({ password: passwordSchema.optional() }) : z.object({ password: passwordSchema });
|
|
85
|
+
function getSignInTwoFactorState(ctx) {
|
|
86
|
+
const remembered = signInTwoFactorStates.get(ctx.context);
|
|
87
|
+
if (remembered) return remembered;
|
|
88
|
+
const state = readSignInTwoFactorState(ctx, trustDeviceMaxAge);
|
|
89
|
+
signInTwoFactorStates.set(ctx.context, state);
|
|
90
|
+
return state;
|
|
91
|
+
}
|
|
92
|
+
async function shouldRequireTwoFactor(ctx) {
|
|
93
|
+
return (await getSignInTwoFactorState(ctx)).requiresTwoFactor;
|
|
94
|
+
}
|
|
51
95
|
return {
|
|
52
96
|
id: "two-factor",
|
|
53
97
|
version: PACKAGE_VERSION,
|
|
98
|
+
/**
|
|
99
|
+
* Whether this sign-in still requires a second factor.
|
|
100
|
+
*
|
|
101
|
+
* @internal
|
|
102
|
+
*/
|
|
103
|
+
shouldRequireTwoFactor,
|
|
54
104
|
endpoints: {
|
|
55
105
|
...totp.endpoints,
|
|
56
106
|
...otp.endpoints,
|
|
@@ -102,7 +152,12 @@ const twoFactor = (options) => {
|
|
|
102
152
|
} }
|
|
103
153
|
} }
|
|
104
154
|
}, async (ctx) => {
|
|
105
|
-
const
|
|
155
|
+
const session = await isSessionOutsideStore(ctx.context.session.session.token) ? ctx.context.session : await getAuthoritativeSessionFromCtx(ctx);
|
|
156
|
+
if (!session) throw APIError.from("UNAUTHORIZED", {
|
|
157
|
+
message: "Unauthorized",
|
|
158
|
+
code: "UNAUTHORIZED"
|
|
159
|
+
});
|
|
160
|
+
const user = session.user;
|
|
106
161
|
const { password, issuer, method } = ctx.body;
|
|
107
162
|
if (await shouldRequirePassword(ctx, user.id, allowPasswordless)) {
|
|
108
163
|
if (!password) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_PASSWORD);
|
|
@@ -246,36 +301,28 @@ const twoFactor = (options) => {
|
|
|
246
301
|
options,
|
|
247
302
|
hooks: { after: [{
|
|
248
303
|
matcher(context) {
|
|
249
|
-
return
|
|
304
|
+
return signInPaths.has(context.path ?? "");
|
|
250
305
|
},
|
|
251
306
|
handler: createAuthMiddleware(async (ctx) => {
|
|
252
307
|
const data = ctx.context.newSession;
|
|
253
|
-
if (!data) return;
|
|
254
308
|
if (!data?.user.twoFactorEnabled) return;
|
|
309
|
+
const { requiresTwoFactor, trustDeviceCookie } = await getSignInTwoFactorState(ctx);
|
|
255
310
|
const trustDeviceCookieAttrs = ctx.context.createAuthCookie(TRUST_DEVICE_COOKIE_NAME, { maxAge: trustDeviceMaxAge });
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
expiresAt: new Date(Date.now() + trustDeviceMaxAge * 1e3)
|
|
270
|
-
});
|
|
271
|
-
const newTrustDeviceCookie = ctx.context.createAuthCookie(TRUST_DEVICE_COOKIE_NAME, { maxAge: trustDeviceMaxAge });
|
|
272
|
-
await ctx.setSignedCookie(newTrustDeviceCookie.name, `${newToken}!${newTrustIdentifier}`, ctx.context.secret, trustDeviceCookieAttrs.attributes);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
expireCookie(ctx, trustDeviceCookieAttrs);
|
|
311
|
+
if (!requiresTwoFactor && trustDeviceCookie) {
|
|
312
|
+
const trustIdentifier = trustDeviceCookie.split("!")[1];
|
|
313
|
+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(trustIdentifier);
|
|
314
|
+
const newTrustIdentifier = `trust-device-${generateRandomString(32)}`;
|
|
315
|
+
const newToken = await createHMAC("SHA-256", "base64urlnopad").sign(ctx.context.secret, `${data.user.id}!${newTrustIdentifier}`);
|
|
316
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
317
|
+
value: data.user.id,
|
|
318
|
+
identifier: newTrustIdentifier,
|
|
319
|
+
expiresAt: new Date(Date.now() + trustDeviceMaxAge * 1e3)
|
|
320
|
+
});
|
|
321
|
+
const newTrustDeviceCookie = ctx.context.createAuthCookie(TRUST_DEVICE_COOKIE_NAME, { maxAge: trustDeviceMaxAge });
|
|
322
|
+
await ctx.setSignedCookie(newTrustDeviceCookie.name, `${newToken}!${newTrustIdentifier}`, ctx.context.secret, trustDeviceCookieAttrs.attributes);
|
|
323
|
+
return;
|
|
278
324
|
}
|
|
325
|
+
if (trustDeviceCookie) expireCookie(ctx, trustDeviceCookieAttrs);
|
|
279
326
|
/**
|
|
280
327
|
* Remove the session cookie set by the credential sign-in.
|
|
281
328
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseUserOutput } from "../../db/schema.mjs";
|
|
2
2
|
import { generateRandomString } from "../../crypto/random.mjs";
|
|
3
3
|
import { expireCookie, setSessionCookie } from "../../cookies/index.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { getAuthoritativeSessionFromCtx } from "../../api/routes/session.mjs";
|
|
5
5
|
import { TWO_FACTOR_ERROR_CODES } from "./error-code.mjs";
|
|
6
6
|
import { TRUST_DEVICE_COOKIE_NAME, TWO_FACTOR_COOKIE_NAME } from "./constant.mjs";
|
|
7
7
|
import { APIError } from "@better-auth/core/error";
|
|
@@ -11,7 +11,7 @@ async function verifyTwoFactor(ctx) {
|
|
|
11
11
|
const invalid = (errorKey) => {
|
|
12
12
|
throw APIError.from("UNAUTHORIZED", TWO_FACTOR_ERROR_CODES[errorKey]);
|
|
13
13
|
};
|
|
14
|
-
const session = await
|
|
14
|
+
const session = await getAuthoritativeSessionFromCtx(ctx);
|
|
15
15
|
if (!session) {
|
|
16
16
|
const twoFactorCookie = ctx.context.createAuthCookie(TWO_FACTOR_COOKIE_NAME);
|
|
17
17
|
const signedTwoFactorCookie = await ctx.getSignedCookie(twoFactorCookie.name, ctx.context.secret);
|
|
@@ -155,24 +155,14 @@ const usernameImpl = (options, includeDisplayUsername) => {
|
|
|
155
155
|
ctx.context.logger.warn("Username or password not found");
|
|
156
156
|
throw APIError.from("UNAUTHORIZED", USERNAME_ERROR_CODES.INVALID_USERNAME_OR_PASSWORD);
|
|
157
157
|
}
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
const maxUsernameLength = options?.maxUsernameLength || 30;
|
|
161
|
-
if (username.length < minUsernameLength) {
|
|
162
|
-
ctx.context.logger.warn("Username too short");
|
|
163
|
-
throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.USERNAME_TOO_SHORT);
|
|
164
|
-
}
|
|
165
|
-
if (username.length > maxUsernameLength) {
|
|
166
|
-
ctx.context.logger.warn("Username too long");
|
|
167
|
-
throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.USERNAME_TOO_LONG);
|
|
168
|
-
}
|
|
169
|
-
if (!await (options?.usernameValidator || defaultUsernameValidator)(username)) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.INVALID_USERNAME);
|
|
158
|
+
const validationError = await validateUsernameValue(ctx.body.username);
|
|
159
|
+
if (validationError) throw APIError.from("UNPROCESSABLE_ENTITY", validationError);
|
|
170
160
|
assertPasswordNotTooLong(ctx, ctx.body.password);
|
|
171
161
|
const user = await ctx.context.adapter.findOne({
|
|
172
162
|
model: "user",
|
|
173
163
|
where: [{
|
|
174
164
|
field: "username",
|
|
175
|
-
value: normalizer(username)
|
|
165
|
+
value: normalizer(ctx.body.username)
|
|
176
166
|
}]
|
|
177
167
|
});
|
|
178
168
|
if (!user) {
|
|
@@ -197,7 +187,7 @@ const usernameImpl = (options, includeDisplayUsername) => {
|
|
|
197
187
|
if (ctx.context.options?.emailAndPassword?.requireEmailVerification && !user.emailVerified) {
|
|
198
188
|
if (!ctx.context.options?.emailVerification?.sendVerificationEmail) throw APIError.from("FORBIDDEN", USERNAME_ERROR_CODES.EMAIL_NOT_VERIFIED);
|
|
199
189
|
if (ctx.context.options?.emailVerification?.sendOnSignIn) {
|
|
200
|
-
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
190
|
+
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: user.id });
|
|
201
191
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
|
|
202
192
|
await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
|
|
203
193
|
user,
|
|
@@ -227,11 +217,8 @@ const usernameImpl = (options, includeDisplayUsername) => {
|
|
|
227
217
|
}, async (ctx) => {
|
|
228
218
|
const username = ctx.body.username;
|
|
229
219
|
if (!username) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.INVALID_USERNAME);
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
if (username.length < minUsernameLength) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.USERNAME_TOO_SHORT);
|
|
233
|
-
if (username.length > maxUsernameLength) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.USERNAME_TOO_LONG);
|
|
234
|
-
if (!await (options?.usernameValidator || defaultUsernameValidator)(username)) throw APIError.from("UNPROCESSABLE_ENTITY", USERNAME_ERROR_CODES.INVALID_USERNAME);
|
|
220
|
+
const validationError = await validateUsernameValue(username);
|
|
221
|
+
if (validationError) throw APIError.from("UNPROCESSABLE_ENTITY", validationError);
|
|
235
222
|
if (await ctx.context.adapter.findOne({
|
|
236
223
|
model: "user",
|
|
237
224
|
where: [{
|