mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-07 20:37:44 +00:00
feat: custom session response (#579)
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
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()
|
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.
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
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";
|
} 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;
|
||||||
|
|||||||
@@ -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",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ 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"] &
|
||||||
|
(O["plugins"] extends Array<infer Pl>
|
||||||
? UnionToIntersection<
|
? UnionToIntersection<
|
||||||
Pl extends {
|
Pl extends {
|
||||||
$InferServerPlugin: infer Plug;
|
$InferServerPlugin: infer Plug;
|
||||||
@@ -80,8 +81,7 @@ export type InferClientAPI<O extends ClientOptions> = InferRoutes<
|
|||||||
: {}
|
: {}
|
||||||
: {}
|
: {}
|
||||||
>
|
>
|
||||||
: {}) &
|
: {})
|
||||||
Auth["api"]
|
|
||||||
: Auth["api"],
|
: Auth["api"],
|
||||||
O
|
O
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -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>;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 "./email-otp";
|
||||||
export * from "./one-tap";
|
export * from "./one-tap";
|
||||||
export * from "./oauth-proxy";
|
export * from "./oauth-proxy";
|
||||||
|
export * from "./custom-session";
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
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 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>);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export interface BetterAuthOptions {
|
|||||||
/**
|
/**
|
||||||
* Database configuration
|
* Database configuration
|
||||||
*/
|
*/
|
||||||
database:
|
database?:
|
||||||
| PostgresPool
|
| PostgresPool
|
||||||
| MysqlPool
|
| MysqlPool
|
||||||
| Database
|
| Database
|
||||||
|
|||||||
Reference in New Issue
Block a user