feat: add context to database hooks (#1180)

database hooks now contain endpoint context on their callback

---------

Co-authored-by: Bereket Engida <86073083+Bekacru@users.noreply.github.com>
This commit is contained in:
kzlar
2025-02-20 15:02:50 +03:00
committed by GitHub
co-authored by Bereket Engida
parent 6ff11bd5ae
commit 63fbd91077
18 changed files with 180 additions and 70 deletions
@@ -137,7 +137,7 @@ export const callbackOAuth = createAuthEndpoint(
userId: link.userId,
providerId: provider.id,
accountId: userInfo.id,
});
}, c);
if (!newAccount) {
return redirectOnError("unable_to_link_account");
}
@@ -247,7 +247,7 @@ export const verifyEmail = createAuthEndpoint(
{
email: parsed.updateTo,
emailVerified: false,
},
},ctx
);
const newToken = await createEmailVerificationToken(
@@ -292,7 +292,7 @@ export const verifyEmail = createAuthEndpoint(
}
await ctx.context.internalAdapter.updateUserByEmail(parsed.email, {
emailVerified: true,
});
},ctx);
if (ctx.context.options.emailVerification?.autoSignInAfterVerification) {
const currentSession = await getSessionFromCtx(ctx);
if (!currentSession || currentSession.user.email !== parsed.email) {
@@ -268,12 +268,12 @@ export const resetPassword = createAuthEndpoint(
providerId: "credential",
password: hashedPassword,
accountId: userId,
});
}, ctx);
return ctx.json({
status: true,
});
}
await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
await ctx.context.internalAdapter.updatePassword(userId, hashedPassword, ctx);
return ctx.json({
status: true,
});
@@ -151,7 +151,7 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
image,
...additionalData,
emailVerified: false,
});
}, ctx);
if (!createdUser) {
throw new APIError("BAD_REQUEST", {
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
@@ -180,7 +180,7 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
providerId: "credential",
accountId: createdUser.id,
password: hash,
});
}, ctx);
if (
ctx.context.options.emailVerification?.sendOnSignUp ||
ctx.context.options.emailAndPassword.requireEmailVerification
@@ -100,7 +100,7 @@ export const updateUser = <O extends BetterAuthOptions>() =>
name,
image,
...additionalFields,
},
},ctx
);
/**
* Update the session cookie with the new user data
@@ -294,7 +294,7 @@ export const setPassword = createAuthEndpoint(
providerId: "credential",
accountId: session.user.id,
password: passwordHash,
});
}, ctx);
return ctx.json({
status: true,
});
@@ -582,7 +582,7 @@ export const changeEmail = createAuthEndpoint(
ctx.context.session.user.email,
{
email: ctx.body.newEmail,
},
},ctx
);
return ctx.json({
status: true,
@@ -1,6 +1,8 @@
import { beforeAll, expect, it, describe, vi, afterEach } from "vitest";
import type { BetterAuthOptions, BetterAuthPlugin } from "../types";
import Database from "better-sqlite3";
import { createInternalAdapter } from "./internal-adapter";
import { getAdapter } from "./utils";
import { init } from "../init";
import { getMigrations } from "./get-migration";
import { SqliteDialect } from "kysely";
@@ -12,10 +14,10 @@ describe("adapter test", async () => {
});
const map = new Map();
let id = 1;
const pluginHookUserCreateBefore = vi.fn();
const pluginHookUserCreateAfter = vi.fn();
const hookUserCreateBefore = vi.fn();
const hookUserCreateAfter = vi.fn();
const pluginHookUserCreateBefore = vi.fn();
const pluginHookUserCreateAfter = vi.fn();
const opts = {
database: {
dialect: sqliteDialect,
@@ -46,12 +48,12 @@ describe("adapter test", async () => {
databaseHooks: {
user: {
create: {
async before(user) {
hookUserCreateBefore(user);
async before(user, context) {
hookUserCreateBefore(user, context);
return { data: user };
},
async after(user) {
hookUserCreateAfter(user);
async after(user, context) {
hookUserCreateAfter(user, context);
return;
},
},
@@ -66,13 +68,12 @@ describe("adapter test", async () => {
databaseHooks: {
user: {
create: {
async before(user) {
pluginHookUserCreateBefore(user);
async before(user, context) {
pluginHookUserCreateBefore(user, context);
return { data: user };
},
async after(user) {
pluginHookUserCreateAfter(user);
return;
async after(user, context) {
pluginHookUserCreateAfter(user, context);
},
},
},
@@ -10,7 +10,13 @@ import { getWithHooks } from "./with-hooks";
import { getIp } from "../utils/get-request-ip";
import { safeJSONParse } from "../utils/json";
import { generateId } from "../utils";
import type { Adapter, AuthContext, BetterAuthOptions, Where } from "../types";
import type {
Adapter,
AuthContext,
BetterAuthOptions,
GenericEndpointContext,
Where,
} from "../types";
export const createInternalAdapter = (
adapter: Adapter,
@@ -31,6 +37,7 @@ export const createInternalAdapter = (
user: Omit<User, "id" | "createdAt" | "updatedAt"> & Partial<User>,
account: Omit<Account, "userId" | "id" | "createdAt" | "updatedAt"> &
Partial<Account>,
context?: GenericEndpointContext,
) => {
const createdUser = await createWithHooks(
{
@@ -39,6 +46,8 @@ export const createInternalAdapter = (
...user,
},
"user",
undefined,
context,
);
const createdAccount = await createWithHooks(
{
@@ -48,6 +57,8 @@ export const createInternalAdapter = (
updatedAt: new Date(),
},
"account",
undefined,
context,
);
return {
user: createdUser,
@@ -58,6 +69,7 @@ export const createInternalAdapter = (
user: Omit<User, "id" | "createdAt" | "updatedAt" | "emailVerified"> &
Partial<User> &
Record<string, any>,
context?: GenericEndpointContext,
) => {
const createdUser = await createWithHooks(
{
@@ -68,6 +80,8 @@ export const createInternalAdapter = (
email: user.email.toLowerCase(),
},
"user",
undefined,
context,
);
return createdUser as T & User;
},
@@ -75,6 +89,7 @@ export const createInternalAdapter = (
account: Omit<Account, "id" | "createdAt" | "updatedAt"> &
Partial<Account> &
Record<string, any>,
context?: GenericEndpointContext,
) => {
const createdAccount = await createWithHooks(
{
@@ -83,6 +98,8 @@ export const createInternalAdapter = (
...account,
},
"account",
undefined,
context,
);
return createdAccount as T & Account;
},
@@ -190,6 +207,7 @@ export const createInternalAdapter = (
request: Request | Headers | undefined,
dontRememberMe?: boolean,
override?: Partial<Session> & Record<string, any>,
context?: GenericEndpointContext,
) => {
const headers = request instanceof Request ? request.headers : request;
const { id: _, ...rest } = override || {};
@@ -248,6 +266,7 @@ export const createInternalAdapter = (
executeMainFn: options.session?.storeSessionInDatabase,
}
: undefined,
context,
);
return res as Session;
},
@@ -384,6 +403,7 @@ export const createInternalAdapter = (
updateSession: async (
sessionToken: string,
session: Partial<Session> & Record<string, any>,
context?: GenericEndpointContext,
) => {
const updatedSession = await updateWithHooks<Session>(
session,
@@ -411,6 +431,7 @@ export const createInternalAdapter = (
executeMainFn: options.session?.storeSessionInDatabase,
}
: undefined,
context,
);
return updatedSession;
},
@@ -615,6 +636,7 @@ export const createInternalAdapter = (
linkAccount: async (
account: Omit<Account, "id" | "createdAt" | "updatedAt"> &
Partial<Account>,
context?: GenericEndpointContext,
) => {
const _account = await createWithHooks(
{
@@ -623,12 +645,15 @@ export const createInternalAdapter = (
updatedAt: new Date(),
},
"account",
undefined,
context,
);
return _account;
},
updateUser: async (
userId: string,
data: Partial<User> & Record<string, any>,
context?: GenericEndpointContext,
) => {
const user = await updateWithHooks<User>(
data,
@@ -639,12 +664,15 @@ export const createInternalAdapter = (
},
],
"user",
undefined,
context,
);
return user;
},
updateUserByEmail: async (
email: string,
data: Partial<User & Record<string, any>>,
context?: GenericEndpointContext,
) => {
const user = await updateWithHooks<User>(
data,
@@ -655,10 +683,12 @@ export const createInternalAdapter = (
},
],
"user",
undefined,
context,
);
return user;
},
updatePassword: async (userId: string, password: string) => {
updatePassword: async (userId: string, password: string, context?: GenericEndpointContext,) => {
await updateManyWithHooks(
{
password,
@@ -674,6 +704,8 @@ export const createInternalAdapter = (
},
],
"account",
undefined,
context
);
},
findAccounts: async (userId: string) => {
@@ -712,17 +744,24 @@ export const createInternalAdapter = (
});
return account;
},
updateAccount: async (accountId: string, data: Partial<Account>) => {
updateAccount: async (
accountId: string,
data: Partial<Account>,
context?: GenericEndpointContext,
) => {
const account = await updateWithHooks<Account>(
data,
[{ field: "id", value: accountId }],
"account",
undefined,
context,
);
return account;
},
createVerificationValue: async (
data: Omit<Verification, "createdAt" | "id" | "updatedAt"> &
Partial<Verification>,
context?: GenericEndpointContext,
) => {
const verification = await createWithHooks(
{
@@ -731,6 +770,8 @@ export const createInternalAdapter = (
...data,
},
"verification",
undefined,
context,
);
return verification as Verification;
},
@@ -789,11 +830,14 @@ export const createInternalAdapter = (
updateVerificationValue: async (
id: string,
data: Partial<Verification>,
context?: GenericEndpointContext,
) => {
const verification = await updateWithHooks<Verification>(
data,
[{ field: "id", value: id }],
"verification",
undefined,
context,
);
return verification;
},
+16 -7
View File
@@ -1,4 +1,10 @@
import type { Adapter, BetterAuthOptions, Models, Where } from "../types";
import type {
Adapter,
BetterAuthOptions,
GenericEndpointContext,
Models,
Where,
} from "../types";
export function getWithHooks(
adapter: Adapter,
@@ -19,12 +25,13 @@ export function getWithHooks(
fn: (data: Record<string, any>) => void | Promise<any>;
executeMainFn?: boolean;
},
context?: GenericEndpointContext,
) {
let actualData = data;
for (const hook of hooks || []) {
const toRun = hook[model]?.create?.before;
if (toRun) {
const result = await toRun(actualData as any);
const result = await toRun(actualData as any, context);
if (result === false) {
return null;
}
@@ -52,7 +59,7 @@ export function getWithHooks(
for (const hook of hooks || []) {
const toRun = hook[model]?.create?.after;
if (toRun) {
await toRun(created as any);
await toRun(created as any, context);
}
}
@@ -67,13 +74,14 @@ export function getWithHooks(
fn: (data: Record<string, any>) => void | Promise<any>;
executeMainFn?: boolean;
},
context?: GenericEndpointContext,
) {
let actualData = data;
for (const hook of hooks || []) {
const toRun = hook[model]?.update?.before;
if (toRun) {
const result = await toRun(data as any);
const result = await toRun(data as any, context);
if (result === false) {
return null;
}
@@ -98,7 +106,7 @@ export function getWithHooks(
for (const hook of hooks || []) {
const toRun = hook[model]?.update?.after;
if (toRun) {
await toRun(updated as any);
await toRun(updated as any, context);
}
}
return updated;
@@ -112,13 +120,14 @@ export function getWithHooks(
fn: (data: Record<string, any>) => void | Promise<any>;
executeMainFn?: boolean;
},
context?: GenericEndpointContext,
) {
let actualData = data;
for (const hook of hooks || []) {
const toRun = hook[model]?.update?.before;
if (toRun) {
const result = await toRun(data as any);
const result = await toRun(data as any, context);
if (result === false) {
return null;
}
@@ -143,7 +152,7 @@ export function getWithHooks(
for (const hook of hooks || []) {
const toRun = hook[model]?.update?.after;
if (toRun) {
await toRun(updated as any);
await toRun(updated as any, context);
}
}
@@ -69,7 +69,7 @@ export async function handleOAuthUserInfo(
accessTokenExpiresAt: account.accessTokenExpiresAt,
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
scope: account.scope,
});
}, c);
} catch (e) {
logger.error("Unable to link account", e);
return {
@@ -93,6 +93,7 @@ export async function handleOAuthUserInfo(
await c.context.internalAdapter.updateAccount(
hasBeenLinked.id,
updateData,
c
);
}
}
@@ -115,6 +116,7 @@ export async function handleOAuthUserInfo(
providerId: account.providerId,
accountId: userInfo.id.toString(),
},
c,
)
.then((res) => res?.user);
if (
@@ -142,7 +142,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
},
session: {
create: {
async before(session) {
async before(session, context) {
const user = (await ctx.internalAdapter.findUserById(
session.userId,
)) as UserWithRole;
@@ -156,7 +156,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
banned: false,
banReason: null,
banExpires: null,
});
},context);
return;
}
return false;
@@ -233,7 +233,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
ctx.body.userId,
{
role: ctx.body.role,
},
},ctx
);
return ctx.json({
user: updatedUser as UserWithRole,
@@ -323,7 +323,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
providerId: "credential",
password: hashedPassword,
userId: user.id,
});
}, ctx);
return ctx.json({
user: user as UserWithRole,
});
@@ -562,7 +562,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
ctx.body.userId,
{
banned: false,
},
},ctx
);
return ctx.json({
user: user,
@@ -637,7 +637,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
: options?.defaultBanExpiresIn
? getDate(options.defaultBanExpiresIn, "sec")
: undefined,
},
},ctx
);
//revoke all sessions
await ctx.context.internalAdapter.deleteSessions(ctx.body.userId);
@@ -113,7 +113,7 @@ export const anonymous = (options?: AnonymousOptions) => {
name: "Anonymous",
createdAt: new Date(),
updatedAt: new Date(),
});
}, ctx);
if (!newUser) {
throw ctx.error("INTERNAL_SERVER_ERROR", {
message: ERROR_CODES.FAILED_TO_CREATE_USER,
@@ -324,7 +324,7 @@ export const emailOTP = (options: EmailOTPOptions) => {
{
email,
emailVerified: true,
},
},ctx
);
if (
@@ -442,7 +442,7 @@ export const emailOTP = (options: EmailOTPOptions) => {
email,
emailVerified: true,
name: email,
});
}, ctx);
const session = await ctx.context.internalAdapter.createSession(
newUser.id,
ctx.request,
@@ -468,7 +468,7 @@ export const emailOTP = (options: EmailOTPOptions) => {
if (!user.user.emailVerified) {
await ctx.context.internalAdapter.updateUser(user.user.id, {
emailVerified: true,
});
}, ctx);
}
const session = await ctx.context.internalAdapter.createSession(
@@ -638,11 +638,12 @@ export const emailOTP = (options: EmailOTPOptions) => {
providerId: "credential",
accountId: user.user.id,
password: passwordHash,
});
}, ctx);
} else {
await ctx.context.internalAdapter.updatePassword(
user.user.id,
passwordHash,
ctx
);
}
@@ -217,8 +217,8 @@ export const magicLink = (options: MagicLinkOptions) => {
const newUser = await ctx.context.internalAdapter.createUser({
email: email,
emailVerified: true,
name: name || email,
});
name: name,
}, ctx);
user = newUser;
if (!user) {
throw ctx.redirect(
@@ -233,7 +233,7 @@ export const magicLink = (options: MagicLinkOptions) => {
if (!user.emailVerified) {
await ctx.context.internalAdapter.updateUser(user.id, {
emailVerified: true,
});
}, ctx);
}
const session = await ctx.context.internalAdapter.createSession(
@@ -115,6 +115,7 @@ export const oneTap = (options?: OneTapOptions) =>
providerId: "google",
accountId: sub,
},
ctx
);
if (!newUser) {
throw new APIError("INTERNAL_SERVER_ERROR", {
@@ -447,7 +447,7 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
{
[opts.phoneNumber]: ctx.body.phoneNumber,
[opts.phoneNumberVerified]: true,
},
},ctx
);
return ctx.json({
status: true,
@@ -488,7 +488,7 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
: ctx.body.phoneNumber,
[opts.phoneNumber]: ctx.body.phoneNumber,
[opts.phoneNumberVerified]: true,
});
}, ctx);
if (!user) {
throw new APIError("INTERNAL_SERVER_ERROR", {
message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER,
@@ -498,7 +498,7 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
} else {
user = await ctx.context.internalAdapter.updateUser(user.id, {
[opts.phoneNumberVerified]: true,
});
},ctx);
}
if (!user) {
@@ -103,7 +103,7 @@ export const twoFactor = (options?: TwoFactorOptions) => {
user.id,
{
twoFactorEnabled: true,
},
},ctx
);
const newSession = await ctx.context.internalAdapter.createSession(
updatedUser.id,
@@ -203,7 +203,7 @@ export const twoFactor = (options?: TwoFactorOptions) => {
user.id,
{
twoFactorEnabled: false,
},
},ctx
);
await ctx.context.adapter.delete({
model: opts.twoFactorTable,
@@ -259,7 +259,7 @@ export const totp2fa = (options?: TOTPOptions) => {
user.id,
{
twoFactorEnabled: true,
},
},ctx
);
const newSession = await ctx.context.internalAdapter
.createSession(
+70 -18
View File
@@ -1,6 +1,16 @@
import type { Dialect, Kysely, MysqlPool, PostgresPool } from "kysely";
import type { Account, Session, User, Verification } from "../types";
import type { BetterAuthPlugin } from "./plugins";
import type {
Account,
GenericEndpointContext,
Session,
User,
Verification,
} from "../types";
import type {
BetterAuthPlugin,
HookAfterHandler,
HookBeforeHandler,
} from "./plugins";
import type { SocialProviderList, SocialProviders } from "../social-providers";
import type { AdapterInstance, SecondaryStorage } from "./adapter";
import type { KyselyDatabaseType } from "../adapters/kysely-adapter/types";
@@ -633,7 +643,10 @@ export type BetterAuthOptions = {
* if the hook returns false, the user will not be created.
* If the hook returns an object, it'll be used instead of the original data
*/
before?: (user: User) => Promise<
before?: (
user: User,
context?: GenericEndpointContext,
) => Promise<
| boolean
| void
| {
@@ -643,7 +656,7 @@ export type BetterAuthOptions = {
/**
* Hook that is called after a user is created.
*/
after?: (user: User) => Promise<void>;
after?: (user: User, context?: GenericEndpointContext) => Promise<void>;
};
update?: {
/**
@@ -651,7 +664,10 @@ export type BetterAuthOptions = {
* if the hook returns false, the user will not be updated.
* If the hook returns an object, it'll be used instead of the original data
*/
before?: (user: Partial<User>) => Promise<
before?: (
user: Partial<User>,
context?: GenericEndpointContext,
) => Promise<
| boolean
| void
| {
@@ -661,7 +677,7 @@ export type BetterAuthOptions = {
/**
* Hook that is called after a user is updated.
*/
after?: (user: User) => Promise<void>;
after?: (user: User, context?: GenericEndpointContext) => Promise<void>;
};
};
/**
@@ -674,7 +690,10 @@ export type BetterAuthOptions = {
* if the hook returns false, the session will not be updated.
* If the hook returns an object, it'll be used instead of the original data
*/
before?: (session: Session) => Promise<
before?: (
session: Session,
context?: GenericEndpointContext,
) => Promise<
| boolean
| void
| {
@@ -684,7 +703,10 @@ export type BetterAuthOptions = {
/**
* Hook that is called after a session is updated.
*/
after?: (session: Session) => Promise<void>;
after?: (
session: Session,
context?: GenericEndpointContext,
) => Promise<void>;
};
/**
* Update hook
@@ -695,7 +717,10 @@ export type BetterAuthOptions = {
* if the hook returns false, the session will not be updated.
* If the hook returns an object, it'll be used instead of the original data
*/
before?: (session: Partial<Session>) => Promise<
before?: (
session: Partial<Session>,
context?: GenericEndpointContext,
) => Promise<
| boolean
| void
| {
@@ -705,7 +730,10 @@ export type BetterAuthOptions = {
/**
* Hook that is called after a session is updated.
*/
after?: (session: Session) => Promise<void>;
after?: (
session: Session,
context?: GenericEndpointContext,
) => Promise<void>;
};
};
/**
@@ -718,7 +746,10 @@ export type BetterAuthOptions = {
* If the hook returns false, the account will not be created.
* If the hook returns an object, it'll be used instead of the original data
*/
before?: (account: Account) => Promise<
before?: (
account: Account,
context?: GenericEndpointContext,
) => Promise<
| boolean
| void
| {
@@ -728,7 +759,10 @@ export type BetterAuthOptions = {
/**
* Hook that is called after a account is created.
*/
after?: (account: Account) => Promise<void>;
after?: (
account: Account,
context?: GenericEndpointContext,
) => Promise<void>;
};
/**
* Update hook
@@ -739,7 +773,10 @@ export type BetterAuthOptions = {
* If the hook returns false, the user will not be updated.
* If the hook returns an object, it'll be used instead of the original data
*/
before?: (account: Partial<Account>) => Promise<
before?: (
account: Partial<Account>,
context?: GenericEndpointContext,
) => Promise<
| boolean
| void
| {
@@ -749,7 +786,10 @@ export type BetterAuthOptions = {
/**
* Hook that is called after a account is updated.
*/
after?: (account: Account) => Promise<void>;
after?: (
account: Account,
context?: GenericEndpointContext,
) => Promise<void>;
};
};
/**
@@ -762,7 +802,10 @@ export type BetterAuthOptions = {
* if the hook returns false, the verification will not be created.
* If the hook returns an object, it'll be used instead of the original data
*/
before?: (verification: Verification) => Promise<
before?: (
verification: Verification,
context?: GenericEndpointContext,
) => Promise<
| boolean
| void
| {
@@ -772,7 +815,10 @@ export type BetterAuthOptions = {
/**
* Hook that is called after a verification is created.
*/
after?: (verification: Verification) => Promise<void>;
after?: (
verification: Verification,
context?: GenericEndpointContext,
) => Promise<void>;
};
update?: {
/**
@@ -780,7 +826,10 @@ export type BetterAuthOptions = {
* if the hook returns false, the verification will not be updated.
* If the hook returns an object, it'll be used instead of the original data
*/
before?: (verification: Partial<Verification>) => Promise<
before?: (
verification: Partial<Verification>,
context?: GenericEndpointContext,
) => Promise<
| boolean
| void
| {
@@ -790,7 +839,10 @@ export type BetterAuthOptions = {
/**
* Hook that is called after a verification is updated.
*/
after?: (verification: Verification) => Promise<void>;
after?: (
verification: Verification,
context?: GenericEndpointContext,
) => Promise<void>;
};
};
};