From f5f60a23e86de5ea68e8304511df76206c153b44 Mon Sep 17 00:00:00 2001 From: Bereket Engida <86073083+Bekacru@users.noreply.github.com> Date: Tue, 19 Nov 2024 21:16:23 +0300 Subject: [PATCH] feat: custom session response (#579) --- demo/nextjs/lib/auth-client.ts | 2 + demo/nextjs/lib/auth.ts | 10 +-- .../nextjs/lib/auth/plugins/custom-session.ts | 32 ++++++++ .../nextjs/lib/auth/plugins/session-client.ts | 9 +++ .../docs/concepts/session-management.mdx | 78 +++++++++++++++++++ .../better-auth/src/api/routes/session.ts | 2 +- packages/better-auth/src/auth.ts | 26 +++---- packages/better-auth/src/client/config.ts | 2 +- packages/better-auth/src/client/index.ts | 9 +++ .../better-auth/src/client/path-to-object.ts | 6 +- .../better-auth/src/client/plugins/index.ts | 2 + .../src/client/plugins/infer-plugin.ts | 23 ++++++ .../better-auth/src/client/react/index.ts | 21 +++-- .../better-auth/src/client/session-atom.ts | 17 +--- .../better-auth/src/client/solid/index.ts | 19 +++-- .../better-auth/src/client/svelte/index.ts | 19 +++-- packages/better-auth/src/client/types.ts | 26 +++---- packages/better-auth/src/client/vanilla.ts | 23 +++--- packages/better-auth/src/client/vue/index.ts | 22 +++--- packages/better-auth/src/db/utils.ts | 12 +++ .../src/plugins/custom-session/client.ts | 10 +++ .../custom-session/custom-session.test.ts | 47 +++++++++++ .../src/plugins/custom-session/index.ts | 43 ++++++++++ packages/better-auth/src/plugins/index.ts | 1 + .../src/test-utils/test-instance.ts | 2 +- packages/better-auth/src/types/api.ts | 44 +++++++++++ packages/better-auth/src/types/helper.ts | 11 +++ packages/better-auth/src/types/options.ts | 2 +- 28 files changed, 425 insertions(+), 95 deletions(-) create mode 100644 demo/nextjs/lib/auth/plugins/custom-session.ts create mode 100644 demo/nextjs/lib/auth/plugins/session-client.ts create mode 100644 packages/better-auth/src/client/plugins/infer-plugin.ts create mode 100644 packages/better-auth/src/plugins/custom-session/client.ts create mode 100644 packages/better-auth/src/plugins/custom-session/custom-session.test.ts create mode 100644 packages/better-auth/src/plugins/custom-session/index.ts create mode 100644 packages/better-auth/src/types/api.ts diff --git a/demo/nextjs/lib/auth-client.ts b/demo/nextjs/lib/auth-client.ts index 69b7a87c..b8091986 100644 --- a/demo/nextjs/lib/auth-client.ts +++ b/demo/nextjs/lib/auth-client.ts @@ -8,6 +8,7 @@ import { oneTapClient, } from "better-auth/client/plugins"; import { toast } from "sonner"; +import { customSessionClient } from "./auth/plugins/session-client"; export const client = createAuthClient({ plugins: [ @@ -22,6 +23,7 @@ export const client = createAuthClient({ oneTapClient({ clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!, }), + customSessionClient(), ], fetchOptions: { onError(e) { diff --git a/demo/nextjs/lib/auth.ts b/demo/nextjs/lib/auth.ts index 2b3f3b7a..50a5ab1b 100644 --- a/demo/nextjs/lib/auth.ts +++ b/demo/nextjs/lib/auth.ts @@ -8,6 +8,7 @@ import { twoFactor, oneTap, oAuthProxy, + createAuthEndpoint, } from "better-auth/plugins"; import { reactInvitationEmail } from "./email/invitation"; import { LibsqlDialect } from "@libsql/kysely-libsql"; @@ -16,7 +17,7 @@ import { resend } from "./email/resend"; import { MysqlDialect } from "kysely"; import { createPool } from "mysql2/promise"; import { nextCookies } from "better-auth/next-js"; -import * as ac from "./access-control"; +import { customSession } from "./auth/plugins/custom-session"; const from = process.env.BETTER_AUTH_EMAIL || "delivered@resend.dev"; const to = process.env.TEST_EMAIL || ""; @@ -112,12 +113,6 @@ export const auth = betterAuth({ }, plugins: [ organization({ - ac: ac.ac, - roles: { - admin: ac.admin, - owner: ac.owner, - member: ac.member, - }, async sendInvitationEmail(data) { const res = await resend.emails.send({ from, @@ -159,5 +154,6 @@ export const auth = betterAuth({ oneTap(), oAuthProxy(), nextCookies(), + customSession(), ], }); diff --git a/demo/nextjs/lib/auth/plugins/custom-session.ts b/demo/nextjs/lib/auth/plugins/custom-session.ts new file mode 100644 index 00000000..1572d868 --- /dev/null +++ b/demo/nextjs/lib/auth/plugins/custom-session.ts @@ -0,0 +1,32 @@ +import { BetterAuthPlugin } from "better-auth"; +import { createAuthEndpoint } from "better-auth/plugins"; +import { getSessionFromCtx } from "better-auth/api"; + +export const customSession = () => { + return { + id: "custom-session", + endpoints: { + getSession: createAuthEndpoint( + "/get-session", + { + method: "GET", + }, + async (ctx) => { + const session = await getSessionFromCtx(ctx); + if (!session) { + return ctx.json(null); + } + const roles: { + id: number; + name: string; + }[] = []; + return ctx.json({ + user: session.user, + session: session.session, + roles, + }); + }, + ), + }, + } satisfies BetterAuthPlugin; +}; diff --git a/demo/nextjs/lib/auth/plugins/session-client.ts b/demo/nextjs/lib/auth/plugins/session-client.ts new file mode 100644 index 00000000..407d4717 --- /dev/null +++ b/demo/nextjs/lib/auth/plugins/session-client.ts @@ -0,0 +1,9 @@ +import { BetterAuthClientPlugin } from "better-auth"; +import { customSession } from "./custom-session"; + +export const customSessionClient = () => { + return { + id: "session-client", + $InferServerPlugin: {} as ReturnType, + } satisfies BetterAuthClientPlugin; +}; diff --git a/docs/content/docs/concepts/session-management.mdx b/docs/content/docs/concepts/session-management.mdx index 0a34a4ca..854b79b8 100644 --- a/docs/content/docs/concepts/session-management.mdx +++ b/docs/content/docs/concepts/session-management.mdx @@ -47,6 +47,8 @@ import { authClient } from "@/lib/client" const session = await authClient.getSession() ``` +To learn how to customize the session response check the [Customizing Session Response](#customizing-session-response) section. + ### Use Session The `useSession` action provides a reactive way to access the current session. @@ -146,3 +148,79 @@ auth.api.getSession({ headers: req.headers, // pass the headers }); ``` + + +### Customizing Session Response + +When you call `getSession` or `useSession`, the session data is returned as a `user` and `session` object. You can customize this response using the `customSession` plugin. + +```ts title="auth.ts" +import { customSession } from "better-auth/plugins"; + +export const auth = betterAuth({ + plugins: [ + customSession(async ({ user, session }) => { + const roles = findUserRoles(session.session.userId); + return { + roles, + user: { + ...user, + newField: "newField", + }, + session + }; + }), + ], +}); +``` + +This will add `roles` and `user.newField` to the session response. + +**Infer on the Client** + +```ts title="client.ts" +import type { auth } from "@/lib/auth"; // Import the auth instance as a type + +const authClient = createAuthClient({ + plugins: [customSessionClient()], +}); + +const { data } = await authClient.useSession(); +const { data: sessionData } = await authClient.getSession(); +// data.roles +// data.user.newField +``` + +**Some Caveats**: + +- The passed `session` object to the callback does not infer fields added by plugins. + +However, as a workaround, you can pull up your auth options and pass it to the plugin to infer the fields. + +```ts +import { betterAuth, BetterAuthOptions } from "better-auth"; + +const options = { + //...config options + plugins: [ + //...plugins + ] +} satisfies BetterAuthOptions; + +export const auth = betterAuth({ + ...options, + plugins: [{ + ...options.plugins, + customSession(async ({ user, session }) => { + // now both user and session will infer the fields added by plugins and your custom fields + retunr { + user, + session + } + }, options), // pass options here // [!code highlight] + }] +}) +``` + +- If you cannot use the `auth` instance as a type, inference will not work on the client. +- Session caching, including secondary storage or cookie cache, does not include custom fields. Each time the session is fetched, your custom session function will be called. diff --git a/packages/better-auth/src/api/routes/session.ts b/packages/better-auth/src/api/routes/session.ts index 759be103..933e2a61 100644 --- a/packages/better-auth/src/api/routes/session.ts +++ b/packages/better-auth/src/api/routes/session.ts @@ -1,4 +1,4 @@ -import { APIError, type Context } from "better-call"; +import { APIError } from "better-call"; import { createAuthEndpoint, createAuthMiddleware } from "../call"; import { getDate } from "../../utils/date"; import { deleteSessionCookie, setSessionCookie } from "../../cookies"; diff --git a/packages/better-auth/src/auth.ts b/packages/better-auth/src/auth.ts index 59e3862f..0b135842 100644 --- a/packages/better-auth/src/auth.ts +++ b/packages/better-auth/src/auth.ts @@ -1,20 +1,14 @@ -import type { Endpoint, Prettify } from "better-call"; import { getEndpoints, router } from "./api"; import { init } from "./init"; import type { BetterAuthOptions } from "./types/options"; -import type { InferPluginTypes, InferSession, InferUser } from "./types"; +import type { + InferPluginTypes, + InferSession, + InferUser, + PrettifyDeep, +} from "./types"; import { getBaseURL } from "./utils/url"; - -type InferAPI = Omit< - API, - API extends { [key in infer K]: Endpoint } - ? K extends string - ? API[K]["options"]["metadata"] extends { isAction: false } - ? K - : never - : never - : never ->; +import type { FilterActions, InferAPI } from "./types/api"; export const betterAuth = (options: O) => { const authContext = init(options); @@ -46,8 +40,8 @@ export const betterAuth = (options: O) => { $context: authContext, $Infer: {} as { Session: { - session: Prettify>; - user: Prettify>; + session: PrettifyDeep>; + user: PrettifyDeep>; }; } & InferPluginTypes, }; @@ -55,6 +49,6 @@ export const betterAuth = (options: O) => { export type Auth = { handler: (request: Request) => Promise; - api: InferAPI["endpoints"]>; + api: FilterActions["endpoints"]>; options: BetterAuthOptions; }; diff --git a/packages/better-auth/src/client/config.ts b/packages/better-auth/src/client/config.ts index 423410da..9ad37e58 100644 --- a/packages/better-auth/src/client/config.ts +++ b/packages/better-auth/src/client/config.ts @@ -27,7 +27,7 @@ export const getClientConfig = (options?: O) => { ...pluginsFetchPlugins, ], }); - const { $sessionSignal, session } = getSessionAtom($fetch); + const { $sessionSignal, session } = getSessionAtom($fetch); const plugins = options?.plugins || []; let pluginsActions = {} as Record; let pluginsAtoms = { diff --git a/packages/better-auth/src/client/index.ts b/packages/better-auth/src/client/index.ts index b9adaba7..c4a2f3ce 100644 --- a/packages/better-auth/src/client/index.ts +++ b/packages/better-auth/src/client/index.ts @@ -1,3 +1,12 @@ +import type { BetterAuthPlugin } from "../types"; +import type { BetterAuthClientPlugin } from "./types"; export * from "./vanilla"; export * from "./query"; export * from "./types"; + +export const InferPlugin = () => { + return { + id: "infer-server-plugin", + $InferServerPlugin: {} as T, + } satisfies BetterAuthClientPlugin; +}; diff --git a/packages/better-auth/src/client/path-to-object.ts b/packages/better-auth/src/client/path-to-object.ts index 56301677..e00376dc 100644 --- a/packages/better-auth/src/client/path-to-object.ts +++ b/packages/better-auth/src/client/path-to-object.ts @@ -138,7 +138,11 @@ export type InferRoute = API extends { ] ) => Promise< BetterFetchResponse< - InferReturn, COpts>, + T["options"]["metadata"] extends { + CUSTOM_SESSION: boolean; + } + ? NonNullable> + : InferReturn, COpts>, unknown, FetchOptions["throw"] extends true ? true diff --git a/packages/better-auth/src/client/plugins/index.ts b/packages/better-auth/src/client/plugins/index.ts index dab1096c..14f9cca4 100644 --- a/packages/better-auth/src/client/plugins/index.ts +++ b/packages/better-auth/src/client/plugins/index.ts @@ -13,3 +13,5 @@ export * from "../../plugins/jwt/client"; export * from "../../plugins/multi-session/client"; export * from "../../plugins/email-otp/client"; export * from "../../plugins/one-tap/client"; +export * from "../../plugins/custom-session/client"; +export * from "./infer-plugin"; diff --git a/packages/better-auth/src/client/plugins/infer-plugin.ts b/packages/better-auth/src/client/plugins/infer-plugin.ts new file mode 100644 index 00000000..a43d21c9 --- /dev/null +++ b/packages/better-auth/src/client/plugins/infer-plugin.ts @@ -0,0 +1,23 @@ +import type { BetterAuthClientPlugin, BetterAuthOptions } from "../../types"; + +export const InferServerPlugin = < + AuthOrOption extends + | BetterAuthOptions + | { + options: BetterAuthOptions; + }, + ID extends string, +>() => { + type Option = AuthOrOption extends { options: infer O } ? O : AuthOrOption; + type Plugin = Option["plugins"] extends Array + ? P extends { + id: ID; + } + ? P + : never + : never; + return { + id: "infer-server-plugin", + $InferServerPlugin: {} as Plugin, + } satisfies BetterAuthClientPlugin; +}; diff --git a/packages/better-auth/src/client/react/index.ts b/packages/better-auth/src/client/react/index.ts index f509a88d..018f7135 100644 --- a/packages/better-auth/src/client/react/index.ts +++ b/packages/better-auth/src/client/react/index.ts @@ -10,7 +10,10 @@ import type { } from "../types"; import { createDynamicPathProxy } from "../proxy"; import type { UnionToIntersection } from "../../types/helper"; -import type { BetterFetchError } from "@better-fetch/fetch"; +import type { + BetterFetchError, + BetterFetchResponse, +} from "@better-fetch/fetch"; import { useStore } from "./react-store"; function getAtomKey(str: string) { @@ -69,21 +72,23 @@ export function createAuthClient