feat: custom session response (#579)

This commit is contained in:
Bereket Engida
2024-11-19 21:16:23 +03:00
committed by GitHub
parent a3f0793d68
commit f5f60a23e8
28 changed files with 425 additions and 95 deletions

View File

@@ -8,6 +8,7 @@ import {
oneTapClient, oneTapClient,
} from "better-auth/client/plugins"; } from "better-auth/client/plugins";
import { toast } from "sonner"; import { toast } from "sonner";
import { customSessionClient } from "./auth/plugins/session-client";
export const client = createAuthClient({ export const client = createAuthClient({
plugins: [ plugins: [
@@ -22,6 +23,7 @@ export const client = createAuthClient({
oneTapClient({ oneTapClient({
clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!, clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
}), }),
customSessionClient(),
], ],
fetchOptions: { fetchOptions: {
onError(e) { onError(e) {

View File

@@ -8,6 +8,7 @@ import {
twoFactor, twoFactor,
oneTap, oneTap,
oAuthProxy, oAuthProxy,
createAuthEndpoint,
} from "better-auth/plugins"; } from "better-auth/plugins";
import { reactInvitationEmail } from "./email/invitation"; import { reactInvitationEmail } from "./email/invitation";
import { LibsqlDialect } from "@libsql/kysely-libsql"; import { LibsqlDialect } from "@libsql/kysely-libsql";
@@ -16,7 +17,7 @@ import { resend } from "./email/resend";
import { MysqlDialect } from "kysely"; import { MysqlDialect } from "kysely";
import { createPool } from "mysql2/promise"; import { createPool } from "mysql2/promise";
import { nextCookies } from "better-auth/next-js"; 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 from = process.env.BETTER_AUTH_EMAIL || "delivered@resend.dev";
const to = process.env.TEST_EMAIL || ""; const to = process.env.TEST_EMAIL || "";
@@ -112,12 +113,6 @@ export const auth = betterAuth({
}, },
plugins: [ plugins: [
organization({ organization({
ac: ac.ac,
roles: {
admin: ac.admin,
owner: ac.owner,
member: ac.member,
},
async sendInvitationEmail(data) { async sendInvitationEmail(data) {
const res = await resend.emails.send({ const res = await resend.emails.send({
from, from,
@@ -159,5 +154,6 @@ export const auth = betterAuth({
oneTap(), oneTap(),
oAuthProxy(), oAuthProxy(),
nextCookies(), nextCookies(),
customSession(),
], ],
}); });

View File

@@ -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;
};

View File

@@ -0,0 +1,9 @@
import { BetterAuthClientPlugin } from "better-auth";
import { customSession } from "./custom-session";
export const customSessionClient = () => {
return {
id: "session-client",
$InferServerPlugin: {} as ReturnType<typeof customSession>,
} satisfies BetterAuthClientPlugin;
};

View File

@@ -47,6 +47,8 @@ import { authClient } from "@/lib/client"
const session = await authClient.getSession() const session = await authClient.getSession()
``` ```
To learn how to customize the session response check the [Customizing Session Response](#customizing-session-response) section.
### Use Session ### Use Session
The `useSession` action provides a reactive way to access the current 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 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<typeof auth>()],
});
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.

View File

@@ -1,4 +1,4 @@
import { APIError, type Context } from "better-call"; import { APIError } from "better-call";
import { createAuthEndpoint, createAuthMiddleware } from "../call"; import { createAuthEndpoint, createAuthMiddleware } from "../call";
import { getDate } from "../../utils/date"; import { getDate } from "../../utils/date";
import { deleteSessionCookie, setSessionCookie } from "../../cookies"; import { deleteSessionCookie, setSessionCookie } from "../../cookies";

View File

@@ -1,20 +1,14 @@
import type { Endpoint, Prettify } from "better-call";
import { getEndpoints, router } from "./api"; import { getEndpoints, router } from "./api";
import { init } from "./init"; import { init } from "./init";
import type { BetterAuthOptions } from "./types/options"; 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"; import { getBaseURL } from "./utils/url";
import type { FilterActions, InferAPI } from "./types/api";
type InferAPI<API> = Omit<
API,
API extends { [key in infer K]: Endpoint }
? K extends string
? API[K]["options"]["metadata"] extends { isAction: false }
? K
: never
: never
: never
>;
export const betterAuth = <O extends BetterAuthOptions>(options: O) => { export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
const authContext = init(options); const authContext = init(options);
@@ -46,8 +40,8 @@ export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
$context: authContext, $context: authContext,
$Infer: {} as { $Infer: {} as {
Session: { Session: {
session: Prettify<InferSession<O>>; session: PrettifyDeep<InferSession<O>>;
user: Prettify<InferUser<O>>; user: PrettifyDeep<InferUser<O>>;
}; };
} & InferPluginTypes<O>, } & InferPluginTypes<O>,
}; };
@@ -55,6 +49,6 @@ export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
export type Auth = { export type Auth = {
handler: (request: Request) => Promise<Response>; handler: (request: Request) => Promise<Response>;
api: InferAPI<ReturnType<typeof router>["endpoints"]>; api: FilterActions<ReturnType<typeof router>["endpoints"]>;
options: BetterAuthOptions; options: BetterAuthOptions;
}; };

View File

@@ -27,7 +27,7 @@ export const getClientConfig = <O extends ClientOptions>(options?: O) => {
...pluginsFetchPlugins, ...pluginsFetchPlugins,
], ],
}); });
const { $sessionSignal, session } = getSessionAtom<O>($fetch); const { $sessionSignal, session } = getSessionAtom($fetch);
const plugins = options?.plugins || []; const plugins = options?.plugins || [];
let pluginsActions = {} as Record<string, any>; let pluginsActions = {} as Record<string, any>;
let pluginsAtoms = { let pluginsAtoms = {

View File

@@ -1,3 +1,12 @@
import type { BetterAuthPlugin } from "../types";
import type { BetterAuthClientPlugin } from "./types";
export * from "./vanilla"; export * from "./vanilla";
export * from "./query"; export * from "./query";
export * from "./types"; export * from "./types";
export const InferPlugin = <T extends BetterAuthPlugin>() => {
return {
id: "infer-server-plugin",
$InferServerPlugin: {} as T,
} satisfies BetterAuthClientPlugin;
};

View File

@@ -138,7 +138,11 @@ export type InferRoute<API, COpts extends ClientOptions> = API extends {
] ]
) => Promise< ) => Promise<
BetterFetchResponse< BetterFetchResponse<
InferReturn<Awaited<R>, COpts>, T["options"]["metadata"] extends {
CUSTOM_SESSION: boolean;
}
? NonNullable<Awaited<R>>
: InferReturn<Awaited<R>, COpts>,
unknown, unknown,
FetchOptions["throw"] extends true FetchOptions["throw"] extends true
? true ? true

View File

@@ -13,3 +13,5 @@ export * from "../../plugins/jwt/client";
export * from "../../plugins/multi-session/client"; export * from "../../plugins/multi-session/client";
export * from "../../plugins/email-otp/client"; export * from "../../plugins/email-otp/client";
export * from "../../plugins/one-tap/client"; export * from "../../plugins/one-tap/client";
export * from "../../plugins/custom-session/client";
export * from "./infer-plugin";

View File

@@ -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<infer P>
? P extends {
id: ID;
}
? P
: never
: never;
return {
id: "infer-server-plugin",
$InferServerPlugin: {} as Plugin,
} satisfies BetterAuthClientPlugin;
};

View File

@@ -10,7 +10,10 @@ import type {
} from "../types"; } from "../types";
import { createDynamicPathProxy } from "../proxy"; import { createDynamicPathProxy } from "../proxy";
import type { UnionToIntersection } from "../../types/helper"; 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"; import { useStore } from "./react-store";
function getAtomKey(str: string) { function getAtomKey(str: string) {
@@ -69,21 +72,23 @@ export function createAuthClient<Option extends ClientOptions>(
atomListeners, atomListeners,
); );
type Session = { type ClientAPI = InferClientAPI<Option>;
session: InferSessionFromClient<Option>; type Session = ClientAPI extends {
user: InferUserFromClient<Option>; getSession: () => Promise<BetterFetchResponse<infer D>>;
}; }
? D
: ClientAPI;
return proxy as UnionToIntersection<InferResolvedHooks<Option>> & return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> & ClientAPI &
InferActions<Option> & { InferActions<Option> & {
useSession: () => { useSession: () => {
data: Session | null; data: Session;
isPending: boolean; isPending: boolean;
error: BetterFetchError | null; error: BetterFetchError | null;
}; };
$Infer: { $Infer: {
Session: Session; Session: NonNullable<Session>;
}; };
$fetch: typeof $fetch; $fetch: typeof $fetch;
$store: typeof $store; $store: typeof $store;

View File

@@ -1,22 +1,13 @@
import type { BetterFetch } from "@better-fetch/fetch"; import type { BetterFetch } from "@better-fetch/fetch";
import { atom } from "nanostores"; import { atom } from "nanostores";
import type { Prettify } from "../types/helper";
import type {
ClientOptions,
InferSessionFromClient,
InferUserFromClient,
} from "./types";
import { useAuthQuery } from "./query"; import { useAuthQuery } from "./query";
import type { Session, User } from "../types";
export function getSessionAtom<Option extends ClientOptions>( export function getSessionAtom($fetch: BetterFetch) {
$fetch: BetterFetch,
) {
type UserWithAdditionalFields = InferUserFromClient<Option>;
type SessionWithAdditionalFields = InferSessionFromClient<Option>;
const $signal = atom<boolean>(false); const $signal = atom<boolean>(false);
const session = useAuthQuery<{ const session = useAuthQuery<{
user: Prettify<UserWithAdditionalFields>; user: User;
session: Prettify<SessionWithAdditionalFields>; session: Session;
}>($signal, "/get-session", $fetch, { }>($signal, "/get-session", $fetch, {
method: "GET", method: "GET",
}); });

View File

@@ -12,7 +12,10 @@ import type {
} from "../types"; } from "../types";
import type { Accessor } from "solid-js"; import type { Accessor } from "solid-js";
import type { UnionToIntersection } from "../../types/helper"; import type { UnionToIntersection } from "../../types/helper";
import type { BetterFetchError } from "@better-fetch/fetch"; import type {
BetterFetchError,
BetterFetchResponse,
} from "@better-fetch/fetch";
import { useStore } from "./solid-store"; import { useStore } from "./solid-store";
function getAtomKey(str: string) { function getAtomKey(str: string) {
@@ -62,21 +65,23 @@ export function createAuthClient<Option extends ClientOptions>(
pluginsAtoms, pluginsAtoms,
atomListeners, atomListeners,
); );
type Session = { type ClientAPI = InferClientAPI<Option>;
session: InferSessionFromClient<Option>; type Session = ClientAPI extends {
user: InferUserFromClient<Option>; getSession: () => Promise<BetterFetchResponse<infer D>>;
}; }
? D
: ClientAPI;
return proxy as UnionToIntersection<InferResolvedHooks<Option>> & return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> & InferClientAPI<Option> &
InferActions<Option> & { InferActions<Option> & {
useSession: () => Accessor<{ useSession: () => Accessor<{
data: Session | null; data: Session;
isPending: boolean; isPending: boolean;
isRefetching: boolean; isRefetching: boolean;
error: BetterFetchError | null; error: BetterFetchError | null;
}>; }>;
$Infer: { $Infer: {
Session: Session; Session: NonNullable<Session>;
}; };
$fetch: typeof $fetch; $fetch: typeof $fetch;
}; };

View File

@@ -12,7 +12,10 @@ import type {
import { createDynamicPathProxy } from "../proxy"; import { createDynamicPathProxy } from "../proxy";
import type { UnionToIntersection } from "../../types/helper"; import type { UnionToIntersection } from "../../types/helper";
import type { Atom } from "nanostores"; import type { Atom } from "nanostores";
import type { BetterFetchError } from "@better-fetch/fetch"; import type {
BetterFetchError,
BetterFetchResponse,
} from "@better-fetch/fetch";
type InferResolvedHooks<O extends ClientOptions> = O["plugins"] extends Array< type InferResolvedHooks<O extends ClientOptions> = O["plugins"] extends Array<
infer Plugin infer Plugin
@@ -60,15 +63,17 @@ export function createAuthClient<Option extends ClientOptions>(
pluginsAtoms, pluginsAtoms,
atomListeners, atomListeners,
); );
type Session = { type ClientAPI = InferClientAPI<Option>;
session: InferSessionFromClient<Option>; type Session = ClientAPI extends {
user: InferUserFromClient<Option>; getSession: () => Promise<BetterFetchResponse<infer D>>;
}; }
? D
: ClientAPI;
return proxy as UnionToIntersection<InferResolvedHooks<Option>> & return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> & InferClientAPI<Option> &
InferActions<Option> & { InferActions<Option> & {
useSession: () => Atom<{ useSession: () => Atom<{
data: Session | null; data: Session;
error: BetterFetchError | null; error: BetterFetchError | null;
isPending: boolean; isPending: boolean;
isRefetching: boolean; isRefetching: boolean;
@@ -76,7 +81,7 @@ export function createAuthClient<Option extends ClientOptions>(
$fetch: typeof $fetch; $fetch: typeof $fetch;
$store: typeof $store; $store: typeof $store;
$Infer: { $Infer: {
Session: Session; Session: NonNullable<Session>;
}; };
}; };
} }

View File

@@ -68,20 +68,20 @@ export interface ClientOptions {
export type InferClientAPI<O extends ClientOptions> = InferRoutes< export type InferClientAPI<O extends ClientOptions> = InferRoutes<
O["plugins"] extends Array<any> O["plugins"] extends Array<any>
? (O["plugins"] extends Array<infer Pl> ? Auth["api"] &
? UnionToIntersection< (O["plugins"] extends Array<infer Pl>
Pl extends { ? UnionToIntersection<
$InferServerPlugin: infer Plug; Pl extends {
} $InferServerPlugin: infer Plug;
? Plug extends { }
endpoints: infer Endpoints; ? Plug extends {
} endpoints: infer Endpoints;
? Endpoints }
? Endpoints
: {}
: {} : {}
: {} >
> : {})
: {}) &
Auth["api"]
: Auth["api"], : Auth["api"],
O O
>; >;

View File

@@ -12,7 +12,10 @@ import type {
import { createDynamicPathProxy } from "./proxy"; import { createDynamicPathProxy } from "./proxy";
import type { UnionToIntersection } from "../types/helper"; import type { UnionToIntersection } from "../types/helper";
import type { Atom } from "nanostores"; import type { Atom } from "nanostores";
import type { BetterFetchError } from "@better-fetch/fetch"; import type {
BetterFetchError,
BetterFetchResponse,
} from "@better-fetch/fetch";
type InferResolvedHooks<O extends ClientOptions> = O["plugins"] extends Array< type InferResolvedHooks<O extends ClientOptions> = O["plugins"] extends Array<
infer Plugin infer Plugin
@@ -60,24 +63,24 @@ export function createAuthClient<Option extends ClientOptions>(
pluginsAtoms, pluginsAtoms,
atomListeners, atomListeners,
); );
type ClientAPI = InferClientAPI<Option>;
type Session = ClientAPI extends {
getSession: () => Promise<BetterFetchResponse<infer D>>;
}
? D
: ClientAPI;
return proxy as UnionToIntersection<InferResolvedHooks<Option>> & return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> & ClientAPI &
InferActions<Option> & { InferActions<Option> & {
useSession: Atom<{ useSession: Atom<{
data: { data: Session;
session: InferSessionFromClient<Option>;
user: InferUserFromClient<Option>;
};
error: BetterFetchError | null; error: BetterFetchError | null;
isPending: boolean; isPending: boolean;
}>; }>;
$fetch: typeof $fetch; $fetch: typeof $fetch;
$store: typeof $store; $store: typeof $store;
$Infer: { $Infer: {
Session: { Session: NonNullable<Session>;
session: InferSessionFromClient<Option>;
user: InferUserFromClient<Option>;
};
}; };
}; };
} }

View File

@@ -7,13 +7,14 @@ import type {
ClientOptions, ClientOptions,
InferActions, InferActions,
InferClientAPI, InferClientAPI,
InferSessionFromClient,
InferUserFromClient,
IsSignal, IsSignal,
} from "../types"; } from "../types";
import { createDynamicPathProxy } from "../proxy"; import { createDynamicPathProxy } from "../proxy";
import type { UnionToIntersection } from "../../types/helper"; import type { UnionToIntersection } from "../../types/helper";
import type { BetterFetchError } from "@better-fetch/fetch"; import type {
BetterFetchError,
BetterFetchResponse,
} from "@better-fetch/fetch";
function getAtomKey(str: string) { function getAtomKey(str: string) {
return `use${capitalizeFirstLetter(str)}`; return `use${capitalizeFirstLetter(str)}`;
@@ -55,14 +56,16 @@ export function createAuthClient<Option extends ClientOptions>(
resolvedHooks[getAtomKey(key)] = () => useStore(value); resolvedHooks[getAtomKey(key)] = () => useStore(value);
} }
type Session = { type ClientAPI = InferClientAPI<Option>;
session: InferSessionFromClient<Option>; type Session = ClientAPI extends {
user: InferUserFromClient<Option>; getSession: () => Promise<BetterFetchResponse<infer D>>;
}; }
? D
: ClientAPI;
function useSession(): () => DeepReadonly< function useSession(): () => DeepReadonly<
Ref<{ Ref<{
data: Session | null; data: Session;
isPending: boolean; isPending: boolean;
isRefetching: boolean; isRefetching: boolean;
error: BetterFetchError | null; error: BetterFetchError | null;
@@ -114,12 +117,13 @@ export function createAuthClient<Option extends ClientOptions>(
pluginsAtoms, pluginsAtoms,
atomListeners, atomListeners,
); );
return proxy as UnionToIntersection<InferResolvedHooks<Option>> & return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
InferClientAPI<Option> & InferClientAPI<Option> &
InferActions<Option> & { InferActions<Option> & {
useSession: typeof useSession; useSession: typeof useSession;
$Infer: { $Infer: {
Session: Session; Session: NonNullable<Session>;
}; };
$fetch: typeof $fetch; $fetch: typeof $fetch;
$store: typeof $store; $store: typeof $store;

View File

@@ -4,9 +4,21 @@ import type { BetterAuthOptions } from "../types";
import type { Adapter } from "../types/adapter"; import type { Adapter } from "../types/adapter";
import { createKyselyAdapter } from "../adapters/kysely-adapter/dialect"; import { createKyselyAdapter } from "../adapters/kysely-adapter/dialect";
import { kyselyAdapter } from "../adapters/kysely-adapter"; import { kyselyAdapter } from "../adapters/kysely-adapter";
import { isDevelopment } from "../utils/env";
import { memoryAdapter } from "../adapters/memory-adapter";
import { logger } from "../utils";
const memoryDB = {};
export async function getAdapter(options: BetterAuthOptions): Promise<Adapter> { export async function getAdapter(options: BetterAuthOptions): Promise<Adapter> {
if (!options.database) { if (!options.database) {
// If no database is provided, use memory adapter in development
if (isDevelopment) {
logger.warn(
"No database configuration provided. Using memory adapter in development",
);
return memoryAdapter(memoryDB)(options);
}
throw new BetterAuthError("Database configuration is required"); throw new BetterAuthError("Database configuration is required");
} }

View File

@@ -0,0 +1,10 @@
import { InferServerPlugin } from "../../client/plugins";
import type { BetterAuthOptions } from "../../types";
export const customSessionClient = <
A extends {
options: BetterAuthOptions;
},
>() => {
return InferServerPlugin<A, "custom-session">();
};

View File

@@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import { getTestInstance } from "../../test-utils/test-instance";
import { customSession } from ".";
import { admin } from "../admin";
import { createAuthClient } from "../../client";
import { customSessionClient } from "./client";
import type { BetterAuthOptions, InferUser } from "../../types";
import { adminClient } from "../admin/client";
describe("Custom Session Plugin Tests", async () => {
const options = {
plugins: [admin()],
} satisfies BetterAuthOptions;
const { auth, signInWithTestUser, testUser, customFetchImpl } =
await getTestInstance({
plugins: [
...options.plugins,
customSession(async ({ user, session }) => {
const newData = {
message: "Hello, World!",
};
return {
user: {
firstName: user.name.split(" ")[0],
lastName: user.name.split(" ")[1],
},
newData,
session,
};
}, options),
],
});
const client = createAuthClient({
baseURL: "http://localhost:3000",
plugins: [customSessionClient<typeof auth>(), adminClient()],
fetchOptions: { customFetchImpl },
});
it("should return the session", async () => {
const { headers } = await signInWithTestUser();
const session = await auth.api.getSession({ headers });
const s = await client.getSession({ fetchOptions: { headers } });
expect(s.data?.newData).toEqual({ message: "Hello, World!" });
expect(session?.newData).toEqual({ message: "Hello, World!" });
});
});

View File

@@ -0,0 +1,43 @@
import { createAuthEndpoint, getSessionFromCtx } from "../../api";
import type {
BetterAuthOptions,
BetterAuthPlugin,
InferSession,
InferUser,
Session,
User,
} from "../../types";
export const customSession = <
Returns extends Record<string, any>,
O extends BetterAuthOptions = BetterAuthOptions,
>(
fn: (session: {
user: InferUser<O>;
session: InferSession<O>;
}) => Promise<Returns>,
options?: O,
) => {
return {
id: "custom-session",
endpoints: {
getSession: createAuthEndpoint(
"/get-session",
{
method: "GET",
metadata: {
CUSTOM_SESSION: true,
},
},
async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session) {
return ctx.json(null);
}
const fnResult = await fn(session as any);
return ctx.json(fnResult);
},
),
},
} satisfies BetterAuthPlugin;
};

View File

@@ -16,3 +16,4 @@ export * from "./multi-session";
export * from "./email-otp"; export * from "./email-otp";
export * from "./one-tap"; export * from "./one-tap";
export * from "./oauth-proxy"; export * from "./oauth-proxy";
export * from "./custom-session";

View File

@@ -106,7 +106,7 @@ export async function getTestInstance<
const testUser = { const testUser = {
email: "test@test.com", email: "test@test.com",
password: "test123456", password: "test123456",
name: "test", name: "test user",
...config?.testUser, ...config?.testUser,
}; };
async function createTestUser() { async function createTestUser() {

View File

@@ -0,0 +1,44 @@
import type { Endpoint } from "better-call";
import type { PrettifyDeep, UnionToIntersection } from "./helper";
export type FilteredAPI<API> = Omit<
API,
API extends { [key in infer K]: Endpoint }
? K extends string
? K extends "getSession"
? K
: API[K]["options"]["metadata"] extends { isAction: false }
? K
: never
: never
: never
>;
export type FilterActions<API> = Omit<
API,
API extends { [key in infer K]: Endpoint }
? K extends string
? API[K]["options"]["metadata"] extends { isAction: false }
? K
: never
: never
: never
>;
export type InferSessionAPI<API> = API extends {
[key: string]: infer E;
}
? UnionToIntersection<
E extends Endpoint
? E["path"] extends "/get-session"
? {
getSession: (context: {
headers: Headers;
}) => Promise<PrettifyDeep<Awaited<ReturnType<E>>>>;
}
: never
: never
>
: never;
export type InferAPI<API> = InferSessionAPI<API> & FilteredAPI<API>;

View File

@@ -5,6 +5,17 @@ export type LiteralString = "" | (string & Record<never, never>);
export type OmitId<T extends { id: unknown }> = Omit<T, "id">; export type OmitId<T extends { id: unknown }> = Omit<T, "id">;
export type Prettify<T> = Omit<T, never>; export type Prettify<T> = Omit<T, never>;
export type PrettifyDeep<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any
? T[K]
: T[K] extends object
? T[K] extends Array<any>
? T[K]
: T[K] extends Date
? T[K]
: PrettifyDeep<T[K]>
: T[K];
} & {};
export type LiteralUnion<LiteralType, BaseType extends Primitive> = export type LiteralUnion<LiteralType, BaseType extends Primitive> =
| LiteralType | LiteralType
| (BaseType & Record<never, never>); | (BaseType & Record<never, never>);

View File

@@ -66,7 +66,7 @@ export interface BetterAuthOptions {
/** /**
* Database configuration * Database configuration
*/ */
database: database?:
| PostgresPool | PostgresPool
| MysqlPool | MysqlPool
| Database | Database