mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-07 12:27:44 +00:00
feat: custom session response (#579)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
});
|
||||
|
||||
32
demo/nextjs/lib/auth/plugins/custom-session.ts
Normal file
32
demo/nextjs/lib/auth/plugins/custom-session.ts
Normal 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;
|
||||
};
|
||||
9
demo/nextjs/lib/auth/plugins/session-client.ts
Normal file
9
demo/nextjs/lib/auth/plugins/session-client.ts
Normal 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;
|
||||
};
|
||||
@@ -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<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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<API> = 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 = <O extends BetterAuthOptions>(options: O) => {
|
||||
const authContext = init(options);
|
||||
@@ -46,8 +40,8 @@ export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
|
||||
$context: authContext,
|
||||
$Infer: {} as {
|
||||
Session: {
|
||||
session: Prettify<InferSession<O>>;
|
||||
user: Prettify<InferUser<O>>;
|
||||
session: PrettifyDeep<InferSession<O>>;
|
||||
user: PrettifyDeep<InferUser<O>>;
|
||||
};
|
||||
} & InferPluginTypes<O>,
|
||||
};
|
||||
@@ -55,6 +49,6 @@ export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
|
||||
|
||||
export type Auth = {
|
||||
handler: (request: Request) => Promise<Response>;
|
||||
api: InferAPI<ReturnType<typeof router>["endpoints"]>;
|
||||
api: FilterActions<ReturnType<typeof router>["endpoints"]>;
|
||||
options: BetterAuthOptions;
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ export const getClientConfig = <O extends ClientOptions>(options?: O) => {
|
||||
...pluginsFetchPlugins,
|
||||
],
|
||||
});
|
||||
const { $sessionSignal, session } = getSessionAtom<O>($fetch);
|
||||
const { $sessionSignal, session } = getSessionAtom($fetch);
|
||||
const plugins = options?.plugins || [];
|
||||
let pluginsActions = {} as Record<string, any>;
|
||||
let pluginsAtoms = {
|
||||
|
||||
@@ -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 = <T extends BetterAuthPlugin>() => {
|
||||
return {
|
||||
id: "infer-server-plugin",
|
||||
$InferServerPlugin: {} as T,
|
||||
} satisfies BetterAuthClientPlugin;
|
||||
};
|
||||
|
||||
@@ -138,7 +138,11 @@ export type InferRoute<API, COpts extends ClientOptions> = API extends {
|
||||
]
|
||||
) => Promise<
|
||||
BetterFetchResponse<
|
||||
InferReturn<Awaited<R>, COpts>,
|
||||
T["options"]["metadata"] extends {
|
||||
CUSTOM_SESSION: boolean;
|
||||
}
|
||||
? NonNullable<Awaited<R>>
|
||||
: InferReturn<Awaited<R>, COpts>,
|
||||
unknown,
|
||||
FetchOptions["throw"] extends true
|
||||
? true
|
||||
|
||||
@@ -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";
|
||||
|
||||
23
packages/better-auth/src/client/plugins/infer-plugin.ts
Normal file
23
packages/better-auth/src/client/plugins/infer-plugin.ts
Normal 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;
|
||||
};
|
||||
@@ -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<Option extends ClientOptions>(
|
||||
atomListeners,
|
||||
);
|
||||
|
||||
type Session = {
|
||||
session: InferSessionFromClient<Option>;
|
||||
user: InferUserFromClient<Option>;
|
||||
};
|
||||
type ClientAPI = InferClientAPI<Option>;
|
||||
type Session = ClientAPI extends {
|
||||
getSession: () => Promise<BetterFetchResponse<infer D>>;
|
||||
}
|
||||
? D
|
||||
: ClientAPI;
|
||||
|
||||
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
|
||||
InferClientAPI<Option> &
|
||||
ClientAPI &
|
||||
InferActions<Option> & {
|
||||
useSession: () => {
|
||||
data: Session | null;
|
||||
data: Session;
|
||||
isPending: boolean;
|
||||
error: BetterFetchError | null;
|
||||
};
|
||||
$Infer: {
|
||||
Session: Session;
|
||||
Session: NonNullable<Session>;
|
||||
};
|
||||
$fetch: typeof $fetch;
|
||||
$store: typeof $store;
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import type { BetterFetch } from "@better-fetch/fetch";
|
||||
import { atom } from "nanostores";
|
||||
import type { Prettify } from "../types/helper";
|
||||
import type {
|
||||
ClientOptions,
|
||||
InferSessionFromClient,
|
||||
InferUserFromClient,
|
||||
} from "./types";
|
||||
import { useAuthQuery } from "./query";
|
||||
import type { Session, User } from "../types";
|
||||
|
||||
export function getSessionAtom<Option extends ClientOptions>(
|
||||
$fetch: BetterFetch,
|
||||
) {
|
||||
type UserWithAdditionalFields = InferUserFromClient<Option>;
|
||||
type SessionWithAdditionalFields = InferSessionFromClient<Option>;
|
||||
export function getSessionAtom($fetch: BetterFetch) {
|
||||
const $signal = atom<boolean>(false);
|
||||
const session = useAuthQuery<{
|
||||
user: Prettify<UserWithAdditionalFields>;
|
||||
session: Prettify<SessionWithAdditionalFields>;
|
||||
user: User;
|
||||
session: Session;
|
||||
}>($signal, "/get-session", $fetch, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
@@ -12,7 +12,10 @@ import type {
|
||||
} from "../types";
|
||||
import type { Accessor } from "solid-js";
|
||||
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";
|
||||
|
||||
function getAtomKey(str: string) {
|
||||
@@ -62,21 +65,23 @@ export function createAuthClient<Option extends ClientOptions>(
|
||||
pluginsAtoms,
|
||||
atomListeners,
|
||||
);
|
||||
type Session = {
|
||||
session: InferSessionFromClient<Option>;
|
||||
user: InferUserFromClient<Option>;
|
||||
};
|
||||
type ClientAPI = InferClientAPI<Option>;
|
||||
type Session = ClientAPI extends {
|
||||
getSession: () => Promise<BetterFetchResponse<infer D>>;
|
||||
}
|
||||
? D
|
||||
: ClientAPI;
|
||||
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
|
||||
InferClientAPI<Option> &
|
||||
InferActions<Option> & {
|
||||
useSession: () => Accessor<{
|
||||
data: Session | null;
|
||||
data: Session;
|
||||
isPending: boolean;
|
||||
isRefetching: boolean;
|
||||
error: BetterFetchError | null;
|
||||
}>;
|
||||
$Infer: {
|
||||
Session: Session;
|
||||
Session: NonNullable<Session>;
|
||||
};
|
||||
$fetch: typeof $fetch;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,10 @@ import type {
|
||||
import { createDynamicPathProxy } from "../proxy";
|
||||
import type { UnionToIntersection } from "../../types/helper";
|
||||
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<
|
||||
infer Plugin
|
||||
@@ -60,15 +63,17 @@ export function createAuthClient<Option extends ClientOptions>(
|
||||
pluginsAtoms,
|
||||
atomListeners,
|
||||
);
|
||||
type Session = {
|
||||
session: InferSessionFromClient<Option>;
|
||||
user: InferUserFromClient<Option>;
|
||||
};
|
||||
type ClientAPI = InferClientAPI<Option>;
|
||||
type Session = ClientAPI extends {
|
||||
getSession: () => Promise<BetterFetchResponse<infer D>>;
|
||||
}
|
||||
? D
|
||||
: ClientAPI;
|
||||
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
|
||||
InferClientAPI<Option> &
|
||||
InferActions<Option> & {
|
||||
useSession: () => Atom<{
|
||||
data: Session | null;
|
||||
data: Session;
|
||||
error: BetterFetchError | null;
|
||||
isPending: boolean;
|
||||
isRefetching: boolean;
|
||||
@@ -76,7 +81,7 @@ export function createAuthClient<Option extends ClientOptions>(
|
||||
$fetch: typeof $fetch;
|
||||
$store: typeof $store;
|
||||
$Infer: {
|
||||
Session: Session;
|
||||
Session: NonNullable<Session>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,7 +68,8 @@ export interface ClientOptions {
|
||||
|
||||
export type InferClientAPI<O extends ClientOptions> = InferRoutes<
|
||||
O["plugins"] extends Array<any>
|
||||
? (O["plugins"] extends Array<infer Pl>
|
||||
? Auth["api"] &
|
||||
(O["plugins"] extends Array<infer Pl>
|
||||
? UnionToIntersection<
|
||||
Pl extends {
|
||||
$InferServerPlugin: infer Plug;
|
||||
@@ -80,8 +81,7 @@ export type InferClientAPI<O extends ClientOptions> = InferRoutes<
|
||||
: {}
|
||||
: {}
|
||||
>
|
||||
: {}) &
|
||||
Auth["api"]
|
||||
: {})
|
||||
: Auth["api"],
|
||||
O
|
||||
>;
|
||||
|
||||
@@ -12,7 +12,10 @@ import type {
|
||||
import { createDynamicPathProxy } from "./proxy";
|
||||
import type { UnionToIntersection } from "../types/helper";
|
||||
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<
|
||||
infer Plugin
|
||||
@@ -60,24 +63,24 @@ export function createAuthClient<Option extends ClientOptions>(
|
||||
pluginsAtoms,
|
||||
atomListeners,
|
||||
);
|
||||
type ClientAPI = InferClientAPI<Option>;
|
||||
type Session = ClientAPI extends {
|
||||
getSession: () => Promise<BetterFetchResponse<infer D>>;
|
||||
}
|
||||
? D
|
||||
: ClientAPI;
|
||||
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
|
||||
InferClientAPI<Option> &
|
||||
ClientAPI &
|
||||
InferActions<Option> & {
|
||||
useSession: Atom<{
|
||||
data: {
|
||||
session: InferSessionFromClient<Option>;
|
||||
user: InferUserFromClient<Option>;
|
||||
};
|
||||
data: Session;
|
||||
error: BetterFetchError | null;
|
||||
isPending: boolean;
|
||||
}>;
|
||||
$fetch: typeof $fetch;
|
||||
$store: typeof $store;
|
||||
$Infer: {
|
||||
Session: {
|
||||
session: InferSessionFromClient<Option>;
|
||||
user: InferUserFromClient<Option>;
|
||||
};
|
||||
Session: NonNullable<Session>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ import type {
|
||||
ClientOptions,
|
||||
InferActions,
|
||||
InferClientAPI,
|
||||
InferSessionFromClient,
|
||||
InferUserFromClient,
|
||||
IsSignal,
|
||||
} 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";
|
||||
|
||||
function getAtomKey(str: string) {
|
||||
return `use${capitalizeFirstLetter(str)}`;
|
||||
@@ -55,14 +56,16 @@ export function createAuthClient<Option extends ClientOptions>(
|
||||
resolvedHooks[getAtomKey(key)] = () => useStore(value);
|
||||
}
|
||||
|
||||
type Session = {
|
||||
session: InferSessionFromClient<Option>;
|
||||
user: InferUserFromClient<Option>;
|
||||
};
|
||||
type ClientAPI = InferClientAPI<Option>;
|
||||
type Session = ClientAPI extends {
|
||||
getSession: () => Promise<BetterFetchResponse<infer D>>;
|
||||
}
|
||||
? D
|
||||
: ClientAPI;
|
||||
|
||||
function useSession(): () => DeepReadonly<
|
||||
Ref<{
|
||||
data: Session | null;
|
||||
data: Session;
|
||||
isPending: boolean;
|
||||
isRefetching: boolean;
|
||||
error: BetterFetchError | null;
|
||||
@@ -114,12 +117,13 @@ export function createAuthClient<Option extends ClientOptions>(
|
||||
pluginsAtoms,
|
||||
atomListeners,
|
||||
);
|
||||
|
||||
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
|
||||
InferClientAPI<Option> &
|
||||
InferActions<Option> & {
|
||||
useSession: typeof useSession;
|
||||
$Infer: {
|
||||
Session: Session;
|
||||
Session: NonNullable<Session>;
|
||||
};
|
||||
$fetch: typeof $fetch;
|
||||
$store: typeof $store;
|
||||
|
||||
@@ -4,9 +4,21 @@ import type { BetterAuthOptions } from "../types";
|
||||
import type { Adapter } from "../types/adapter";
|
||||
import { createKyselyAdapter } from "../adapters/kysely-adapter/dialect";
|
||||
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> {
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
10
packages/better-auth/src/plugins/custom-session/client.ts
Normal file
10
packages/better-auth/src/plugins/custom-session/client.ts
Normal 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">();
|
||||
};
|
||||
@@ -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!" });
|
||||
});
|
||||
});
|
||||
43
packages/better-auth/src/plugins/custom-session/index.ts
Normal file
43
packages/better-auth/src/plugins/custom-session/index.ts
Normal 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;
|
||||
};
|
||||
@@ -16,3 +16,4 @@ export * from "./multi-session";
|
||||
export * from "./email-otp";
|
||||
export * from "./one-tap";
|
||||
export * from "./oauth-proxy";
|
||||
export * from "./custom-session";
|
||||
|
||||
@@ -106,7 +106,7 @@ export async function getTestInstance<
|
||||
const testUser = {
|
||||
email: "test@test.com",
|
||||
password: "test123456",
|
||||
name: "test",
|
||||
name: "test user",
|
||||
...config?.testUser,
|
||||
};
|
||||
async function createTestUser() {
|
||||
|
||||
44
packages/better-auth/src/types/api.ts
Normal file
44
packages/better-auth/src/types/api.ts
Normal 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>;
|
||||
@@ -5,6 +5,17 @@ export type LiteralString = "" | (string & Record<never, never>);
|
||||
export type OmitId<T extends { id: unknown }> = Omit<T, "id">;
|
||||
|
||||
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> =
|
||||
| LiteralType
|
||||
| (BaseType & Record<never, never>);
|
||||
|
||||
@@ -66,7 +66,7 @@ export interface BetterAuthOptions {
|
||||
/**
|
||||
* Database configuration
|
||||
*/
|
||||
database:
|
||||
database?:
|
||||
| PostgresPool
|
||||
| MysqlPool
|
||||
| Database
|
||||
|
||||
Reference in New Issue
Block a user