Merge remote-tracking branch 'origin/main' into v1.3

This commit is contained in:
Bereket Engida
2025-06-27 22:57:26 -07:00
11 changed files with 509 additions and 47 deletions

View File

@@ -159,38 +159,38 @@ export default function UserCard(props: {
Please verify your email address. Check your inbox for the Please verify your email address. Check your inbox for the
verification email. If you haven't received the email, click the verification email. If you haven't received the email, click the
button below to resend. button below to resend.
<Button
size="sm"
variant="secondary"
className="mt-2"
onClick={async () => {
await client.sendVerificationEmail(
{
email: session?.user.email || "",
},
{
onRequest(context) {
setEmailVerificationPending(true);
},
onError(context) {
toast.error(context.error.message);
setEmailVerificationPending(false);
},
onSuccess() {
toast.success("Verification email sent successfully");
setEmailVerificationPending(false);
},
},
);
}}
>
{emailVerificationPending ? (
<Loader2 size={15} className="animate-spin" />
) : (
"Resend Verification Email"
)}
</Button>
</AlertDescription> </AlertDescription>
<Button
size="sm"
variant="secondary"
className="mt-2"
onClick={async () => {
await client.sendVerificationEmail(
{
email: session?.user.email || "",
},
{
onRequest(context) {
setEmailVerificationPending(true);
},
onError(context) {
toast.error(context.error.message);
setEmailVerificationPending(false);
},
onSuccess() {
toast.success("Verification email sent successfully");
setEmailVerificationPending(false);
},
},
);
}}
>
{emailVerificationPending ? (
<Loader2 size={15} className="animate-spin" />
) : (
"Resend Verification Email"
)}
</Button>
</Alert> </Alert>
)} )}

View File

@@ -597,7 +597,7 @@ await authClient.organization.updateMemberRole({
### Get Active Member ### Get Active Member
To get the current member of the organization you can use the `organization.getActiveMember` function. This function will return the current active member. To get the member details of the active organization you can use the `organization.getActiveMember` function.
```ts title="auth-client.ts" ```ts title="auth-client.ts"
const member = await authClient.organization.getActiveMember() const member = await authClient.organization.getActiveMember()

View File

@@ -674,7 +674,7 @@
"@simplewebauthn/server": "^13.0.0", "@simplewebauthn/server": "^13.0.0",
"better-call": "catalog:", "better-call": "catalog:",
"defu": "^6.1.4", "defu": "^6.1.4",
"jose": "^5.9.6", "jose": "^6.0.11",
"kysely": "^0.28.2", "kysely": "^0.28.2",
"nanostores": "^0.11.3", "nanostores": "^0.11.3",
"zod": "^3.24.1" "zod": "^3.24.1"

View File

@@ -159,11 +159,20 @@ export const createAdapter =
* then we should return the model name ending with an `s`. * then we should return the model name ending with an `s`.
*/ */
const getModelName = (model: string) => { const getModelName = (model: string) => {
return schema[getDefaultModelName(model)].modelName !== model const defaultModelKey = getDefaultModelName(model);
? schema[getDefaultModelName(model)].modelName const usePlural = config && config.usePlural;
: config.usePlural const useCustomModelName =
? `${model}s` schema &&
: model; schema[defaultModelKey] &&
schema[defaultModelKey].modelName !== model;
if (useCustomModelName) {
return usePlural
? `${schema[defaultModelKey].modelName}s`
: schema[defaultModelKey].modelName;
}
return usePlural ? `${model}s` : model;
}; };
/** /**
* Get the field name which is expected to be saved in the database based on the user's schema. * Get the field name which is expected to be saved in the database based on the user's schema.
@@ -477,6 +486,8 @@ export const createAdapter =
? undefined ? undefined
: CleanedWhere[] => { : CleanedWhere[] => {
if (!where) return undefined as any; if (!where) return undefined as any;
const newMappedKeys = config.mapKeysTransformInput ?? {};
return where.map((w) => { return where.map((w) => {
const { const {
field: unsafe_field, field: unsafe_field,
@@ -495,11 +506,13 @@ export const createAdapter =
field: unsafe_field, field: unsafe_field,
model, model,
}); });
const fieldName: string =
newMappedKeys[defaultFieldName] ||
getFieldName({
field: defaultFieldName,
model: defaultModelName,
});
const fieldName = getFieldName({
field: defaultFieldName,
model: defaultModelName,
});
const fieldAttr = getFieldAttributes({ const fieldAttr = getFieldAttributes({
field: defaultFieldName, field: defaultFieldName,
model: defaultModelName, model: defaultModelName,

View File

@@ -1,6 +1,10 @@
import { describe, test, expect } from "vitest"; import { describe, test, expect } from "vitest";
import { createAdapter } from ".."; import { createAdapter } from "..";
import type { AdapterConfig, CreateCustomAdapter } from "../types"; import type {
AdapterConfig,
CleanedWhere,
CreateCustomAdapter,
} from "../types";
import type { BetterAuthOptions, User, Where } from "../../../types"; import type { BetterAuthOptions, User, Where } from "../../../types";
import { betterAuth } from "../../../auth"; import { betterAuth } from "../../../auth";
@@ -161,6 +165,31 @@ describe("Create Adapter Helper", async () => {
expect(res.id).toBe("HARD-CODED-ID"); expect(res.id).toBe("HARD-CODED-ID");
}); });
test("Should not generate an id if `advanced.database.generateId` is not defined or false", async () => {
const adapter = await createTestAdapter({
config: {},
options: {
advanced: {
database: { generateId: false, useNumberId: false },
},
},
adapter(args_0) {
return {
async create(data) {
expect(data.data.id).not.toBeDefined();
return data.data;
},
};
},
});
const testResult = await adapter.create({
model: "user",
data: { name: "test-name" },
});
expect(testResult.id).not.toBeDefined();
});
test("Should throw an error if the database doesn't support numeric ids and the user has enabled `useNumberId`", async () => { test("Should throw an error if the database doesn't support numeric ids and the user has enabled `useNumberId`", async () => {
let error: any | null = null; let error: any | null = null;
try { try {
@@ -559,6 +588,7 @@ describe("Create Adapter Helper", async () => {
}; };
}, },
}); });
const res = (await adapter.create({ const res = (await adapter.create({
model: "user", model: "user",
data: { email: "test@test.com" }, data: { email: "test@test.com" },
@@ -572,6 +602,33 @@ describe("Create Adapter Helper", async () => {
expect(parameters.data.email_address).toEqual("test@test.com"); expect(parameters.data.email_address).toEqual("test@test.com");
}); });
test("Should allow custom transform input to transform the where clause", async () => {
const parameters: CleanedWhere[] = await new Promise(async (r) => {
const adapter = await createTestAdapter({
config: {
debugLogs: {},
mapKeysTransformInput: {
id: "_id",
},
},
adapter(args_0) {
return {
async findOne({ model, where, select }) {
r(where);
return {} as any;
},
};
},
});
adapter.findOne({
model: "user",
where: [{ field: "id", value: "123" }],
});
});
expect(parameters[0]!.field).toEqual("_id");
});
test("Should allow custom map output key transformation", async () => { test("Should allow custom map output key transformation", async () => {
const parameters: { const parameters: {
data: { email: string }; data: { email: string };

View File

@@ -94,7 +94,10 @@ export const originCheck = (
if (pattern.includes("*")) { if (pattern.includes("*")) {
return wildcardMatch(pattern)(getHost(url)); return wildcardMatch(pattern)(getHost(url));
} }
return url.startsWith(pattern); const protocol = getProtocol(url);
return protocol === "http:" || protocol === "https:" || !protocol
? pattern === getOrigin(url)
: url.startsWith(pattern);
}; };
const validateURL = (url: string | undefined, label: string) => { const validateURL = (url: string | undefined, label: string) => {

View File

@@ -256,6 +256,7 @@ export const linkSocialAccount = createAuthEndpoint(
if (hasBeenLinked) { if (hasBeenLinked) {
return c.json({ return c.json({
redirect: false, redirect: false,
url: "", // this is for type inference
status: true, status: true,
}); });
} }
@@ -316,6 +317,7 @@ export const linkSocialAccount = createAuthEndpoint(
return c.json({ return c.json({
redirect: false, redirect: false,
url: "", // this is for type inference
status: true, status: true,
}); });
} }

View File

@@ -5,6 +5,8 @@ import { parseSetCookieHeader } from "../cookies";
let isBuilding: boolean | undefined; let isBuilding: boolean | undefined;
let isBuilding: boolean | undefined;
export const toSvelteKitHandler = (auth: { export const toSvelteKitHandler = (auth: {
handler: (request: Request) => any; handler: (request: Request) => any;
options: BetterAuthOptions; options: BetterAuthOptions;

View File

@@ -392,6 +392,10 @@ export const emailOTP = (options: EmailOTPOptions) => {
}, },
ctx, ctx,
); );
await ctx.context.options.emailVerification?.onEmailVerification?.(
updatedUser,
ctx.request,
);
if ( if (
ctx.context.options.emailVerification?.autoSignInAfterVerification ctx.context.options.emailVerification?.autoSignInAfterVerification

View File

@@ -430,7 +430,7 @@ export const getActiveMember = createAuthEndpoint(
use: [orgMiddleware, orgSessionMiddleware], use: [orgMiddleware, orgSessionMiddleware],
metadata: { metadata: {
openapi: { openapi: {
description: "Get the active member in the organization", description: "Get the member details of the active organization",
responses: { responses: {
"200": { "200": {
description: "Success", description: "Success",

385
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff