feat(docs): APIMethod, documents all server & client auth examples (#2577)

This commit is contained in:
Maxwell
2025-07-18 09:20:10 +10:00
committed by GitHub
parent 8fa4c9ce7e
commit 1ed38cd28b
53 changed files with 4810 additions and 976 deletions

View File

@@ -58,10 +58,10 @@ export default function Component() {
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid w-full items-center gap-4">
<div className="grid items-center w-full gap-4">
{!isOtpSent ? (
<Button onClick={requestOTP} className="w-full">
<Mail className="mr-2 h-4 w-4" /> Send OTP to Email
<Mail className="w-4 h-4 mr-2" /> Send OTP to Email
</Button>
) : (
<>
@@ -94,9 +94,9 @@ export default function Component() {
}`}
>
{isError ? (
<AlertCircle className="h-4 w-4" />
<AlertCircle className="w-4 h-4" />
) : (
<CheckCircle2 className="h-4 w-4" />
<CheckCircle2 className="w-4 h-4" />
)}
<p className="text-sm">{message}</p>
</div>

View File

@@ -21,6 +21,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
import { contents } from "@/components/sidebar-content";
import { Endpoint } from "@/components/endpoint";
import { DividerText } from "@/components/divider-text";
import { APIMethod } from "@/components/api-method";
import { LLMCopyButton, ViewOptions } from "./page.client";
import { GenerateAppleJwt } from "@/components/generate-apple-jwt";
@@ -105,6 +106,7 @@ export default async function Page({
Accordion,
Accordions,
Endpoint,
APIMethod,
Callout: ({ children, ...props }) => (
<defaultMdxComponents.Callout
{...props}

View File

@@ -0,0 +1,715 @@
import { Endpoint } from "./endpoint";
// import { Tab, Tabs } from "fumadocs-ui/components/tabs";
import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "./ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
import { ReactNode } from "react";
import { Link } from "lucide-react";
import { Button } from "./ui/button";
import { cn } from "@/lib/utils";
type Property = {
isOptional: boolean;
description: string | null;
propName: string;
type: string;
exampleValue: string | null;
comments: string | null;
isServerOnly: boolean;
path: string[];
isNullable: boolean;
isClientOnly: boolean;
};
const placeholderProperty: Property = {
isOptional: false,
comments: null,
description: null,
exampleValue: null,
propName: "",
type: "",
isServerOnly: false,
path: [],
isNullable: false,
isClientOnly: false,
};
export const APIMethod = ({
path,
isServerOnly,
isClientOnly,
method,
children,
noResult,
requireSession,
note,
clientOnlyNote,
serverOnlyNote,
resultVariable = "data",
forceAsBody,
forceAsQuery,
}: {
/**
* Endpoint path
*/
path: string;
/**
* If enabled, we will add `headers` to the fetch options, indicating the given API method requires auth headers.
*
* @default false
*/
requireSession?: boolean;
/**
* The HTTP method to the endpoint
*
* @default "GET"
*/
method?: "POST" | "GET" | "DELETE" | "PUT";
/**
* Wether the endpoint is server only or not.
*
* @default false
*/
isServerOnly?: boolean;
/**
* Wether the code example is client-only, thus maening it's an endpoint.
*
* @default false
*/
isClientOnly?: boolean;
/**
* The `ts` codeblock which describes the API method.
* I recommend checking other parts of the Better-Auth docs which is using this component to get an idea of how to
* write out the children.
*/
children: JSX.Element;
/**
* If enabled, will remove the `const data = ` part, since this implies there will be no return data from the API method.
*/
noResult?: boolean;
/**
* A small note to display above the client-auth example code-block.
*/
clientOnlyNote?: string;
/**
* A small note to display above the server-auth example code-block.
*/
serverOnlyNote?: string;
/**
* A small note to display above both the client & server auth example code-blocks.
*/
note?: string;
/**
* The result output variable name.
*
* @default "data"
*/
resultVariable?: string;
/**
* Force the server auth API to use `body`, rather than auto choosing
*/
forceAsBody?: boolean;
/**
* Force the server auth API to use `query`, rather than auto choosing
*/
forceAsQuery?: boolean;
}) => {
let { props, functionName, code_prefix, code_suffix } = parseCode(children);
const authClientMethodPath = pathToDotNotation(path);
const clientBody = createClientBody({ props });
const serverBody = createServerBody({
props,
method: method ?? "GET",
requireSession: requireSession ?? false,
forceAsQuery,
forceAsBody,
});
const serverCodeBlock = (
<DynamicCodeBlock
code={`${code_prefix}${
noResult ? "" : `const ${resultVariable} = `
}await auth.api.${functionName}(${serverBody});${code_suffix}`}
lang="ts"
/>
);
let pathId = path.replaceAll("/", "-");
return (
<>
<div className="relative">
<div
id={`api-method${pathId}`}
aria-hidden
className="absolute invisible -top-[100px]"
/>
</div>
<Tabs
defaultValue={isServerOnly ? "server" : "client"}
className="w-full gap-0"
>
<TabsList className="relative flex justify-start w-full p-0 bg-transparent hover:[&>div>a>button]:opacity-100">
<TabsTrigger
value="client"
className="transition-all duration-150 ease-in-out max-w-[100px] data-[state=active]:bg-border hover:bg-border/50 bg-border/50 border hover:border-primary/15 cursor-pointer data-[state=active]:border-primary/10 rounded-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 36 36"
>
<path
fill="currentColor"
d="M23.81 26c-.35.9-.94 1.5-1.61 1.5h-8.46c-.68 0-1.26-.6-1.61-1.5H1v1.75A2.45 2.45 0 0 0 3.6 30h28.8a2.45 2.45 0 0 0 2.6-2.25V26Z"
/>
<path
fill="currentColor"
d="M7 10h22v14h3V7.57A1.54 1.54 0 0 0 30.5 6h-25A1.54 1.54 0 0 0 4 7.57V24h3Z"
/>
<path fill="none" d="M0 0h36v36H0z" />
</svg>
<span>Client</span>
</TabsTrigger>
<TabsTrigger
value="server"
className="transition-all duration-150 ease-in-out max-w-[100px] data-[state=active]:bg-border hover:bg-border/50 bg-border/50 border hover:border-primary/15 cursor-pointer data-[state=active]:border-primary/10 rounded-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M3 3h18v18H3zm2 2v6h14V5zm14 8H5v6h14zM7 7h2v2H7zm2 8H7v2h2z"
/>
</svg>
<span>Server</span>
</TabsTrigger>
<div className="absolute right-0">
<a href={`#api-method${pathId}`}>
<Button
variant="ghost"
className="transition-all duration-150 ease-in-out scale-90 opacity-100 md:opacity-0"
size={"icon"}
>
<Link className="size-4" />
</Button>
</a>
</div>
</TabsList>
<TabsContent value="client">
{isServerOnly ? null : (
<Endpoint
method={method || "GET"}
path={path}
isServerOnly={isServerOnly ?? false}
/>
)}
{clientOnlyNote || note ? (
<Note>
{note && tsxifyBackticks(note)}
{clientOnlyNote ? (
<>
{note ? <br /> : null}
{tsxifyBackticks(clientOnlyNote)}
</>
) : null}
</Note>
) : null}
<div className={cn("w-full relative")}>
<DynamicCodeBlock
code={`${code_prefix}${
noResult
? ""
: `const { data${
resultVariable === "data" ? "" : `: ${resultVariable}`
}, error } = `
}await authClient.${authClientMethodPath}(${clientBody});${code_suffix}`}
lang="ts"
/>
{isServerOnly ? (
<div className="absolute inset-0 flex items-center justify-center w-full h-full border rounded-lg backdrop-brightness-50 backdrop-blur-xs border-border">
<span>This is a server-only endpoint</span>
</div>
) : null}
</div>
{!isServerOnly ? <TypeTable props={props} isServer={false} /> : null}
</TabsContent>
<TabsContent value="server">
{isClientOnly ? null : (
<Endpoint
method={method || "GET"}
path={path}
isServerOnly={isServerOnly ?? false}
className=""
/>
)}
{serverOnlyNote || note ? (
<Note>
{note && tsxifyBackticks(note)}
{serverOnlyNote ? (
<>
{note ? <br /> : null}
{tsxifyBackticks(serverOnlyNote)}
</>
) : null}
</Note>
) : null}
<div className={cn("w-full relative")}>
{serverCodeBlock}
{isClientOnly ? (
<div className="absolute inset-0 flex items-center justify-center w-full h-full border rounded-lg backdrop-brightness-50 backdrop-blur-xs border-border">
<span>This is a client-only endpoint</span>
</div>
) : null}
</div>
{!isClientOnly ? <TypeTable props={props} isServer /> : null}
</TabsContent>
</Tabs>
</>
);
};
function pathToDotNotation(input: string): string {
return input
.split("/") // split into segments
.filter(Boolean) // remove empty strings (from leading '/')
.map((segment) =>
segment
.split("-") // split kebab-case
.map((word, i) =>
i === 0
? word.toLowerCase()
: word.charAt(0).toUpperCase() + word.slice(1),
)
.join(""),
)
.join(".");
}
function getChildren(
x:
| ({ props: { children: string } } | string)
| ({ props: { children: string } } | string)[],
): string[] {
if (Array.isArray(x)) {
const res = [];
for (const item of x) {
res.push(getChildren(item));
}
return res.flat();
} else {
if (typeof x === "string") return [x];
return [x.props.children];
}
}
function TypeTable({
props,
isServer,
}: { props: Property[]; isServer: boolean }) {
if (!isServer && !props.filter((x) => !x.isServerOnly).length) return null;
if (isServer && !props.filter((x) => !x.isClientOnly).length) return null;
if (!props.length) return null;
return (
<Table className="mt-2 mb-0 overflow-hidden">
<TableHeader>
<TableRow>
<TableHead className="text-primary w-[100px]">Prop</TableHead>
<TableHead className="text-primary">Description</TableHead>
<TableHead className="text-primary w-[100px]">Type</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{props.map((prop, i) =>
(prop.isServerOnly && isServer === false) ||
(prop.isClientOnly && isServer === true) ? null : (
<TableRow key={i}>
<TableCell>
<code>
{prop.path.join(".") + (prop.path.length ? "." : "")}
{prop.propName}
{prop.isOptional ? "?" : ""}
</code>
{prop.isServerOnly ? (
<span className="mx-2 text-xs text-muted-foreground">
(server-only)
</span>
) : null}
</TableCell>
<TableCell className="max-w-[500px] overflow-hidden">
<div className="w-full break-words h-fit text-wrap ">
{tsxifyBackticks(prop.description ?? "")}
</div>
</TableCell>
<TableCell className="max-w-[200px] overflow-auto">
<code>
{prop.type}
{prop.isNullable ? " | null" : ""}
</code>
</TableCell>
</TableRow>
),
)}
</TableBody>
</Table>
);
}
function tsxifyBackticks(input: string): JSX.Element {
const parts = input.split(/(`[^`]+`)/g); // Split by backtick sections
return (
<>
{parts.map((part, index) => {
if (part.startsWith("`") && part.endsWith("`")) {
const content = part.slice(1, -1); // remove backticks
return <code key={index}>{content}</code>;
} else {
return <span key={index}>{part}</span>;
}
})}
</>
);
}
function parseCode(children: JSX.Element) {
// These two variables are essentially taking the `children` JSX shiki code, and converting them to
// an array string purely of it's code content.
const arrayOfJSXCode = children?.props.children.props.children.props.children
.map((x: any) =>
x === "\n" ? { props: { children: { props: { children: "\n" } } } } : x,
)
.map((x: any) => x.props.children);
const arrayOfCode: string[] = arrayOfJSXCode
.flatMap(
(
x: { props: { children: string } } | { props: { children: string } }[],
) => {
return getChildren(x);
},
)
.join("")
.split("\n");
let props: Property[] = [];
let functionName: string = "";
let currentJSDocDescription: string = "";
let withinApiMethodType = false;
let hasAlreadyDefinedApiMethodType = false;
let isServerOnly_ = false;
let isClientOnly_ = false;
let nestPath: string[] = []; // str arr segmented-path, eg: ["data", "metadata", "something"]
let serverOnlyPaths: string[] = []; // str arr full-path, eg: ["data.metadata.something"]
let clientOnlyPaths: string[] = []; // str arr full-path, eg: ["data.metadata.something"]
let isNullable = false;
let code_prefix = "";
let code_suffix = "";
for (let line of arrayOfCode) {
const originalLine = line;
line = line.trim();
if (line === "}" && withinApiMethodType && !nestPath.length) {
withinApiMethodType = false;
hasAlreadyDefinedApiMethodType = true;
continue;
} else {
if (line === "}" && withinApiMethodType && nestPath.length) {
nestPath.pop();
continue;
}
}
if (
line.toLowerCase().startsWith("type") &&
!hasAlreadyDefinedApiMethodType &&
!withinApiMethodType
) {
withinApiMethodType = true;
// Will grab the name of the API method function name from:
// type createOrganization = {
// ^^^^^^^^^^^^^^^^^^
functionName = line.replace("type ", "").split("=")[0].trim();
continue;
}
if (!withinApiMethodType) {
if (!hasAlreadyDefinedApiMethodType) {
code_prefix += originalLine + "\n";
} else {
code_suffix += "\n" + originalLine + "";
}
continue;
}
if (
line.startsWith("/*") ||
line.startsWith("*") ||
line.startsWith("*/")
) {
if (line.startsWith("/*")) {
continue;
} else if (line.startsWith("*/")) {
continue;
} else {
if (line === "*" || line === "* ") continue;
line = line.replace("* ", "");
if (line.trim() === "@serverOnly") {
isServerOnly_ = true;
continue;
} else if (line.trim() === "@nullable") {
isNullable = true;
continue;
} else if (line.trim() === "@clientOnly") {
isClientOnly_ = true;
continue;
}
currentJSDocDescription += line + " ";
}
} else {
// New property field
// Example:
// name: string = "My Organization",
let propName = line.split(":")[0].trim();
const isOptional = propName.endsWith("?") ? true : false;
if (isOptional) propName = propName.slice(0, -1); // Remove `?` in propname.
let propType = line
.replace(propName, "")
.replace("?", "")
.replace(":", "")
.split("=")[0]
.trim()!;
let isTheStartOfNest = false;
if (propType === "{") {
// This means that it's a nested object.
propType = `Object`;
isTheStartOfNest = true;
nestPath.push(propName);
if (isServerOnly_) {
serverOnlyPaths.push(nestPath.join("."));
}
if (isClientOnly_) {
clientOnlyPaths.push(nestPath.join("."));
}
}
if (clientOnlyPaths.includes(nestPath.join("."))) {
isClientOnly_ = true;
}
if (serverOnlyPaths.includes(nestPath.join("."))) {
isServerOnly_ = true;
}
let exampleValue = !line.includes("=")
? null
: line
.replace(propName, "")
.replace("?", "")
.replace(":", "")
.replace(propType, "")
.replace("=", "")
.trim();
if (exampleValue?.endsWith(",")) exampleValue = exampleValue.slice(0, -1);
// const comments =
// line
// .replace(propName, "")
// .replace("?", "")
// .replace(":", "")
// .replace(propType, "")
// .replace("=", "")
// .replace(exampleValue || "IMPOSSIBLE_TO_REPLACE_!!!!", "")
// .split("//")[1] ?? null;
const comments = null;
const description =
currentJSDocDescription.length > 0 ? currentJSDocDescription : null;
if (description) {
currentJSDocDescription = "";
}
const property: Property = {
...placeholderProperty,
description,
comments,
exampleValue,
isOptional,
propName,
type: propType,
isServerOnly: isServerOnly_,
isClientOnly: isClientOnly_,
path: isTheStartOfNest
? nestPath.slice(0, nestPath.length - 1)
: nestPath.slice(),
isNullable: isNullable,
};
isServerOnly_ = false;
isClientOnly_ = false;
isNullable = false;
// console.log(property);
props.push(property);
}
}
return {
functionName,
props,
code_prefix,
code_suffix,
};
}
const indentationSpace = ` `;
function createClientBody({ props }: { props: Property[] }) {
let body = ``;
let i = -1;
for (const prop of props) {
i++;
if (prop.isServerOnly) continue;
if (body === "") body += "{\n";
let addComment = false;
let comment: string[] = [];
if (!prop.isOptional || prop.comments) addComment = true;
if (!prop.isOptional) comment.push("required");
if (prop.comments) comment.push(prop.comments);
body += `${indentationSpace.repeat(prop.path.length + 1)}${prop.propName}${
prop.exampleValue ? `: ${prop.exampleValue}` : ""
}${prop.type === "Object" ? ": {" : ","}${
addComment ? ` // ${comment.join(", ")}` : ""
}\n`;
if ((props[i + 1]?.path?.length || 0) < prop.path.length) {
const diff = prop.path.length - (props[i + 1]?.path?.length || 0);
for (const index of Array(diff)
.fill(0)
.map((_, i) => i)
.reverse()) {
body += `${indentationSpace.repeat(index + 1)}},\n`;
}
}
}
if (body !== "") body += "}";
return body;
}
function createServerBody({
props,
requireSession,
method,
forceAsBody,
forceAsQuery,
}: {
props: Property[];
requireSession: boolean;
method: string;
forceAsQuery: boolean | undefined;
forceAsBody: boolean | undefined;
}) {
let serverBody = "";
let body2 = ``;
let i = -1;
for (const prop of props) {
i++;
if (prop.isClientOnly) continue;
if (body2 === "") body2 += "{\n";
let addComment = false;
let comment: string[] = [];
if (!prop.isOptional || prop.comments) {
addComment = true;
}
if (
prop.isServerOnly &&
!(
prop.path.length &&
props.find(
(x) =>
x.path.join(".") ===
prop.path.slice(0, prop.path.length - 2).join(".") &&
x.propName === prop.path[prop.path.length - 1],
)
)
) {
comment.push("server-only");
addComment = true;
}
if (!prop.isOptional) comment.push("required");
if (prop.comments) comment.push(prop.comments);
body2 += `${indentationSpace.repeat(prop.path.length + 2)}${prop.propName}${
prop.exampleValue ? `: ${prop.exampleValue}` : ""
}${prop.type === "Object" ? ": {" : ","}${
addComment ? ` // ${comment.join(", ")}` : ""
}\n`;
if ((props[i + 1]?.path?.length || 0) < prop.path.length) {
const diff = prop.path.length - (props[i + 1]?.path?.length || 0);
for (const index of Array(diff)
.fill(0)
.map((_, i) => i)
.reverse()) {
body2 += `${indentationSpace.repeat(index + 2)}},\n`;
}
}
}
if (body2 !== "") body2 += " },";
let fetchOptions = "";
if (requireSession) {
fetchOptions +=
"\n // This endpoint requires session cookies.\n headers: await headers(),";
}
if (props.filter((x) => !x.isClientOnly).length > 0) {
serverBody += "{\n";
if ((method === "POST" || forceAsBody) && !forceAsQuery) {
serverBody += ` body: ${body2}${fetchOptions}\n}`;
} else {
serverBody += ` query: ${body2}${fetchOptions}\n}`;
}
} else if (fetchOptions.length) {
serverBody += `{${fetchOptions}\n}`;
}
return serverBody;
}
function Note({ children }: { children: ReactNode }) {
return (
<div className="relative flex flex-col w-full gap-2 p-3 mb-2 break-words border rounded-md text-md text-wrap border-border bg-fd-secondary/50">
<span className="w-full -mb-1 text-xs select-none text-muted-foreground">
Notes
</span>
<p className="mt-0 mb-0 text-sm">{children as any}</p>
</div>
);
}

View File

@@ -1,10 +1,8 @@
import { Server } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./ui/tooltip";
"use client";
import { Check, Copy } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "./ui/button";
import { useState } from "react";
function Method({ method }: { method: "POST" | "GET" | "DELETE" | "PUT" }) {
return (
@@ -18,31 +16,43 @@ export function Endpoint({
path,
method,
isServerOnly,
className,
}: {
path: string;
method: "POST" | "GET" | "DELETE" | "PUT";
isServerOnly?: boolean;
className?: string;
}) {
const [copying, setCopying] = useState(false);
return (
<div className="relative flex items-center w-full gap-2 p-2 border rounded-md border-muted bg-fd-secondary/50">
<div
className={cn(
"relative flex items-center w-full gap-2 p-2 border-t border-x border-border bg-fd-secondary/50 group",
className,
)}
>
<Method method={method} />
<span className="font-mono text-sm text-muted-foreground">{path}</span>
{isServerOnly && (
<div className="absolute right-2">
<TooltipProvider delayDuration={1}>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center justify-center transition-colors duration-150 ease-in-out size-6 text-muted-foreground/50 hover:text-muted-foreground">
<Server className="size-4" />
</div>
</TooltipTrigger>
<TooltipContent className="border bg-fd-popover text-fd-popover-foreground border-fd-border">
Server Only Endpoint
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="absolute right-2" slot="copy">
<Button
variant="ghost"
size="icon"
className="transition-all duration-150 ease-in-out opacity-0 cursor-pointer scale-80 group-hover:opacity-100"
onClick={() => {
setCopying(true);
navigator.clipboard.writeText(path);
setTimeout(() => {
setCopying(false);
}, 1000);
}}
>
{copying ? (
<Check className="duration-150 ease-in-out size-4 zoom-in" />
) : (
<Copy className="size-4" />
)}
</Button>
</div>
</div>
);
}

View File

@@ -34,46 +34,82 @@ export const auth = betterAuth({
### Sign Up
To sign a user up, you can use the `signUp.email` function provided by the client. The `signUp` function takes an object with the following properties:
To sign a user up, you can use the `signUp.email` function provided by the client.
- `email`: The email address of the user.
- `password`: The password of the user. It should be at least 8 characters long and max 128 by default.
- `name`: The name of the user.
- `image`: The image of the user. (optional)
- `callbackURL`: The URL to redirect to after the user verifies their email. (optional)
```ts title="auth-client.ts"
const { data, error } = await authClient.signUp.email({
email: "test@example.com",
password: "password1234",
name: "test",
image: "https://example.com/image.png",
});
<APIMethod path="/sign-up/email" method="POST">
```ts
type signUpEmail = {
/**
* The name of the user.
*/
name: string = "John Doe"
/**
* The email address of the user.
*/
email: string = "john.doe@example.com"
/**
* The password of the user. It should be at least 8 characters long and max 128 by default.
*/
password: string = "password1234"
/**
* An optional profile image of the user.
*/
image?: string = "https://example.com/image.png"
/**
* An optional URL to redirect to after the user signs up.
*/
callbackURL?: string = "https://example.com/callback"
}
```
</APIMethod>
<Callout>
These are the default properties for the sign up email endpoint, however it's possible that with [additonal fields](/docs/concepts/typescript#additional-user-fields) or special plugins you can pass more properties to the endpoint.
</Callout>
### Sign In
To sign a user in, you can use the `signIn.email` function provided by the client. The `signIn` function takes an object with the following properties:
To sign a user in, you can use the `signIn.email` function provided by the client.
- `email`: The email address of the user.
- `password`: The password of the user.
- `rememberMe`: If false, the user will be signed out when the browser is closed. (optional) (default: true)
- `callbackURL`: The URL to redirect to after the user signs in. (optional)
```ts title="auth-client.ts"
const { data, error } = await authClient.signIn.email({
email: "test@example.com",
password: "password1234",
});
<APIMethod path="/sign-in/email" method="POST" requireSession>
```ts
type signInEmail = {
/**
* The email address of the user.
*/
email: string = "john.doe@example.com"
/**
* The password of the user. It should be at least 8 characters long and max 128 by default.
*/
password: string = "password1234"
/**
* If false, the user will be signed out when the browser is closed. (optional) (default: true)
*/
rememberMe?: boolean = true
/**
* An optional URL to redirect to after the user signs in. (optional)
*/
callbackURL?: string = "https://example.com/callback"
}
```
</APIMethod>
<Callout>
These are the default properties for the sign in email endpoint, however it's possible that with [additonal fields](/docs/concepts/typescript#additional-user-fields) or special plugins you can pass different properties to the endpoint.
</Callout>
### Sign Out
To sign a user out, you can use the `signOut` function provided by the client.
```ts title="auth-client.ts"
await authClient.signOut();
<APIMethod path="/sign-out" method="POST" requireSession noResult>
```ts
type signOut = {
}
```
</APIMethod>
you can pass `fetchOptions` to redirect onSuccess
@@ -197,33 +233,54 @@ export const auth = betterAuth({
Once you configured your server you can call `requestPasswordReset` function to send reset password link to user. If the user exists, it will trigger the `sendResetPassword` function you provided in the auth config.
It takes an object with the following properties:
- `email`: The email address of the user.
- `redirectTo`: The URL to redirect to after the user clicks on the link in the email. If the token is valid, the user will be redirected to this URL with the token in the query string. If the token is invalid, the user will be redirected to this URL with an error message in the query string `?error=invalid_token`.
```ts title="auth-client.ts"
const { data, error } = await authClient.requestPasswordReset({
email: "test@example.com",
redirectTo: "/reset-password",
});
<APIMethod path="/request-password-reset" method="POST">
```ts
type requestPasswordReset = {
/**
* The email address of the user to send a password reset email to
*/
email: string = "john.doe@example.com"
/**
* The URL to redirect the user to reset their password. If the token isn't valid or expired, it'll be redirected with a query parameter `?error=INVALID_TOKEN`. If the token is valid, it'll be redirected with a query parameter `?token=VALID_TOKEN
*/
redirectTo?: string = "https://example.com/reset-password"
}
```
</APIMethod>
When a user clicks on the link in the email, they will be redirected to the reset password page. You can add the reset password page to your app. Then you can use `resetPassword` function to reset the password. It takes an object with the following properties:
- `newPassword`: The new password of the user.
```ts title="auth-client.ts"
const token = new URLSearchParams(window.location.search).get("token");
if (!token) {
// Handle the error
}
const { data, error } = await authClient.resetPassword({
newPassword: "password1234",
token,
});
```
<APIMethod path="/reset-password" method="POST">
```ts
const token = new URLSearchParams(window.location.search).get("token");
if (!token) {
// Handle the error
}
type resetPassword = {
/**
* The new password to set
*/
newPassword: string = "password1234"
/**
* The token to reset the password
*/
token: string
}
```
</APIMethod>
### Update password
<Callout type="warn">

View File

@@ -82,122 +82,153 @@ This plugin offers two main methods to do a second factor verification:
To enable two-factor authentication, call `twoFactor.enable` with the user's password and issuer (optional):
```ts title="two-factor.ts"
const { data } = await authClient.twoFactor.enable({
password: "password", // user password required
issuer: "my-app-name", // Optional, defaults to the app name
});
<APIMethod
path="/two-factor/enable"
method="POST"
requireSession
>
```ts
type enableTwoFactor = {
/**
* The user's password
*/
password: string = "secure-password"
/**
* An optional custom issuer for the TOTP URI. Defaults to app-name defined in your auth config.
*/
issuer?: string = "my-app-name"
}
```
</APIMethod>
When 2FA is enabled:
- An encrypted `secret` and `backupCodes` are generated.
- `enable` returns `totpURI` and `backupCodes`.
Note: `twoFactorEnabled` wont be set to `true` until the user verifies their TOTP code.
Note: `twoFactorEnabled` wont be set to `true` until the user verifies their TOTP code. Learn more about veryifying TOTP [here](#totp). You can skip verification by setting `skipVerificationOnEnable` to true in your plugin config.
To verify, display the QR code for the user to scan with their authenticator app. After they enter the code, call `verifyTotp`:
```ts
await authClient.twoFactor.verifyTotp({
code: "" // user input
})
```
<Callout>
You can skip verification by setting `skipVerificationOnEnable` to true in your plugin config.
<Callout type="warn">
Two Factor can only be enabled for credential accounts at the moment. For social accounts, it's assumed the provider already handles 2FA.
</Callout>
### Sign In with 2FA
When a user with 2FA enabled tries to sign in via email, the response will contain `twoFactorRedirect` set to `true`. This indicates that the user needs to verify their 2FA code.
When a user with 2FA enabled tries to sign in via email, the response object will contain `twoFactorRedirect` set to `true`. This indicates that the user needs to verify their 2FA code.
```ts title="sign-in.ts"
You can handle this in the `onSuccess` callback or by providing a `onTwoFactorRedirect` callback in the plugin config.
```ts title="sign-in.tsx"
await authClient.signIn.email({
email: "user@example.com",
password: "password123",
})
},
{
async onSuccess(context) {
if (context.data.twoFactorRedirect) {
// Handle the 2FA verification in place
}
},
}
)
```
You can handle this in the `onSuccess` callback or by providing a `onTwoFactorRedirect` callback in the plugin config.
Using the `onTwoFactorRedirect` config:
```ts title="sign-in.ts"
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [twoFactorClient({
plugins: [
twoFactorClient({
onTwoFactorRedirect(){
// Handle the 2FA verification globally
}
})]
})
},
}),
],
});
```
Or you can handle it in place:
```ts
await authClient.signIn.email({
email: "user@example.com",
password: "password123",
}, {
async onSuccess(context) {
if (context.data.twoFactorRedirect) {
// Handle the 2FA verification in place
}
}
}
})
```
#### Using `auth.api`
When you call `auth.api.signInEmail` on the server, and the user has 2FA enabled, it will, by default, respond with an object where `twoFactorRedirect` is set to `true`. This behavior isnt inferred in TypeScript, which can be misleading. We recommend passing `asResponse: true` to receive the Response object instead.
<Callout type="warn">
**With `auth.api`**
When you call `auth.api.signInEmail` on the server, and the user has 2FA enabled, it will return an object where `twoFactorRedirect` is set to `true`. This behavior isnt inferred in TypeScript, which can be misleading. You can check using `in` instead to check if `twoFactorRedirect` is set to `true`.
```ts
const response = await auth.api.signInEmail({
email: "my-email@email.com",
password: "secure-password",
asResponse: true
})
body: {
email: "test@test.com",
password: "test",
},
});
if ("twoFactorRedirect" in response) {
// Handle the 2FA verification in place
}
```
</Callout>
### Disabling 2FA
To disable two-factor authentication, call `twoFactor.disable` with the user's password:
```ts title="two-factor.ts"
const { data } = await authClient.twoFactor.disable({
password: "password" // user password required
})
<APIMethod
path="/two-factor/disable"
method="POST"
requireSession
>
```ts
type disableTwoFactor = {
/**
* The user's password
*/
password: string
}
```
</APIMethod>
### TOTP
TOTP (Time-Based One-Time Password) is an algorithm that generates a unique password for each login attempt using time as a counter. Every fixed interval (Better Auth defaults to 30 seconds), a new password is generated. This addresses several issues with traditional passwords: they can be forgotten, stolen, or guessed. OTPs solve some of these problems, but their delivery via SMS or email can be unreliable (or even risky, considering it opens new attack vectors).
TOTP, however, generates codes offline, making it both secure and convenient. You just need an authenticator app on your phone, and youre set—no internet required.
TOTP, however, generates codes offline, making it both secure and convenient. You just need an authenticator app on your phone.
#### Getting TOTP URI
After enabling 2FA, you can get the TOTP URI to display to the user. This URI is generated by the server using the `secret` and `issuer` and can be used to generate a QR code for the user to scan with their authenticator app.
<APIMethod
path="/two-factor/get-totp-uri"
method="POST"
requireSession
>
```ts
const { data, error } = await authClient.twoFactor.getTotpUri({
password: "password" // user password required
})
type getTOTPURI = {
/**
* The user's password
*/
password: string
}
```
</APIMethod>
**Example: Using React**
Once you have the TOTP URI, you can use it to generate a QR code for the user to scan with their authenticator app.
```tsx title="user-card.tsx"
import QRCode from "react-qr-code";
export default function UserCard(){
export default function UserCard({ password }: { password: string }){
const { data: session } = client.useSession();
const { data: qr } = useQuery({
queryKey: ["two-factor-qr"],
queryFn: async () => {
const res = await authClient.twoFactor.getTotpUri();
const res = await authClient.twoFactor.getTotpUri({ password });
return res.data;
},
enabled: !!session?.user.twoFactorEnabled,
@@ -216,11 +247,20 @@ By default the issuer for TOTP is set to the app name provided in the auth confi
After the user has entered their 2FA code, you can verify it using `twoFactor.verifyTotp` method.
<APIMethod path="/two-factor/verify-totp" method="POST">
```ts
const verifyTotp = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyTotp({ code })
type verifyTOTP = {
/**
* The otp code to verify.
*/
code: string = "012345"
/**
* If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
*/
trustDevice?: boolean = true
}
```
</APIMethod>
### OTP
@@ -249,29 +289,39 @@ export const auth = betterAuth({
Sending an OTP is done by calling the `twoFactor.sendOtp` function. This function will trigger your sendOTP implementation that you provided in the Better Auth configuration.
<APIMethod path="/two-factor/send-otp" method="POST">
```ts
const { data, error } = await authClient.twoFactor.sendOtp()
type send2FaOTP = {
/**
* If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
*/
trustDevice?: boolean = true
}
if (data) {
// redirect or show the user to enter the code
}
```
</APIMethod>
#### Verifying OTP
After the user has entered their OTP code, you can verify it
<APIMethod path="/two-factor/verify-otp" method="POST">
```ts
const verifyOtp = async (code: string) => {
await authClient.twoFactor.verifyOtp({ code }, {
onSuccess(){
//redirect the user on success
},
onError(ctx){
alert(ctx.error.message)
}
})
type verifyOTP = {
/**
* The otp code to verify.
*/
code: string = "012345"
/**
* If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
*/
trustDevice?: boolean = true
}
```
</APIMethod>
### Backup Codes
@@ -280,43 +330,77 @@ Backup codes are generated and stored in the database. This can be used to recov
#### Generating Backup Codes
Generate backup codes for account recovery:
<APIMethod
path="/two-factor/generate-backup-codes"
method="POST"
requireSession
>
```ts
const { data, error } = await authClient.twoFactor.generateBackupCodes({
password: "password" // user password required
})
type generateBackupCodes = {
/**
* The users password.
*/
password: string
}
if (data) {
// Show the backup codes to the user
}
```
</APIMethod>
<Callout type="warn">
When you generate backup codes, the old backup codes will be deleted and new ones will be generated.
</Callout>
#### Using Backup Codes
You can now allow users to provider backup code as account recover method.
```ts
await authClient.twoFactor.verifyBackupCode({code: ""}, {
onSuccess(){
//redirect the user on success
},
onError(ctx){
alert(ctx.error.message)
}
})
```
once a backup code is used, it will be removed from the database and can't be used again.
<APIMethod path="/two-factor/verify-backup-code" method="POST">
```ts
type verifyBackupCode = {
/**
* A backup code to verify.
*/
code: string = "123456"
/**
* If true, the session cookie will not be set.
*/
disableSession?: boolean = false
/**
* If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.
*/
trustDevice?: boolean = true
}
```
</APIMethod>
<Callout>
Once a backup code is used, it will be removed from the database and can't be used again.
</Callout>
#### Viewing Backup Codes
You can view the backup codes at any time by calling `viewBackupCodes`. This action can only be performed on the server using `auth.api`.
To display the backup codes to the user, you can call `viewBackupCodes` on the server. This will return the backup codes in the response. You should only this if the user has a fresh session - a session that was just created.
<APIMethod
path="/two-factor/view-backup-codes"
method="GET"
isServerOnly
forceAsBody
>
```ts
await auth.api.viewBackupCodes({
body: {
userId: "user-id"
}
})
type viewBackupCodes = {
/**
* The user ID to view all backup codes.
*/
userId?: string | null = "user-id"
}
```
</APIMethod>
### Trusted Devices
@@ -462,9 +546,9 @@ import { twoFactorClient } from "better-auth/client/plugins"
const authClient = createAuthClient({
plugins: [
twoFactorClient({ // [!code highlight]
onTwoFactorRedirect(){
window.location.href = "/2fa" // Handle the 2FA verification redirect
}
onTwoFactorRedirect(){ // [!code highlight]
window.location.href = "/2fa" // Handle the 2FA verification redirect // [!code highlight]
} // [!code highlight]
}) // [!code highlight]
]
})

View File

@@ -74,59 +74,94 @@ Before performing any admin operations, the user must be authenticated with an a
Allows an admin to create a new user.
```ts title="admin.ts"
const newUser = await authClient.admin.createUser({
name: "Test User",
email: "test@example.com",
password: "password123",
role: "user", // this can also be an array for multiple roles (e.g. ["user", "sale"])
data: {
// any additional on the user table including plugin fields and custom fields
customField: "customValue",
},
});
<APIMethod
path="/admin/create-user"
method="POST"
resultVariable="newUser"
>
```ts
type createUser = {
/**
* The email of the user.
*/
email: string = "user@example.com"
/**
* The password of the user.
*/
password: string = "some-secure-password"
/**
* The name of the user.
*/
name: string = "James Smith"
/**
* A string or array of strings representing the roles to apply to the new user.
*/
role?: string | string[] = "user"
/**
* Extra fields for the user. Including custom additional fields.
*/
data?: Record<string, any> = { customField: "customValue" }
}
```
</APIMethod>
### List Users
Allows an admin to list all users in the database.
```ts title="admin.ts"
const users = await authClient.admin.listUsers({
query: {
limit: 10,
},
});
<APIMethod
path="/admin/list-users"
method="GET"
requireSession
note={"All properties are optional to configure. By default, 100 rows are returned, you can configure this by the `limit` property."}
resultVariable={"users"}
>
```ts
type listUsers = {
/**
* The value to search for.
*/
searchValue?: string = "some name"
/**
* The field to search in, defaults to email. Can be `email` or `name`.
*/
searchField?: "email" | "name" = "name"
/**
* The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`.
*/
searchOperator?: "contains" | "starts_with" | "ends_with" = "contains"
/**
* The number of users to return. Defaults to 100.
*/
limit?: string | number = 100
/**
* The offset to start from.
*/
offset?: string | number = 100
/**
* The field to sort by.
*/
sortBy?: string = "name"
/**
* The direction to sort by.
*/
sortDirection?: "asc" | "desc" = "desc"
/**
* The field to filter by.
*/
filterField?: string = "email"
/**
* The value to filter by.
*/
filterValue?: string | number | boolean = "hello@example.com"
/**
* The operator to use for the filter.
*/
filterOperator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" = "eq"
}
```
</APIMethod>
By default, 100 users are returned. You can adjust the limit and offset using the following query parameters:
- `search`: The search query to apply to the users. It can be an object with the following properties:
- `field`: The field to search on, which can be `email` or `name`.
- `operator`: The operator to use for the search. It can be `contains`, `starts_with`, or `ends_with`.
- `value`: The value to search for.
- `limit`: The number of users to return.
- `offset`: The number of users to skip.
- `sortBy`: The field to sort the users by.
- `sortDirection`: The direction to sort the users by. Defaults to `asc`.
- `filter`: The filter to apply to the users. It can be an array of objects.
```ts title="admin.ts"
const users = await authClient.admin.listUsers({
query: {
searchField: "email",
searchOperator: "contains",
searchValue: "@example.com",
limit: 10,
offset: 0,
sortBy: "createdAt",
sortDirection: "desc",
filterField: "role",
filterOperator: "eq",
filterValue: "admin"
}
});
```
#### Query Filtering
@@ -177,92 +212,204 @@ const totalPages = Math.ceil(totalUsers / limit)
Changes the role of a user.
```ts title="admin.ts"
const updatedUser = await authClient.admin.setRole({
userId: "user_id_here",
role: "admin", // this can also be an array for multiple roles (e.g. ["admin", "sale"])
});
<APIMethod
path="/admin/set-role"
method="POST"
requireSession
>
```ts
type setRole = {
/**
* The user id which you want to set the role for.
*/
userId?: string = "user-id"
/**
* The role to set, this can be a string or an array of strings.
*/
role: string | string[] = "admin"
}
```
</APIMethod>
### Set User Password
Changes the password of a user.
<APIMethod
path="/admin/set-user-password"
method="POST"
requireSession
>
```ts
type setUserPassword = {
/**
* The new password.
*/
newPassword: string = 'new-password'
/**
* The user id which you want to set the password for.
*/
userId: string = 'user-id'
}
```
</APIMethod>
### Ban User
Bans a user, preventing them from signing in and revokes all of their existing sessions.
```ts title="admin.ts"
const bannedUser = await authClient.admin.banUser({
userId: "user_id_here",
banReason: "Spamming", // Optional (if not provided, the default ban reason will be used - No reason)
banExpiresIn: 60 * 60 * 24 * 7, // Optional (if not provided, the ban will never expire)
});
<APIMethod
path="/admin/ban-user"
method="POST"
requireSession
noResult
>
```ts
type banUser = {
/**
* The user id which you want to ban.
*/
userId: string = "user-id"
/**
* The reason for the ban.
*/
banReason?: string = "Spamming"
/**
* The number of seconds until the ban expires. If not provided, the ban will never expire.
*/
banExpiresIn?: number = 60 * 60 * 24 * 7
}
```
</APIMethod>
### Unban User
Removes the ban from a user, allowing them to sign in again.
```ts title="admin.ts"
const unbannedUser = await authClient.admin.unbanUser({
userId: "user_id_here",
});
<APIMethod
path="/admin/unban-user"
method="POST"
requireSession
noResult
>
```ts
type unbanUser = {
/**
* The user id which you want to unban.
*/
userId: string = "user-id"
}
```
</APIMethod>
### List User Sessions
Lists all sessions for a user.
```ts title="admin.ts"
const sessions = await authClient.admin.listUserSessions({
userId: "user_id_here",
});
<APIMethod
path="/admin/list-user-sessions"
method="POST"
requireSession
>
```ts
type listUserSessions = {
/**
* The user id.
*/
userId: string = "user-id"
}
```
</APIMethod>
### Revoke User Session
Revokes a specific session for a user.
```ts title="admin.ts"
const revokedSession = await authClient.admin.revokeUserSession({
sessionToken: "session_token_here",
});
<APIMethod
path="/admin/revoke-user-session"
method="POST"
requireSession
>
```ts
type revokeUserSession = {
/**
* The session token which you want to revoke.
*/
sessionToken: string = "session_token_here"
}
```
</APIMethod>
### Revoke All Sessions for a User
Revokes all sessions for a user.
```ts title="admin.ts"
const revokedSessions = await authClient.admin.revokeUserSessions({
userId: "user_id_here",
});
<APIMethod
path="/admin/revoke-user-sessions"
method="POST"
requireSession
>
```ts
type revokeUserSessions = {
/**
* The user id which you want to revoke all sessions for.
*/
userId: string = "user-id"
}
```
</APIMethod>
### Impersonate User
This feature allows an admin to create a session that mimics the specified user. The session will remain active until either the browser session ends or it reaches 1 hour. You can change this duration by setting the `impersonationSessionDuration` option.
```ts title="admin.ts"
const impersonatedSession = await authClient.admin.impersonateUser({
userId: "user_id_here",
});
<APIMethod
path="/admin/impersonate-user"
method="POST"
requireSession
>
```ts
type impersonateUser = {
/**
* The user id which you want to impersonate.
*/
userId: string = "user-id"
}
```
</APIMethod>
### Stop Impersonating User
To stop impersonating a user and continue with the admin account, you can use `stopImpersonating`
```ts title="admin.ts"
await authClient.admin.stopImpersonating();
<APIMethod path="/admin/stop-impersonating" method="POST" noResult requireSession>
```ts
type stopImpersonating = {
}
```
</APIMethod>
### Remove User
Hard deletes a user from the database.
```ts title="admin.ts"
const deletedUser = await authClient.admin.removeUser({
userId: "user_id_here",
});
<APIMethod
path="/admin/remove-user"
method="POST"
requireSession
resultVariable="deletedUser"
>
```ts
type removeUser = {
/**
* The user id which you want to remove.
*/
userId: string = "user-id"
}
```
</APIMethod>
## Access Control
@@ -418,6 +565,33 @@ The plugin provides an easy way to define your own set of permissions for each r
To check a user's permissions, you can use the `hasPermission` function provided by the client.
<APIMethod path="/admin/has-permission" method="POST">
```ts
type userHasPermission = {
/**
* The user id which you want to check the permissions for.
*/
userId?: string = "user-id"
/**
* Check role permissions.
* @serverOnly
*/
role?: string = "admin"
/**
* Optionally check if a single permission is granted. Must use this, or permissions.
*/
permission?: Record<string, string[]> = { "project": ["create", "update"] } /* Must use this, or permissions */,
/**
* Optionally check if multiple permissions are granted. Must use this, or permission.
*/
permissions?: Record<string, string[]>
}
```
</APIMethod>
Example usage:
```ts title="auth-client.ts"
const canCreateProject = await authClient.admin.hasPermission({
permissions: {
@@ -436,6 +610,8 @@ const canCreateProjectAndCreateSale = await authClient.admin.hasPermission({
If you want to check a user's permissions server-side, you can use the `userHasPermission` action provided by the `api` to check the user's permissions.
```ts title="api.ts"
import { auth } from "@/auth";

View File

@@ -73,75 +73,74 @@ You can view the list of API Key plugin options [here](/docs/plugins/api-key#api
### Create an API key
<Endpoint path="/api-key/create" method="POST" />
<Tabs items={['Client', 'Server']}>
<Tab value="Client">
```ts
const { data: apiKey, error } = await authClient.apiKey.create({
name: "My API Key",
expiresIn: 60 * 60 * 24 * 7, // 7 days
prefix: "my_app",
metadata: {
tier: "premium",
},
});
```
</Tab>
<Tab value="Server">
on the server, you can create an API key for a user by passing the `userId` property in the body. And allows you to add any properties you want to the API key.
```ts
const apiKey = await auth.api.createApiKey({
body: {
name: "My API Key",
expiresIn: 60 * 60 * 24 * 365, // 1 year
prefix: "my_app",
remaining: 100,
refillAmount: 100,
refillInterval: 60 * 60 * 24 * 7, // 7 days
metadata: {
tier: "premium",
},
rateLimitTimeWindow: 1000 * 60 * 60 * 24, // everyday
rateLimitMax: 100, // every day, they can use up to 100 requests
rateLimitEnabled: true,
userId: user.id, // the user ID to create the API key for
},
});
```
</Tab>
</Tabs>
All API keys are assigned to a user. If you're creating an API key on the server, without access to headers, you must pass the `userId` property. This is the ID of the user that the API key is associated with.
#### Properties
All properties are optional. However if you pass a `refillAmount`, you must also pass a `refillInterval`, and vice versa.
- `name`?: The name of the API key.
- `expiresIn`?: The expiration time of the API key in seconds. If not provided, the API key will never expire.
- `prefix`?: The prefix of the API key. This is used to identify the API key in the database.
- `metadata`?: The metadata of the API key. This is used to store additional information about the API key.
<DividerText>Server Only Properties</DividerText>
- `remaining`?: The remaining number of requests for the API key. If `null`, then there is no cap to key usage.
- `refillAmount`?: The amount to refill the `remaining` count of the API key.
- `refillInterval`?: The interval to refill the API key in milliseconds.
- `rateLimitTimeWindow`?: The duration in milliseconds where each request is counted. Once the `rateLimitMax` is reached, the request will be rejected until the `timeWindow` has passed, at which point the time window will be reset.
- `rateLimitMax`?: The maximum number of requests allowed within the `rateLimitTimeWindow`.
- `rateLimitEnabled`?: Whether rate limiting is enabled for the API key.
- `permissions`?: Permissions for the API key, structured as a record mapping resource types to arrays of allowed actions.
<APIMethod
path="/api-key/create"
method="POST"
serverOnlyNote="If you're creating an API key on the server, without access to headers, you must pass the `userId` property. This is the ID of the user that the API key is associated with."
clientOnlyNote="You can adjust more specific API key configurations by using the server method instead."
>
```ts
const example = {
projects: ["read", "read-write"],
};
type createApiKey = {
/**
* Name of the Api Key.
*/
name?: string = 'project-api-key'
/**
* Expiration time of the Api Key in seconds.
*/
expiresIn?: number = 60 * 60 * 24 * 7
/**
* User Id of the user that the Api Key belongs to. server-only.
* @serverOnly
*/
userId?: string = "user-id"
/**
* Prefix of the Api Key.
*/
prefix?: string = 'project-api-key'
/**
* Remaining number of requests. server-only.
* @serverOnly
*/
remaining?: number = 100
/**
* Metadata of the Api Key.
*/
metadata?: any | null = { someKey: 'someValue' }
/**
* Amount to refill the remaining count of the Api Key. server-only.
* @serverOnly
*/
refillAmount?: number = 100
/**
* Interval to refill the Api Key in milliseconds. server-only.
* @serverOnly
*/
refillInterval?: number = 1000
/**
* The duration in milliseconds where each request is counted. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only.
* @serverOnly
*/
rateLimitTimeWindow?: number = 1000
/**
* Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only.
* @serverOnly
*/
rateLimitMax?: number = 100
/**
* Whether the key has rate limiting enabled. server-only.
* @serverOnly
*/
rateLimitEnabled?: boolean = true
/**
* Permissions of the Api Key.
*/
permissions?: Record<string, string[]>
}
```
</APIMethod>
- `userId`?: The ID of the user associated with the API key. When creating an API Key, you must pass the headers of the user who will own the key. However if you do not have the headers, you can pass this field, which will allow you to bypass the need for headers.
<Callout>API keys are assigned to a user.</Callout>
#### Result
@@ -152,30 +151,29 @@ Otherwise if it throws, it will throw an `APIError`.
### Verify an API key
<Endpoint path="/api-key/verify" method="POST" isServerOnly />
<APIMethod
path="/api-key/verify"
method="POST"
isServerOnly
>
```ts
const { valid, error, key } = await auth.api.verifyApiKey({
body: {
key: "your_api_key_here",
},
});
//with permissions check
const { valid, error, key } = await auth.api.verifyApiKey({
body: {
key: "your_api_key_here",
permissions: {
const permissions = { // Permissions to check are optional.
projects: ["read", "read-write"],
},
},
});
}
type verifyApiKey = {
/**
* The key to verify.
*/
key: string = "your_api_key_here"
/**
* The permissions to verify. Optional.
*/
permissions?: Record<string, string[]>
}
```
</APIMethod>
#### Properties
- `key`: The API Key to validate
- `permissions`?: The permissions to check against the API key.
#### Result
@@ -191,19 +189,20 @@ type Result = {
### Get an API key
<Endpoint method="GET" path="/api-key/get" isServerOnly />
<APIMethod
path="/api-key/get"
method="GET"
requireSession
>
```ts
const key = await auth.api.getApiKey({
body: {
keyId: "your_api_key_id_here",
},
});
type getApiKey = {
/**
* The id of the Api Key.
*/
id: string = "some-api-key-id"
}
```
#### Properties
- `keyId`: The API key ID to get information on.
</APIMethod>
#### Result
@@ -218,56 +217,75 @@ type Result = Omit<ApiKey, "key">;
### Update an API key
<Endpoint method="POST" path="/api-key/update" isServerOnly />
<Tabs items={['Client', 'Server']}>
<Tab value="Client">
```ts
const { data: apiKey, error } = await authClient.apiKey.update({
keyId: "your_api_key_id_here",
name: "New API Key Name",
enabled: false,
});
```
</Tab>
<Tab value="Server">
You can update an API key on the server by passing the `keyId` and any other properties you want to update.
```ts
const apiKey = await auth.api.updateApiKey({
body: {
keyId: "your_api_key_id_here",
name: "New API Key Name",
userId: "userId",
enabled: false,
remaining: 100,
refillAmount: null,
refillInterval: null,
metadata: null,
expiresIn: 60 * 60 * 24 * 7,
rateLimitEnabled: false,
rateLimitTimeWindow: 1000 * 60 * 60 * 24,
rateLimitMax: 100,
},
});
```
</Tab>
</Tabs>
#### Properties
<DividerText>Client</DividerText>- `keyId`: The API key ID to update on. -
`name`?: Update the key name.
<DividerText>Server Only</DividerText>- `userId`?: Update the user ID who owns
this key. - `name`?: Update the key name. - `enabled`?: Update whether the API
key is enabled or not. - `remaining`?: Update the remaining count. -
`refillAmount`?: Update the amount to refill the `remaining` count every
interval. - `refillInterval`?: Update the interval to refill the `remaining`
count. - `metadata`?: Update the metadata of the API key. - `expiresIn`?: Update
the expiration time of the API key. In seconds. - `rateLimitEnabled`?: Update
whether the rate-limiter is enabled or not. - `rateLimitTimeWindow`?: Update the
time window for the rate-limiter. - `rateLimitMax`?: Update the maximum number
of requests they can make during the rate-limit-time-window.
<APIMethod path="/api-key/update" method="POST">
```ts
type updateApiKey = {
/**
* The id of the Api Key to update.
*/
keyId: string = "some-api-key-id"
/**
* The id of the user which the api key belongs to. server-only.
* @serverOnly
*/
userId?: string = "some-user-id"
/**
* The name of the key.
*/
name?: string = "some-api-key-name"
/**
* Whether the Api Key is enabled or not. server-only.
* @serverOnly
*/
enabled?: boolean = true
/**
* The number of remaining requests. server-only.
* @serverOnly
*/
remaining?: number = 100
/**
* The refill amount. server-only.
* @serverOnly
*/
refillAmount?: number = 100
/**
* The refill interval in milliseconds. server-only.
* @serverOnly
*/
refillInterval?: number = 1000
/**
* The metadata of the Api Key. server-only.
* @serverOnly
*/
metadata?: any | null = { "key": "value" }
/**
* Expiration time of the Api Key in seconds. server-only.
* @serverOnly
*/
expiresIn?: number = 60 * 60 * 24 * 7
/**
* Whether the key has rate limiting enabled. server-only.
* @serverOnly
*/
rateLimitEnabled?: boolean = true
/**
* The duration in milliseconds where each request is counted. server-only.
* @serverOnly
*/
rateLimitTimeWindow?: number = 1000
/**
* Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only.
* @serverOnly
*/
rateLimitMax?: number = 100
/**
* Update the permissions on the API Key. server-only.
* @serverOnly
*/
permissions?: Record<string, string[]>
}
```
</APIMethod>
#### Result
@@ -278,30 +296,21 @@ Otherwise, you'll receive the API Key details, except for the `key` value itself
### Delete an API Key
<Endpoint method="POST" path="/api-key/delete" />
<Tabs items={['Client', 'Server']}>
<Tab value="Client">
```ts
const { data: result, error } = await authClient.apiKey.delete({
keyId: "your_api_key_id_here",
});
```
</Tab>
<Tab value="Server">
```ts
const apiKey = await auth.api.deleteApiKey({
body: {
keyId: "your_api_key_id_here",
userId: "userId",
},
});
```
</Tab>
</Tabs>
#### Properties
- `keyId`: The API key ID to delete.
<APIMethod
path="/api-key/delete"
method="POST"
requireSession
note="This endpoint is attempting to delete the API key from the perspective of the user. It will check if the user's ID matches the key owner to be able to delete it. If you want to delete a key without these checks, we recommend you use an ORM to directly mutate your DB instead."
>
```ts
type deleteApiKey = {
/**
* The id of the Api Key to delete.
*/
keyId: string = "some-api-key-id"
}
```
</APIMethod>
#### Result
@@ -318,22 +327,16 @@ type Result = {
### List API keys
<Endpoint method="GET" path="/api-key/list" />
<Tabs items={['Client', 'Server']}>
<Tab value="Client">
```ts
const { data: apiKeys, error } = await authClient.apiKey.list();
```
</Tab>
<Tab value="Server">
```ts
const apiKeys = await auth.api.listApiKeys({
headers: user_headers,
});
```
</Tab>
</Tabs>
<APIMethod
path="/api-key/list"
method="GET"
requireSession
>
```ts
type listApiKeys = {
}
```
</APIMethod>
#### Result
@@ -350,12 +353,16 @@ type Result = ApiKey[];
This function will delete all API keys that have an expired expiration date.
<Endpoint
method="DELETE"
<APIMethod
path="/api-key/delete-all-expired-api-keys"
method="POST"
isServerOnly
/>
```ts await auth.api.deleteAllExpiredApiKeys(); ```
>
```ts
type deleteAllExpiredApiKeys = {
}
```
</APIMethod>
<Callout>
We automatically delete expired API keys every time any apiKey plugin
@@ -521,11 +528,12 @@ export const auth = betterAuth({
customKeyGenerator: () => {
return crypto.randomUUID();
},
defaultKeyLength: 36 // Or whatever the length is
})
]
defaultKeyLength: 36, // Or whatever the length is
}),
],
});
```
</Callout>
If an API key is validated from your `customAPIKeyValidator`, we still must match that against the database's key.
@@ -598,7 +606,7 @@ Customize the starting characters configuration.
<Accordion title="startingCharactersConfig Options">
`shouldStore` <span className="opacity-70">`boolean`</span>
Whether to store the starting characters in the database.
Wether to store the starting characters in the database.
If false, we will set `start` to `null`.
Default is `true`.
@@ -659,7 +667,7 @@ Customize the key expiration.
`disableCustomExpiresTime` <span className="opacity-70">`boolean`</span>
Whether to disable the expires time passed from the client.
Wether to disable the expires time passed from the client.
If `true`, the expires time will be based on the default values.
Default is `false`.

View File

@@ -103,11 +103,17 @@ const authClient = createAuthClient({
To link account with Dub, you need to use the `dub.link`.
<APIMethod path="/dub/link" method="POST" requireSession>
```ts
const response = await authClient.dub.link({
callbackURL: "/dashboard", // URL to redirect to after linking
});
type dubLink = {
/**
* URL to redirect to after linking
* @clientOnly
*/
callbackURL: string = "/dashboard"
}
```
</APIMethod>
## Options

View File

@@ -52,48 +52,99 @@ The Email OTP plugin allows user to sign in, verify their email, or reset their
First, send an OTP to the user's email address.
```ts title="example.ts"
const { data, error } = await authClient.emailOtp.sendVerificationOtp({
email: "user-email@email.com",
type: "sign-in" // or "email-verification", "forget-password"
})
<APIMethod path="/email-otp/send-verification-otp" method="POST">
```ts
type sendVerificationOTP = {
/**
* Email address to send the OTP.
*/
email: string = "user@example.com"
/**
* Type of the OTP. `sign-in`, `email-verification`, or `forget-password`.
*/
type: "email-verification" | "sign-in" | "forget-password" = "sign-in"
}
```
</APIMethod>
### Sign in with OTP
Once the user provides the OTP, you can sign in the user using the `signIn.emailOtp()` method.
```ts title="example.ts"
const { data, error } = await authClient.signIn.emailOtp({
email: "user-email@email.com",
otp: "123456"
})
<APIMethod path="/sign-in/email-otp" method="POST">
```ts
type signInEmailOTP = {
/**
* Email address to sign in.
*/
email: string = "user@example.com"
/**
* OTP sent to the email.
*/
otp: string = "123456"
}
```
</APIMethod>
<Callout>
If the user is not registered, they'll be automatically registered. If you want to prevent this, you can pass `disableSignUp` as `true` in the options.
</Callout>
### Verify Email
To verify the user's email address, use the `verifyEmail()` method.
```ts title="example.ts"
const { data, error } = await authClient.emailOtp.verifyEmail({
email: "user-email@email.com",
otp: "123456"
})
<APIMethod path="/email-otp/verify-email" method="POST">
```ts
type verifyEmailOTP = {
/**
* Email address to verify.
*/
email: string = "user@example.com"
/**
* OTP to verify.
*/
otp: string = "123456"
}
```
</APIMethod>
### Reset Password
### Forgot & Reset Password
To reset the user's password, use the `resetPassword()` method.
To reset the user's password, you must use the `forgotPassword` method:
```ts title="example.ts"
const { data, error } = await authClient.emailOtp.resetPassword({
email: "user-email@email.com",
otp: "123456",
password: "password"
})
<APIMethod path="/forget-password/email-otp" method="POST">
```ts
type forgetPasswordEmailOTP = {
/**
* Email address to send the OTP.
*/
email: string = "user@example.com"
}
```
</APIMethod>
After that, you may use the `resetPassword()` method to apply the password reset.
<APIMethod path="/email-otp/reset-password" method="POST">
```ts
type resetPasswordEmailOTP = {
/**
* Email address to reset the password.
*/
email: string = "user@example.com"
/**
* OTP sent to the email.
*/
otp: string = "123456"
/**
* New password.
*/
password: string = "new-secure-password"
}
```
</APIMethod>
### Override Default Email Verification

View File

@@ -63,23 +63,63 @@ The Generic OAuth plugin provides endpoints for initiating the OAuth flow and ha
To start the OAuth sign-in process:
```ts title="sign-in.ts"
const response = await authClient.signIn.oauth2({
providerId: "provider-id",
callbackURL: "/dashboard" // the path to redirect to after the user is authenticated
});
<APIMethod path="/sign-in/oauth2" method="POST">
```ts
type signInWithOAuth2 = {
/**
* The provider ID for the OAuth provider.
*/
providerId: string = "provider-id"
/**
* The URL to redirect to after sign in.
*/
callbackURL?: string = "/dashboard"
/**
* The URL to redirect to if an error occurs.
*/
errorCallbackURL?: string = "/error-page"
/**
* The URL to redirect to after login if the user is new.
*/
newUserCallbackURL?: string = "/welcome"
/**
* Disable redirect.
*/
disableRedirect?: boolean = false
/**
* Scopes to be passed to the provider authorization request.
*/
scopes?: string[] = ["my-scope"]
/**
* Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider.
*/
requestSignUp?: boolean = false
}
```
</APIMethod>
### Linking OAuth Accounts
To link an OAuth account to an existing user:
```ts title="link-account.ts"
const response = await authClient.oauth2.link({
providerId: "provider-id",
callbackURL: "/dashboard" // the path to redirect to after the account is linked
});
<APIMethod
path="/oauth2/link"
method="POST"
requireSession
>
```ts
type oAuth2LinkAccount = {
/**
* The OAuth provider ID.
*/
providerId: string = "my-provider-id"
/**
* The URL to redirect to once the account linking was complete.
*/
callbackURL: string = "/successful-link"
}
```
</APIMethod>
### Handle OAuth Callback

View File

@@ -53,14 +53,32 @@ Magic link or email link is a way to authenticate users without a password. When
To sign in with a magic link, you need to call `signIn.magicLink` with the user's email address. The `sendMagicLink` function is called to send the magic link to the user's email.
```ts title="magic-link.ts"
const { data, error } = await authClient.signIn.magicLink({
email: "user@email.com",
callbackURL: "/dashboard", //redirect after successful login (optional)
});
<APIMethod
path="/sign-in/magic-link"
method="POST"
requireSession
>
```ts
type signInMagicLink = {
/**
* Email address to send the magic link.
*/
email: string = "user@email.com"
/**
* User display name. Only used if the user is registering for the first time.
*/
name?: string = "my-name"
/**
* URL to redirect after magic link verification.
*/
callbackURL?: string = "/dashboard"
}
```
</APIMethod>
<Callout>
If the user has not signed up, unless `disableSignUp` is set to `true`, the user will be signed up automatically.
</Callout>
### Verify Magic Link
@@ -72,13 +90,25 @@ When you send the URL generated by the `sendMagicLink` function to a user, click
If you want to handle the verification manually, (e.g, if you send the user a different URL), you can use the `verify` function.
```ts title="magic-link.ts"
const { data, error } = await authClient.magicLink.verify({
query: {
token,
},
});
<APIMethod
path="/magic-link/verify"
method="GET"
requireSession
>
```ts
type magicLinkVerify = {
/**
* Verification token.
*/
token: string = "123456"
/**
* URL to redirect after magic link verification, if not provided will return the session.
*/
callbackURL?: string = "/dashboard"
}
```
</APIMethod>
## Configuration Options

View File

@@ -49,35 +49,54 @@ Whenever a user logs in, the plugin will add additional cookie to the browser. T
To list all active sessions for the current user, you can call the `listDeviceSessions` method.
<APIMethod
path="/multi-session/list-device-sessions"
method="GET"
requireSession
>
```ts
await authClient.multiSession.listDeviceSessions()
```
on the server you can call `listDeviceSessions` method.
```ts
await auth.api.listDeviceSessions()
type listDeviceSessions = {
}
```
</APIMethod>
### Set active session
To set the active session, you can call the `setActive` method.
<APIMethod
path="/multi-session/set-active"
method="POST"
requireSession
>
```ts
await authClient.multiSession.setActive({
sessionToken: "session-token"
})
type setActiveSession = {
/**
* The session token to set as active.
*/
sessionToken: string = "some-session-token"
}
```
</APIMethod>
### Revoke a session
To revoke a session, you can call the `revoke` method.
<APIMethod
path="/multi-session/revoke"
method="POST"
requireSession
>
```ts
await authClient.multiSession.revoke({
sessionToken: "session-token"
})
type revokeDeviceSession = {
/**
* The session token to revoke.
*/
sessionToken: string = "some-session-token"
}
```
</APIMethod>
### Signout and Revoke all sessions

View File

@@ -87,18 +87,103 @@ Once installed, you can utilize the OIDC Provider to manage authentication flows
To register a new OIDC client, use the `oauth2.register` method.
<Endpoint path="/oauth2/register" method="POST" />
```ts title="client.ts"
#### Simple Example
```ts
const application = await client.oauth2.register({
client_name: "My Client",
redirect_uris: ["https://client.example.com/callback"],
});
```
#### Full Method
<APIMethod path="/oauth2/register" method="POST">
```ts
type registerOAuthApplication = {
/**
* A list of redirect URIs.
*/
redirect_uris: string[] = ["https://client.example.com/callback"]
/**
* The authentication method for the token endpoint.
*/
token_endpoint_auth_method?: "none" | "client_secret_basic" | "client_secret_post" = "client_secret_basic"
/**
* The grant types supported by the application.
*/
grant_types?: ("authorization_code" | "implicit" | "password" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:jwt-bearer" | "urn:ietf:params:oauth:grant-type:saml2-bearer")[] = ["authorization_code"]
/**
* The response types supported by the application.
*/
response_types?: ("code" | "token")[] = ["code"]
/**
* The name of the application.
*/
client_name?: string = "My App"
/**
* The URI of the application.
*/
client_uri?: string = "https://client.example.com"
/**
* The URI of the application logo.
*/
logo_uri?: string = "https://client.example.com/logo.png"
/**
* The scopes supported by the application. Separated by spaces.
*/
scope?: string = "profile email"
/**
* The contact information for the application.
*/
contacts?: string[] = ["admin@example.com"]
/**
* The URI of the application terms of service.
*/
tos_uri?: string = "https://client.example.com/tos"
/**
* The URI of the application privacy policy.
*/
policy_uri?: string = "https://client.example.com/policy"
/**
* The URI of the application JWKS.
*/
jwks_uri?: string = "https://client.example.com/jwks"
/**
* The JWKS of the application.
*/
jwks?: Record<string, any> = {"keys": [{"kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..."}]}
/**
* The metadata of the application.
*/
metadata?: Record<string, any> = {"key": "value"}
/**
* The software ID of the application.
*/
software_id?: string = "my-software"
/**
* The software version of the application.
*/
software_version?: string = "1.0.0"
/**
* The software statement of the application.
*/
software_statement?: string
}
```
</APIMethod>
<Callout>
This endpoint supports [RFC7591](https://datatracker.ietf.org/doc/html/rfc7591) compliant client registration.
</Callout>
Once the application is created, you will receive a `client_id` and `client_secret` that you can display to the user.
This Endpoint support [RFC7591](https://datatracker.ietf.org/doc/html/rfc7591) compliant client registration.
### Trusted Clients
For first-party applications and internal services, you can configure trusted clients directly in your OIDC provider configuration. Trusted clients bypass database lookups for better performance and can optionally skip consent screens for improved user experience.
@@ -108,7 +193,8 @@ import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";
const auth = betterAuth({
plugins: [oidcProvider({
plugins: [
oidcProvider({
loginPage: "/sign-in",
trustedClients: [
{

View File

@@ -25,20 +25,16 @@ export const auth = betterAuth({
Generate a token using `auth.api.generateOneTimeToken` or `authClient.oneTimeToken.generate`
<Tabs items={["Server", "Client"]}>
<Tab value="Server">
```ts
const response = await auth.api.generateOneTimeToken({
headers: await headers() // pass the request headers
})
```
</Tab>
<Tab value="Client">
```ts
const response = await authClient.oneTimeToken.generate()
```
</Tab>
</Tabs>
<APIMethod
path="/one-time-token/generate"
method="GET"
requireSession
>
```ts
type generateOneTimeToken = {
}
```
</APIMethod>
This will return a `token` that is attached to the current session which can be used to verify the one-time token. By default, the token will expire in 3 minutes.
@@ -46,27 +42,16 @@ This will return a `token` that is attached to the current session which can be
When the user clicks the link or submits the token, use the `auth.api.verifyOneTimeToken` or `authClient.oneTimeToken.verify` method in another API route to validate it.
<Tabs items={["Server", "Client"]}>
<Tab value="Server">
```ts
const token = request.query.token as string; //retrieve a token
const response = await auth.api.verifyOneTimeToken({
body: {
token
}
})
```
</Tab>
<Tab value="Client">
```ts
const url = window.location.href;
const token = url.split("token=")[1]; //retrieve a token
const response = await authClient.oneTimeToken.verify({
token
})
```
</Tab>
</Tabs>
<APIMethod path="/one-time-token/verify" method="POST">
```ts
type verifyOneTimeToken = {
/**
* The token to verify.
*/
token: string = "some-token"
}
```
</APIMethod>
This will return the session that was attached to the token.

View File

@@ -67,20 +67,39 @@ Once you've installed the plugin, you can start using the organization plugin to
### Create an organization
To create an organization, you need to provide:
- `name`: The name of the organization.
- `slug`: The slug of the organization.
- `logo`: The logo of the organization. (Optional)
```ts title="auth-client.ts"
await authClient.organization.create({
name: "My Organization",
slug: "my-org",
logo: "https://example.com/logo.png"
})
```
<APIMethod path="/organization/create" method="POST" requireSession>
```ts
const metadata = { someKey: "someValue" };
type createOrganization = {
/**
* The organization name.
*/
name: string = "My Organization"
/**
* The organization slug.
*/
slug: string = "my-org"
/**
* The organization logo.
*/
logo?: string = "https://example.com/logo.png"
/**
* The metadata of the organization.
*/
metadata?: Record<string, any>
/**
* The user id of the organization creator. If not provided, the current user will be used. Should only be used by admins or when called by the server.
* @serverOnly
*/
userId?: string = "some_user_id"
/**
* Whether to keep the current active organization active after creating a new one.
*/
keepCurrentActiveOrganization?: boolean = false
}
```
</APIMethod>
#### Restrict who can create an organization
@@ -107,13 +126,16 @@ const auth = betterAuth({
To check if an organization slug is taken or not you can use the `checkSlug` function provided by the client. The function takes an object with the following properties:
- `slug`: The slug of the organization.
```ts title="auth-client.ts"
await authClient.organization.checkSlug({
slug: "my-org",
});
<APIMethod path="/organization/check-slug" method="POST">
```ts
type checkOrganizationSlug = {
/**
* The organization slug to check.
*/
slug: string = "my-org"
}
```
</APIMethod>
#### Organization Creation Hooks
@@ -238,6 +260,15 @@ export default {
</Tab>
</Tabs>
Or alternatively, you can call `organization.list` if you don't want to use a hook.
<APIMethod path="/organization/list" method="GET">
```ts
type listOrganizations = {
}
```
</APIMethod>
### Active Organization
@@ -250,43 +281,27 @@ Active organization is the workspace the user is currently working on. By defaul
#### Set Active Organization
You can set the active organization by calling the `organization.setActive` function. It'll set the active organization for the user session.
<Tabs items={["client", "server"]} defaultValue="client">
<Tab value="client">
```ts title="auth-client.ts"
import { authClient } from "@/lib/auth-client";
await authClient.organization.setActive({
organizationId: "organization-id"
})
<Callout>
In some applications, you may want the ability to un-set an active organization.
In this case, you can call this endpoint with `organizationId` set to `null`.
</Callout>
// you can also use organizationSlug instead of organizationId
await authClient.organization.setActive({
organizationSlug: "organization-slug"
})
```
</Tab>
<APIMethod path="/organization/set-active" method="POST">
```ts
type setActiveOrganization = {
/**
* The organization id to set as active. It can be null to unset the active organization.
*/
organizationId?: string | null = "org-id"
/**
* The organization slug to set as active. It can be null to unset the active organization if organizationId is not provided.
*/
organizationSlug?: string = "org-slug"
}
```
</APIMethod>
<Tab value="server">
```ts title="api.ts"
import { auth } from "@/lib/auth";
await auth.api.setActiveOrganization({
headers: // pass the headers,
body: {
organizationSlug: "organization-slug"
}
})
// you can also use organizationId instead of organizationSlug
await auth.api.setActiveOrganization({
headers: // pass the headers,
body: {
organizationId: "organization-id"
}
})
```
</Tab>
</Tabs>
To set active organization when a session is created you can use [database hooks](/docs/concepts/database#database-hooks).
@@ -374,73 +389,88 @@ To retrieve the active organization for the user, you can call the `useActiveOrg
### Get Full Organization
To get the full details of an organization, you can use the `getFullOrganization` function provided by the client. The function takes an object with the following properties:
To get the full details of an organization, you can use the `getFullOrganization` function.
By default, if you don't pass any properties, it will use the active organization.
- `organizationId`: The ID of the organization. (Optional) By default, it will use the active organization.
- `organizationSlug`: The slug of the organization. (Optional) To get the organization by slug.
<Tabs items={["client", "server"]}>
<Tab value="client">
```ts title="auth-client.ts"
const organization = await authClient.organization.getFullOrganization({
query: { organizationId: "organization-id" } // optional, by default it will use the active organization
})
// you can also use organizationSlug instead of organizationId
const organization = await authClient.organization.getFullOrganization({
query: { organizationSlug: "organization-slug" }
})
```
</Tab>
<Tab value="server">
```ts title="api.ts"
import { auth } from "@/auth";
auth.api.getFullOrganization({
headers: // pass the headers
})
// you can also use organizationSlug instead of organizationId
auth.api.getFullOrganization({
headers: // pass the headers,
query: {
organizationSlug: "organization-slug"
}
})
```
</Tab>
</Tabs>
<APIMethod
path="/organization/get-full-organization"
method="GET"
requireSession
>
```ts
type getFullOrganization = {
/**
* The organization id to get. By default, it will use the active organization.
*/
organizationId?: string = "org-id"
/**
* The organization slug to get.
*/
organizationSlug?: string = "org-slug"
}
```
</APIMethod>
### Update Organization
To update organization info, you can use `organization.update`
<APIMethod
path="/organization/update"
method="POST"
requireSession
>
```ts
await authClient.organization.update({
type updateOrganization = {
/**
* A partial list of data to update the organization.
*/
data: {
name: "updated-name",
logo: "new-logo.url",
metadata: {
customerId: "test"
},
slug: "updated-slug"
},
organizationId: 'org-id' //defaults to the current active organization
})
/**
* The name of the organization.
*/
name?: string = "updated-name"
/**
* The slug of the organization.
*/
slug?: string = "updated-slug"
/**
* The logo of the organization.
*/
logo?: string = "new-logo.url"
/**
* The metadata of the organization.
*/
metadata?: Record<string, any> | null = { customerId: "test" }
}
/**
* The organization ID. to update.
*/
organizationId?: string = "org-id"
}
```
</APIMethod>
### Delete Organization
To remove user owned organization, you can use `organization.delete`
```ts title="org.ts"
await authClient.organization.delete({
organizationId: "test"
});
<APIMethod
path="/organization/delete"
method="POST"
requireSession
>
```ts
type deleteOrganization = {
/*
* The organization id to delete.
*/
organizationId: string = "org-id"
}
```
</APIMethod>
If the user has the necessary permissions (by default: role is owner) in the specified organization, all members, invitations and organization information will be removed.
@@ -500,16 +530,32 @@ export const auth = betterAuth({
To invite users to an organization, you can use the `invite` function provided by the client. The `invite` function takes an object with the following properties:
- `email`: The email address of the user.
- `role`: The role of the user in the organization. It can be `admin`, `member`, or `guest`.
- `organizationId`: The ID of the organization. this is optional by default it will use the active organization. (Optional)
```ts title="invitation.ts"
await authClient.organization.inviteMember({
email: "test@email.com",
role: "admin", //this can also be an array for multiple roles (e.g. ["admin", "sale"])
})
<APIMethod path="/organization/invite-member" method="POST">
```ts
type createInvitation = {
/**
* The email address of the user to invite.
*/
email: string = "example@gmail.com"
/**
* The role(s) to assign to the user. It can be `admin`, `member`, or `guest`.
*/
role: string | string[] = "member"
/**
* The organization ID to invite the user to. Defaults to the active organization.
*/
organizationId?: string = "org-id"
/**
* Resend the invitation email, if the user is already invited.
*/
resend?: boolean = true
/**
* The team ID to invite the user to.
*/
teamId?: string = "team-id"
}
```
</APIMethod>
<Callout>
- If the user is already a member of the organization, the invitation will be canceled.
@@ -523,52 +569,83 @@ When a user receives an invitation email, they can click on the invitation link
Make sure to call the `acceptInvitation` function after the user is logged in.
```ts title="auth-client.ts"
await authClient.organization.acceptInvitation({
invitationId: "invitation-id"
})
<APIMethod path="/organization/accept-invitation" method="POST">
```ts
type acceptInvitation = {
/**
* The ID of the invitation to accept.
*/
invitationId: string = "invitation-id"
}
```
</APIMethod>
### Update Invitation Status
To update the status of invitation you can use the `acceptInvitation`, `cancelInvitation`, `rejectInvitation` functions provided by the client. The functions take the invitation ID as an argument.
### Cancel Invitation
```ts title="auth-client.ts"
//cancel invitation
await authClient.organization.cancelInvitation({
invitationId: "invitation-id"
})
If a user has sent out an invitation, you can use this method to cancel it.
//reject invitation (needs to be called when the user who received the invitation is logged in)
await authClient.organization.rejectInvitation({
invitationId: "invitation-id"
})
If you're looking for how a user can reject an invitation, you can find that [here](#reject-invitation).
<APIMethod path="/organization/cancel-invitation" method="POST" noResult>
```ts
type cancelInvitation = {
/**
* The ID of the invitation to cancel.
*/
invitationId: string = "invitation-id"
}
```
</APIMethod>
### Reject Invitation
If this user has recieved an invitation, but wants to decline it, this method will allow you to do so by rejecting it.
<APIMethod path="/organization/reject-invitation" method="POST" noResult>
```ts
type rejectInvitation = {
/**
* The ID of the invitation to reject.
*/
invitationId: string = "invitation-id"
}
```
</APIMethod>
### Get Invitation
To get an invitation you can use the `getInvitation` function provided by the client. You need to provide the invitation ID as a query parameter.
To get an invitation you can use the `organization.getInvitation` function provided by the client. You need to provide the invitation id as a query parameter.
```ts title="auth-client.ts"
await authClient.organization.getInvitation({
query: {
id: params.id
}
})
<APIMethod
path="/organization/get-invitation"
method="GET"
requireSession
>
```ts
type getInvitation = {
/**
* The ID of the invitation to get.
*/
id: string = "invitation-id"
}
```
</APIMethod>
### List Invitations
To list all invitations for a given organization you can use the `listInvitations` function provided by the client.
```ts title="auth-client.ts"
const invitations = await authClient.organization.listInvitations({
query: {
organizationId: "organization-id" // optional, by default it will use the active organization
}
})
<APIMethod path="/organization/list-invitations" method="GET">
```ts
type listInvitations = {
/**
* An optional ID of the organization to list invitations for. If not provided, will default to the users active organization.
*/
organizationId?: string = "organization-id"
}
```
</APIMethod>
### List user invitations
@@ -600,59 +677,112 @@ The `email` query parameter is only available on the server to query for invitat
To remove you can use `organization.removeMember`
```ts title="auth-client.ts"
//remove member
await authClient.organization.removeMember({
memberIdOrEmail: "member-id", // this can also be the email of the member
organizationId: "organization-id" // optional, by default it will use the active organization
})
<APIMethod path="/organization/remove-member" method="POST">
```ts
type removeMember = {
/**
* The ID or email of the member to remove.
*/
memberIdOrEmail: string = "user@example.com"
/**
* The ID of the organization to remove the member from. If not provided, the active organization will be used.
*/
organizationId?: string = "org-id"
}
```
</APIMethod>
### Update Member Role
To update the role of a member in an organization, you can use the `organization.updateMemberRole`. If the user has the permission to update the role of the member, the role will be updated.
```ts title="auth-client.ts"
await authClient.organization.updateMemberRole({
memberId: "member-id",
role: "admin" // this can also be an array for multiple roles (e.g. ["admin", "sale"])
})
<APIMethod path="/organization/update-member-role" method="POST" noResult>
```ts
type updateMemberRole = {
/**
* The new role to be applied. This can be a string or array of strings representing the roles.
*/
role: string | string[] = ["admin", "sale"]
/**
* The member id to apply the role update to.
*/
memberId: string = "member-id"
/**
* An optional organization ID which the member is a part of to apply the role update. If not provided, you must provide session headers to get the active organization.
*/
organizationId?: string = "organization-id"
}
```
</APIMethod>
### Get Active Member
To get the member details of the active organization you can use the `organization.getActiveMember` function.
To get the current member of the active organization you can use the `organization.getActiveMember` function. This function will return the user's member details in their active organization.
```ts title="auth-client.ts"
const member = await authClient.organization.getActiveMember()
<APIMethod
path="/organization/get-active-member"
method="GET"
requireSession
resultVariable="member"
>
```ts
type getActiveMember = {
}
```
</APIMethod>
### Add Member
If you want to add a member directly to an organization without sending an invitation, you can use the `addMember` function which can only be invoked on the server.
```ts title="api.ts"
import { auth } from "@/auth";
await auth.api.addMember({
body: {
userId: "user-id",
organizationId: "organization-id",
role: "admin", // this can also be an array for multiple roles (e.g. ["admin", "sale"])
teamId: "team-id" // Optionally specify a teamId to add the member to a team. (requires teams to be enabled)
}
})
<APIMethod
path="/organization/add-member"
method="POST"
isServerOnly
>
```ts
type addMember = {
/**
* The user Id which represents the user to be added as a member. If `null` is provided, then it's expected to provide session headers.
*/
userId?: string | null = "user-id"
/**
* The role(s) to assign to the new member.
*/
role: string | string[] = ["admin", "sale"]
/**
* An optional organization ID to pass. If not provided, will default to the user's active organization.
*/
organizationId?: string = "org-id"
/**
* An optional team ID to add the member to.
*/
teamId?: string = "team-id"
}
```
</APIMethod>
### Leave Organization
To leave organization you can use `organization.leave` function. This function will remove the current user from the organization.
```ts title="auth-client.ts"
await authClient.organization.leave({
organizationId: "organization-id"
})
<APIMethod
path="/organization/leave"
method="POST"
requireSession
noResult
>
```ts
type leaveOrganization = {
/**
* The organization Id for the member to leave.
*/
organizationId: string = "organization-id"
}
```
</APIMethod>
## Access Control
@@ -931,45 +1061,95 @@ export const authClient = createAuthClient({
#### Create Team
Create a new team within an organization:
<APIMethod path="/organization/create-team" method="POST">
```ts
const team = await authClient.organization.createTeam({
name: "Development Team",
organizationId: "org-id" // Optional: defaults to active organization
})
type createTeam = {
/**
* The name of the team.
*/
name: string = "my-team"
/**
* The organization ID which the team will be created in. Defaults to the active organization.
*/
organizationId?: string = "organization-id"
}
```
</APIMethod>
#### List Teams
Get all teams in an organization:
<APIMethod
path="/organization/list-teams"
method="GET"
requireSession
>
```ts
const teams = await authClient.organization.listTeams({
query: {
organizationId: org.id, // Optional: defaults to active organization
},
});
type listOrganizationTeams = {
/**
* The organization ID which the teams are under to list. Defaults to the users active organization.
*/
organizationId?: string = "organziation-id"
}
```
</APIMethod>
#### Update Team
Update a team's details:
<APIMethod
path="/organization/update-team"
method="POST"
requireSession
>
```ts
const updatedTeam = await authClient.organization.updateTeam({
teamId: "team-id",
type updateTeam = {
/**
* The ID of the team to be updated.
*/
teamId: string = "team-id"
/**
* A partial object containing options for you to update.
*/
data: {
name: "Updated Team Name"
/**
* The name of the team to be updated.
*/
name?: string = "My new team name"
/**
* The organization ID which the team falls under.
*/
organizationId?: string = "My new organization ID for this team"
/**
* The timestamp of when the team was created.
*/
createdAt?: Date = new Date()
/**
* The timestamp of when the team was last updated.
*/
updatedAt?: Date = new Date()
}
})
}
```
</APIMethod>
#### Remove Team
Delete a team from an organization:
<APIMethod path="/organization/remove-team" method="POST">
```ts
await authClient.organization.removeTeam({
teamId: "team-id",
organizationId: "org-id" // Optional: defaults to active organization
})
type removeTeam = {
/**
* The team ID of the team to remove.
*/
teamId: string = "team-id"
/**
* The organization ID which the team falls under. If not provided, it will default to the user's active organization.
*/
organizationId?: string = "organization-id"
}
```
</APIMethod>
### Team Permissions
@@ -1287,7 +1467,8 @@ To change the schema table name or fields, you can pass `schema` option to the o
```ts title="auth.ts"
const auth = betterAuth({
plugins: [organization({
plugins: [
organization({
schema: {
organization: {
modelName: "organizations", //map the organization table to organizations
@@ -1296,7 +1477,8 @@ const auth = betterAuth({
}
}
}
})]
})
]
})
```

View File

@@ -92,38 +92,97 @@ The passkey plugin implementation is powered by [SimpleWebAuthn](https://simplew
To add or register a passkey make sure a user is authenticated and then call the `passkey.addPasskey` function provided by the client.
<APIMethod path="/passkey/add-passkey" method="POST" isClientOnly>
```ts
// Default behavior allows both platform and cross-platform passkeys
const { data, error } = await authClient.passkey.addPasskey();
type addPasskey = {
/**
* You can also specify the type of authenticator you want to register. Default behavior allows both platform and cross-platform passkeys
*/
authenticatorAttachment?: "platform" | "cross-platform" = "cross-platform"
}
```
</APIMethod>
This will prompt the user to register a passkey. And it'll add the passkey to the user's account.
You can also specify the type of authenticator you want to register. The `authenticatorAttachment` can be either `platform` or `cross-platform`.
```ts
// Register a cross-platform passkey showing only a QR code
// for the user to scan as well as the option to plug in a security key
const { data, error } = await authClient.passkey.addPasskey({
authenticatorAttachment: 'cross-platform'
});
```
### Sign in with a passkey
To sign in with a passkey you can use the passkeySignIn method. This will prompt the user to sign in with their passkey.
To sign in with a passkey you can use the `signIn.passkey` method. This will prompt the user to sign in with their passkey.
Signin method accepts:
`autoFill`: Browser autofill, a.k.a. Conditional UI. [read more](https://simplewebauthn.dev/docs/packages/browser#browser-autofill-aka-conditional-ui)
`email`: The email of the user to sign in.
`fetchOptions`: Fetch options to pass to the fetch request.
<APIMethod path="/sign-in/passkey" method="POST" isClientOnly>
```ts
const data = await authClient.signIn.passkey();
type signInPasskey = {
/**
* The email of the user to sign in.
*/
email: string = "example@gmail.com"
/**
* Browser autofill, a.k.a. Conditional UI. Read more: https://simplewebauthn.dev/docs/packages/browser#browser-autofill-aka-conditional-ui
*/
autoFill?: boolean = true
/**
* The URL to redirect to after the user has signed in.
*/
callbackURL?: string = "/dashboard"
}
```
</APIMethod>
### List passkeys
You can list all of the passkeys for the authenticated user by calling `passkey.listUserPasskeys`:
<APIMethod
path="/passkey/list-user-passkeys"
method="GET"
requireSession
resultVariable="passkeys"
>
```ts
type listPasskeys = {
}
```
</APIMethod>
### Deleting passkeys
You can delete a passkey by calling `passkey.delete` and providing the passkey ID.
<APIMethod
path="/passkey/delete-passkey"
method="POST"
requireSession
>
```ts
type deletePasskey = {
/**
* The ID of the passkey to delete.
*/
id: string = "some-passkey-id"
}
```
</APIMethod>
### Updating passkey names
<APIMethod
path="/passkey/update-passkey"
method="POST"
requireSession
>
```ts
type updatePasskey = {
/**
* The ID of the passkey which you want to update.
*/
id: string = "id of passkey"
/**
* The new name which the passkey will be updated to.
*/
name: string = "my-new-passkey-name"
}
```
</APIMethod>
### Conditional UI

View File

@@ -67,24 +67,47 @@ The phone number plugin extends the authentication system by allowing users to s
To send an OTP to a user's phone number for verification, you can use the `sendVerificationCode` endpoint.
```ts title="auth-client.ts"
await authClient.phoneNumber.sendOtp({
phoneNumber: "+1234567890"
})
<APIMethod path="/phone-number/send-otp" method="POST">
```ts
type sendPhoneNumberOTP = {
/**
* Phone number to send OTP.
*/
phoneNumber: string = "+1234567890"
}
```
</APIMethod>
### Verify Phone Number
After the OTP is sent, users can verify their phone number by providing the code.
```ts title="auth-client.ts"
const isVerified = await authClient.phoneNumber.verify({
phoneNumber: "+1234567890",
code: "123456"
})
<APIMethod path="/phone-number/verify" method="POST">
```ts
type verifyPhoneNumber = {
/**
* Phone number to verify.
*/
phoneNumber: string = "+1234567890"
/**
* OTP code.
*/
code: string = "123456"
/**
* Disable session creation after verification.
*/
disableSession?: boolean = false
/**
* Check if there is a session and update the phone number.
*/
updatePhoneNumber?: boolean = true
}
```
</APIMethod>
<Callout>
When the phone number is verified, the `phoneNumberVerified` field in the user table is set to `true`. If `disableSession` is not set to `true`, a session is created for the user. Additionally, if `callbackOnVerification` is provided, it will be called.
</Callout>
### Allow Sign-Up with Phone Number
@@ -115,19 +138,29 @@ export const auth = betterAuth({
In addition to signing in a user using send-verify flow, you can also use phone number as an identifier and sign in a user using phone number and password.
<APIMethod path="/sign-in/phone-number" method="POST">
```ts
await authClient.signIn.phoneNumber({
phoneNumber: "+123456789",
password: "password",
rememberMe: true //optional defaults to true
})
type signInPhoneNumber = {
/**
* Phone number to sign in.
*/
phoneNumber: string = "+1234567890"
/**
* Password to use for sign in.
*/
password: string
/**
* Remember the session.
*/
rememberMe?: boolean = true
}
```
</APIMethod>
### Update Phone Number
Updating phone number uses the same process as verifying a phone number. The user will receive an OTP code to verify the new phone number.
```ts title="auth-client.ts"
await authClient.phoneNumber.sendOtp({
phoneNumber: "+1234567890" // New phone number
@@ -140,7 +173,7 @@ Then verify the new phone number with the OTP code.
const isVerified = await authClient.phoneNumber.verify({
phoneNumber: "+1234567890",
code: "123456",
updatePhoneNumber: true // Set to true to update the phone number
updatePhoneNumber: true // Set to true to update the phone number [!code highlight]
})
```
@@ -155,7 +188,7 @@ By default, the plugin creates a session for the user after verifying the phone
const isVerified = await authClient.phoneNumber.verify({
phoneNumber: "+1234567890",
code: "123456",
disableSession: true
disableSession: true // [!code highlight]
})
```
@@ -163,21 +196,37 @@ const isVerified = await authClient.phoneNumber.verify({
To initiate a request password reset flow using `phoneNumber`, you can start by calling `requestPasswordReset` on the client to send an OTP code to the user's phone number.
```ts title="auth-client.ts"
await authClient.phoneNumber.requestPasswordReset({
phoneNumber: "+1234567890"
})
<APIMethod path="/phone-number/request-password-reset" method="POST">
```ts
type requestPasswordResetPhoneNumber = {
/**
* The phone number which is associated with the user.
*/
phoneNumber: string = "+1234567890"
}
```
</APIMethod>
Then, you can reset the password by calling `resetPassword` on the client with the OTP code and the new password.
```ts title="auth-client.ts"
const isVerified = await authClient.phoneNumber.resetPassword({
otp: "123456", // OTP code sent to the user's phone number
phoneNumber: "+1234567890",
newPassword: "newPassword"
})
<APIMethod path="/phone-number/reset-password" method="POST">
```ts
type resetPasswordPhoneNumber = {
/**
* The one time password to reset the password.
*/
otp: string = "123456"
/**
* The phone number to the account which intends to reset the password for.
*/
phoneNumber: string = "+1234567890"
/**
* The new password.
*/
newPassword: string = "new-and-secure-password"
}
```
</APIMethod>
## Options

View File

@@ -74,6 +74,8 @@ To register an OIDC provider, use the `registerSSOProvider` endpoint and provide
A redirect URL will be automatically generated using the provider ID. For instance, if the provider ID is `hydra`, the redirect URL would be `{baseURL}/api/auth/sso/callback/hydra`. Note that `/api/auth` may vary depending on your base path configuration.
#### Example
<Tabs items={["client", "server"]}>
<Tab value="client">
```ts title="register-oidc-provider.ts"
@@ -137,6 +139,7 @@ await auth.api.registerSSOProvider({
</Tab>
</Tabs>
### Register a SAML Provider
To register a SAML provider, use the `registerSSOProvider` endpoint with SAML configuration details. The provider will act as a Service Provider (SP) and integrate with your Identity Provider (IdP).
@@ -312,6 +315,51 @@ const res = await auth.api.signInSSO({
});
```
#### Full method
<APIMethod path="/sign-in/sso" method="POST">
```ts
type signInSSO = {
/**
* The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided.
*/
email?: string = "john@example.com"
/**
* The slug of the organization to sign in with.
*/
organizationSlug?: string = "example-org"
/**
* The ID of the provider to sign in with. This can be provided instead of email or issuer.
*/
providerId?: string = "example-provider"
/**
* The domain of the provider.
*/
domain?: string = "example.com"
/**
* The URL to redirect to after login.
*/
callbackURL: string = "https://example.com/callback"
/**
* The URL to redirect to after login.
*/
errorCallbackURL?: string = "https://example.com/callback"
/**
* The URL to redirect to after login if the user is new.
*/
newUserCallbackURL?: string = "https://example.com/new-user"
/**
* Scopes to request from the provider.
*/
scopes?: string[] = ["openid", "email", "profile", "offline_access"]
/**
* Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider.
*/
requestSignUp?: boolean = true
}
```
</APIMethod>
When a user is authenticated, if the user does not exist, the user will be provisioned using the `provisionUser` function. If the organization provisioning is enabled and a provider is associated with an organization, the user will be added to the organization.
```ts title="auth.ts"

View File

@@ -205,6 +205,55 @@ see [plan configuration](#plan-configuration) for more.
To create a subscription, use the `subscription.upgrade` method:
<APIMethod
path="/subscription/upgrade"
method="POST"
requireSession
>
```ts
type upgradeSubscription = {
/**
* The name of the plan to upgrade to.
*/
plan: string = "pro"
/**
* Whether to upgrade to an annual plan.
*/
annual?: boolean = true
/**
* Reference id of the subscription to upgrade.
*/
referenceId?: string = "123"
/**
* The id of the subscription to upgrade.
*/
subscriptionId?: string = "sub_123"
metadata?: Record<string, any>
/**
* Number of seats to upgrade to (if applicable).
*/
seats?: number = 1
/**
* Callback URL to redirect back after successful subscription.
*/
successUrl: string
/**
* Callback URL to redirect back after successful subscription.
*/
cancelUrl: string
* Return URL to redirect back after successful subscription.
*/
returnUrl?: string
/**
* Disable redirect after successful subscription.
*/
disableRedirect: boolean = true
}
```
</APIMethod>
**Simple Example:**
```ts title="client.ts"
await client.subscription.upgrade({
plan: "pro",
@@ -256,8 +305,19 @@ This ensures that the user only pays for the new plan, and not both.
To get the user's active subscriptions:
```ts title="client.ts"
const { data: subscriptions } = await client.subscription.list();
<APIMethod
path="/subscription/list"
method="GET"
requireSession
resultVariable="subscriptions"
>
```ts
type listActiveSubscriptions = {
/**
* Reference id of the subscription to list.
*/
referenceId?: string = '123'
}
// get the active subscription
const activeSubscription = subscriptions.find(
@@ -267,17 +327,34 @@ const activeSubscription = subscriptions.find(
// Check subscription limits
const projectLimit = subscriptions?.limits?.projects || 0;
```
</APIMethod>
#### Canceling a Subscription
To cancel a subscription:
```ts title="client.ts"
const { data } = await client.subscription.cancel({
returnUrl: "/account",
referenceId: "org_123" // optional defaults to userId
});
<APIMethod
path="/subscription/cancel"
method="POST"
requireSession
>
```ts
type cancelSubscription = {
/**
* Reference id of the subscription to cancel. Defaults to the userId.
*/
referenceId?: string = 'org_123'
/**
* The id of the subscription to cancel.
*/
subscriptionId?: string = 'sub_123'
/**
* Return URL to redirect back after successful subscription.
*/
returnUrl: string = '/account'
}
```
</APIMethod>
This will redirect the user to the Stripe Billing Portal where they can cancel their subscription.
@@ -285,11 +362,26 @@ This will redirect the user to the Stripe Billing Portal where they can cancel t
If a user changes their mind after canceling a subscription (but before the subscription period ends), you can restore the subscription:
```ts title="client.ts"
const { data } = await client.subscription.restore({
referenceId: "org_123" // optional, defaults to userId
});
<APIMethod
path="/subscription/restore"
method="POST"
requireSession
>
```ts
type restoreSubscription = {
/**
* Reference id of the subscription to restore. Defaults to the userId.
*/
referenceId?: string = '123'
/**
* The id of the subscription to restore.
*/
subscriptionId?: string = 'sub_123'
}
```
</APIMethod>
This will reactivate a subscription that was previously set to cancel at the end of the billing period (`cancelAtPeriodEnd: true`). The subscription will continue to renew automatically.

View File

@@ -59,7 +59,7 @@ The username plugin wraps the email and password authenticator and adds username
## Usage
### Sign up with username
### Sign up
To sign up a user with username, you can use the existing `signUp.email` function provided by the client. The `signUp` function should take a new `username` property in the object.
@@ -72,7 +72,7 @@ const data = await authClient.signUp.email({
})
```
### Sign in with username
### Sign in
To sign in a user with username, you can use the `signIn.username` function provided by the client. The `signIn` function takes an object with the following properties:
@@ -136,7 +136,7 @@ The plugin requires 2 fields to be added to the user table:
## Options
### Min Username Length
**Min Username Length**
The minimum length of the username. Default is `3`.
@@ -153,7 +153,7 @@ const auth = betterAuth({
})
```
### Max Username Length
**Max Username Length**
The maximum length of the username. Default is `30`.
@@ -170,7 +170,7 @@ const auth = betterAuth({
})
```
### Username Validator
**Username Validator**
A function that validates the username. The function should return false if the username is invalid. By default, the username should only contain alphanumeric characters, underscores, and dots.

View File

@@ -6,7 +6,8 @@
"build": "next build",
"dev": "next dev",
"start": "next start",
"postinstall": "fumadocs-mdx"
"postinstall": "fumadocs-mdx",
"scripts:endpoint-to-doc": "bun ./scripts/endpoint-to-doc/index.ts"
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",

View File

@@ -0,0 +1,491 @@
import type { createAuthEndpoint as BAcreateAuthEndpoint } from "better-auth/api";
import { z } from "zod";
import fs from "fs";
import path from "path";
playSound("Hero");
let isUsingSessionMiddleware = false;
export const {
orgMiddleware,
orgSessionMiddleware,
requestOnlySessionMiddleware,
sessionMiddleware,
originCheck,
adminMiddleware,
referenceMiddleware,
} = {
orgMiddleware: () => {},
referenceMiddleware: (cb: (x: any) => void) => () => {},
orgSessionMiddleware: () => {},
requestOnlySessionMiddleware: () => {},
sessionMiddleware: () => {
isUsingSessionMiddleware = true;
},
originCheck: (cb: (x: any) => void) => () => {},
adminMiddleware: () => {
isUsingSessionMiddleware = true;
},
};
const file = path.join(process.cwd(), "./scripts/endpoint-to-doc/input.ts");
function clearImportCache() {
const resolved = new URL(file, import.meta.url).pathname;
delete (globalThis as any).__dynamicImportCache?.[resolved];
delete require.cache[require.resolve(resolved)];
}
console.log(`Watching: ${file}`);
fs.watch(file, async () => {
isUsingSessionMiddleware = false;
playSound();
console.log(`Detected file change. Regenerating mdx.`);
const inputCode = fs.readFileSync(file, "utf-8");
if (inputCode.includes(".coerce"))
fs.writeFileSync(file, inputCode.replaceAll(".coerce", ""), "utf-8");
await generateMDX();
playSound("Hero");
});
async function generateMDX() {
const exports = await import("./input");
clearImportCache();
if (Object.keys(exports).length !== 1)
return console.error(`Please provide at least 1 export.`);
const start = Date.now();
const functionName = Object.keys(exports)[0]! as string;
const [path, options]: [string, Options] =
//@ts-ignore
await exports[Object.keys(exports)[0]!];
if (!path || !options) return console.error(`No path or options.`);
if (options.use) {
options.use.forEach((fn) => fn());
}
console.log(`function name:`, functionName);
let jsdoc = generateJSDoc({
path,
functionName,
options,
isServerOnly: options.metadata?.SERVER_ONLY ?? false,
});
let mdx = `<APIMethod${parseParams(path, options)}>\n\`\`\`ts\n${parseType(
functionName,
options,
)}\n\`\`\`\n</APIMethod>`;
console.log(`Generated in ${(Date.now() - start).toFixed(2)}ms!`);
fs.writeFileSync(
"./scripts/endpoint-to-doc/output.mdx",
`${APIMethodsHeader}\n\n${mdx}\n\n${JSDocHeader}\n\n${jsdoc}`,
"utf-8",
);
console.log(`Successfully updated \`output.mdx\`!`);
}
type CreateAuthEndpointProps = Parameters<typeof BAcreateAuthEndpoint>;
type Options = CreateAuthEndpointProps[1];
const APIMethodsHeader = `{/* -------------------------------------------------------- */}
{/* APIMethod component */}
{/* -------------------------------------------------------- */}`;
const JSDocHeader = `{/* -------------------------------------------------------- */}
{/* JSDOC For the endpoint */}
{/* -------------------------------------------------------- */}`;
export const createAuthEndpoint = async (
...params: Partial<CreateAuthEndpointProps>
) => {
const [path, options] = params;
if (!path || !options) return console.error(`No path or options.`);
return [path, options];
};
type Body = {
propName: string;
type: string[];
isOptional: boolean;
isServerOnly: boolean;
jsDocComment: string | null;
path: string[];
example: string | undefined;
};
function parseType(functionName: string, options: Options) {
const body: z.ZodAny = (options.query ?? options.body) as any;
const parsedBody: Body[] = parseZodShape(body, []);
// console.log(parsedBody);
let strBody: string = convertBodyToString(parsedBody);
return `type ${functionName} = {\n${strBody}}`;
}
function convertBodyToString(parsedBody: Body[]) {
let strBody: string = ``;
const indentationSpaces = ` `;
let i = -1;
for (const body of parsedBody) {
i++;
if (body.jsDocComment || body.isServerOnly) {
strBody += `${indentationSpaces.repeat(
1 + body.path.length,
)}/**\n${indentationSpaces.repeat(1 + body.path.length)} * ${
body.jsDocComment
} ${
body.isServerOnly
? `\n${indentationSpaces.repeat(1 + body.path.length)} * @serverOnly`
: ""
}\n${indentationSpaces.repeat(1 + body.path.length)} */\n`;
}
if (body.type[0] === "Object") {
strBody += `${indentationSpaces.repeat(1 + body.path.length)}${
body.propName
}${body.isOptional ? "?" : ""}: {\n`;
} else {
strBody += `${indentationSpaces.repeat(1 + body.path.length)}${
body.propName
}${body.isOptional ? "?" : ""}: ${body.type.join(" | ")}${
typeof body.example !== "undefined" ? ` = ${body.example}` : ""
}\n`;
}
if (
!parsedBody[i + 1] ||
parsedBody[i + 1].path.length < body.path.length
) {
let diff = body.path.length - (parsedBody[i + 1]?.path?.length || 0);
for (const index of Array(diff)
.fill(0)
.map((_, i) => i)
.reverse()) {
strBody += `${indentationSpaces.repeat(index + 1)}}\n`;
}
}
}
return strBody;
}
function parseZodShape(zod: z.ZodAny, path: string[]) {
const parsedBody: Body[] = [];
if (!zod || !zod._def) {
return parsedBody;
}
let isRootOptional = undefined;
let shape = z.object(
{ test: z.string({ description: "" }) },
{ description: "some descriptiom" },
).shape;
//@ts-ignore
if (zod._def.typeName === "ZodOptional") {
isRootOptional = true;
const eg = z.optional(z.object({}));
const x = zod as never as typeof eg;
//@ts-ignore
shape = x._def.innerType.shape;
} else {
const eg = z.object({});
const x = zod as never as typeof eg;
//@ts-ignore
shape = x.shape;
}
for (const [key, value] of Object.entries(shape)) {
if (!value) continue;
let description = value.description;
let { type, isOptional, defaultValue } = getType(value as any, {
forceOptional: isRootOptional,
});
let example = description ? description.split(" Eg: ")[1] : undefined;
if (example) description = description?.replace(" Eg: " + example, "");
let isServerOnly = description
? description.includes("server-only.")
: false;
if (isServerOnly) description = description?.replace(" server-only. ", "");
if (!description?.trim().length) description = undefined;
parsedBody.push({
propName: key,
isOptional: isOptional,
jsDocComment: description ?? null,
path,
isServerOnly,
type,
example: example ?? defaultValue ?? undefined,
});
if (type[0] === "Object") {
const v = value as never as z.ZodAny;
parsedBody.push(...parseZodShape(v, [...path, key]));
}
}
return parsedBody;
}
function getType(
value: z.ZodAny,
{
forceNullable,
forceOptional,
forceDefaultValue,
}: {
forceOptional?: boolean;
forceNullable?: boolean;
forceDefaultValue?: string;
} = {},
): { type: string[]; isOptional: boolean; defaultValue?: string } {
if (!value._def) {
console.error(
`Something went wrong during "getType". value._def isn't defined.`,
);
console.error(`value:`);
console.log(value);
process.exit(1);
}
const _null: string[] = value?.isNullable() ? ["null"] : [];
switch (value._def.typeName as string) {
case "ZodString": {
return {
type: ["string", ..._null],
isOptional: forceOptional ?? value.isOptional(),
defaultValue: forceDefaultValue,
};
}
case "ZodObject": {
return {
type: ["Object", ..._null],
isOptional: forceOptional ?? value.isOptional(),
defaultValue: forceDefaultValue,
};
}
case "ZodBoolean": {
return {
type: ["boolean", ..._null],
isOptional: forceOptional ?? value.isOptional(),
defaultValue: forceDefaultValue,
};
}
case "ZodDate": {
return {
type: ["date", ..._null],
isOptional: forceOptional ?? value.isOptional(),
defaultValue: forceDefaultValue,
};
}
case "ZodEnum": {
const v = value as never as z.ZodEnum<["hello", "world"]>;
const types: string[] = [];
for (const value of v._def.values) {
types.push(JSON.stringify(value));
}
return {
type: types,
isOptional: forceOptional ?? v.isOptional(),
defaultValue: forceDefaultValue,
};
}
case "ZodOptional": {
const v = value as never as z.ZodOptional<z.ZodAny>;
const r = getType(v._def.innerType, {
forceOptional: true,
forceNullable: forceNullable,
});
return {
type: r.type,
isOptional: forceOptional ?? r.isOptional,
defaultValue: forceDefaultValue,
};
}
case "ZodDefault": {
const v = value as never as z.ZodDefault<z.ZodAny>;
const r = getType(v._def.innerType, {
forceOptional: forceOptional,
forceDefaultValue: JSON.stringify(v._def.defaultValue()),
forceNullable: forceNullable,
});
return {
type: r.type,
isOptional: forceOptional ?? r.isOptional,
defaultValue: forceDefaultValue ?? r.defaultValue,
};
}
case "ZodAny": {
return {
type: ["any", ..._null],
isOptional: forceOptional ?? value.isOptional(),
defaultValue: forceDefaultValue,
};
}
case "ZodRecord": {
const v = value as never as z.ZodRecord;
const keys: string[] = getType(v._def.keyType as any).type;
const values: string[] = getType(v._def.valueType as any).type;
return {
type: keys.map((key, i) => `Record<${key}, ${values[i]}>`),
isOptional: forceOptional ?? v.isOptional(),
defaultValue: forceDefaultValue,
};
}
case "ZodNumber": {
return {
type: ["number", ..._null],
isOptional: forceOptional ?? value.isOptional(),
defaultValue: forceDefaultValue,
};
}
case "ZodUnion": {
const v = value as never as z.ZodUnion<[z.ZodAny]>;
const types: string[] = [];
for (const option of v.options) {
const t = getType(option as any).type;
types.push(t.length === 0 ? t[0] : `${t.join(" | ")}`);
}
return {
type: types,
isOptional: forceOptional ?? v.isOptional(),
defaultValue: forceDefaultValue,
};
}
case "ZodNullable": {
const v = value as never as z.ZodNullable<z.ZodAny>;
const r = getType(v._def.innerType, { forceOptional: true });
return {
type: r.type,
isOptional: forceOptional ?? r.isOptional,
defaultValue: forceDefaultValue,
};
}
case "ZodArray": {
const v = value as never as z.ZodArray<z.ZodAny>;
const types = getType(v._def.type as any);
return {
type: [
`${
types.type.length === 1
? types.type[0]
: `(${types.type.join(" | ")})`
}[]`,
..._null,
],
isOptional: forceOptional ?? v.isOptional(),
defaultValue: forceDefaultValue,
};
}
default: {
console.error(`Unknown Zod type: ${value._def.typeName}`);
console.log(value._def);
process.exit(1);
}
}
}
function parseParams(path: string, options: Options): string {
let params: string[] = [];
params.push(`path="${path}"`);
params.push(`method="${options.method}"`);
if (options.requireHeaders || isUsingSessionMiddleware)
params.push("requireSession");
if (options.metadata?.SERVER_ONLY) params.push("isServerOnly");
if (options.method === "GET" && options.body) params.push("forceAsBody");
if (options.method === "POST" && options.query) params.push("forceAsQuery");
if (params.length === 2) return " " + params.join(" ");
return "\n " + params.join("\n ") + "\n";
}
function generateJSDoc({
path,
options,
functionName,
isServerOnly,
}: {
path: string;
options: Options;
functionName: string;
isServerOnly: boolean;
}) {
/**
* ### Endpoint
*
* POST `/organization/set-active`
*
* ### API Methods
*
* **server:**
* `auth.api.setActiveOrganization`
*
* **client:**
* `authClient.organization.setActive`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-set-active)
*/
let jsdoc: string[] = [];
jsdoc.push(`### Endpoint`);
jsdoc.push(``);
jsdoc.push(`${options.method} \`${path}\``);
jsdoc.push(``);
jsdoc.push(`### API Methods`);
jsdoc.push(``);
jsdoc.push(`**server:**`);
jsdoc.push(`\`auth.api.${functionName}\``);
jsdoc.push(``);
if (!isServerOnly) {
jsdoc.push(`**client:**`);
jsdoc.push(`\`authClient.${pathToDotNotation(path)}\``);
jsdoc.push(``);
}
jsdoc.push(
`@see [Read our docs to learn more.](https://better-auth.com/docs/plugins/${
path.split("/")[1]
}#api-method${path.replaceAll("/", "-")})`,
);
return `/**\n * ${jsdoc.join("\n * ")}\n */`;
}
function pathToDotNotation(input: string): string {
return input
.split("/") // split into segments
.filter(Boolean) // remove empty strings (from leading '/')
.map((segment) =>
segment
.split("-") // split kebab-case
.map((word, i) =>
i === 0
? word.toLowerCase()
: word.charAt(0).toUpperCase() + word.slice(1),
)
.join(""),
)
.join(".");
}
async function playSound(name: string = "Ping") {
const path = `/System/Library/Sounds/${name}.aiff`;
await Bun.$`afplay ${path}`;
}

View File

@@ -0,0 +1,112 @@
//@ts-nocheck
import {
createAuthEndpoint,
sessionMiddleware,
referenceMiddleware,
} from "./index";
import { z } from "zod";
export const restoreSubscription = createAuthEndpoint(
"/subscription/restore",
{
method: "POST",
body: z.object({
referenceId: z
.string({
description: "Reference id of the subscription to restore. Eg: '123'",
})
.optional(),
subscriptionId: z.string({
description: "The id of the subscription to restore. Eg: 'sub_123'",
}),
}),
use: [sessionMiddleware, referenceMiddleware("restore-subscription")],
},
async (ctx) => {
const referenceId = ctx.body?.referenceId || ctx.context.session.user.id;
const subscription = ctx.body.subscriptionId
? await ctx.context.adapter.findOne<Subscription>({
model: "subscription",
where: [
{
field: "id",
value: ctx.body.subscriptionId,
},
],
})
: await ctx.context.adapter
.findMany<Subscription>({
model: "subscription",
where: [
{
field: "referenceId",
value: referenceId,
},
],
})
.then((subs) =>
subs.find(
(sub) => sub.status === "active" || sub.status === "trialing",
),
);
if (!subscription || !subscription.stripeCustomerId) {
throw ctx.error("BAD_REQUEST", {
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
});
}
if (subscription.status != "active" && subscription.status != "trialing") {
throw ctx.error("BAD_REQUEST", {
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE,
});
}
if (!subscription.cancelAtPeriodEnd) {
throw ctx.error("BAD_REQUEST", {
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION,
});
}
const activeSubscription = await client.subscriptions
.list({
customer: subscription.stripeCustomerId,
})
.then(
(res) =>
res.data.filter(
(sub) => sub.status === "active" || sub.status === "trialing",
)[0],
);
if (!activeSubscription) {
throw ctx.error("BAD_REQUEST", {
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
});
}
try {
const newSub = await client.subscriptions.update(activeSubscription.id, {
cancel_at_period_end: false,
});
await ctx.context.adapter.update({
model: "subscription",
update: {
cancelAtPeriodEnd: false,
updatedAt: new Date(),
},
where: [
{
field: "id",
value: subscription.id,
},
],
});
return ctx.json(newSub);
} catch (error) {
ctx.context.logger.error("Error restoring subscription", error);
throw new APIError("BAD_REQUEST", {
message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER,
});
}
},
);

View File

@@ -0,0 +1,42 @@
{/* -------------------------------------------------------- */}
{/* APIMethod component */}
{/* -------------------------------------------------------- */}
<APIMethod
path="/subscription/restore"
method="POST"
requireSession
>
```ts
type restoreSubscription = {
/**
* Reference id of the subscription to restore.
*/
referenceId?: string = '123'
/**
* The id of the subscription to restore.
*/
subscriptionId: string = 'sub_123'
}
```
</APIMethod>
{/* -------------------------------------------------------- */}
{/* JSDOC For the endpoint */}
{/* -------------------------------------------------------- */}
/**
* ### Endpoint
*
* POST `/subscription/restore`
*
* ### API Methods
*
* **server:**
* `auth.api.restoreSubscription`
*
* **client:**
* `authClient.subscription.restore`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/subscription#api-method-subscription-restore)
*/

View File

@@ -0,0 +1,29 @@
# Endpoint To Documentation
This script allows you to copy the code of what you would normally pass into `createAuthEndpoint`, and it will automatically convert it into a `APIMethod` component which you can use in the Better-Auth documentation
to easily document the details of a given endpoint.
This script will also generate JSDoc which you can then place above each endpoint code.
## Requirements
This does however require Bun since we're running typescript code without transpiling to JS before executing.
## How to run
Head into the `docs/scripts/endpoint-to-doc/input.ts` file,
and copy over the desired `createAuthEndpoint` properties.
Note: The file has `//@ts-nocheck` at the start of the file, so that we can ignore type errors that may be within the handler param.
Since we don't run the handler, we can safely ignore those types.
However, it's possible that the options param may be using a middleware indicated by the `use` prop, and likely using a variable undefined in this context. So remember to remove any `use` props in the options.
Then, make sure you're in the `docs` directory within your terminal.
and run:
```bash
bun scripts:endpoint-to-doc
```
This will read and execute that `input.ts` file which you have recently edited. It may prompt you to answer a few questions, and after it will output a `output.mdx` file which you can then copy it's contents to the Better-Auth docs.

View File

@@ -286,6 +286,9 @@ export const getSessionFromCtx = async <
} | null;
};
/**
* The middleware forces the endpoint to require a valid session.
*/
export const sessionMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session?.session) {
@@ -296,6 +299,10 @@ export const sessionMiddleware = createAuthMiddleware(async (ctx) => {
};
});
/**
* This middleware allows you to call the endpoint on the client if session is valid.
* However, if called on the server, no session is required.
*/
export const requestOnlySessionMiddleware = createAuthMiddleware(
async (ctx) => {
const session = await getSessionFromCtx(ctx);
@@ -306,6 +313,13 @@ export const requestOnlySessionMiddleware = createAuthMiddleware(
},
);
/**
* This middleware forces the endpoint to require a valid session,
* as well as making sure the session is fresh before proceeding.
*
* Session freshness check will be skipped if the session config's freshAge
* is set to 0
*/
export const freshSessionMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session?.session) {

View File

@@ -60,6 +60,10 @@ export const admin = <O extends AdminOptions>(options?: O) => {
permission?: never;
};
/**
* Ensures a valid session, if not will throw.
* Will also provide additional types on the user to include role types.
*/
const adminMiddleware = createAuthMiddleware(async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session) {
@@ -167,6 +171,21 @@ export const admin = <O extends AdminOptions>(options?: O) => {
],
},
endpoints: {
/**
* ### Endpoint
*
* POST `/admin/set-role`
*
* ### API Methods
*
* **server:**
* `auth.api.setRole`
*
* **client:**
* `authClient.admin.setRole`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-set-role)
*/
setRole: createAuthEndpoint(
"/admin/set-role",
{
@@ -175,17 +194,24 @@ export const admin = <O extends AdminOptions>(options?: O) => {
userId: z.coerce.string().meta({
description: "The user id",
}),
role: z.union([
role: z
.union([
z.string().meta({
description: "The role to set. `admin` or `user` by default",
}),
z.array(
z.string().meta({
description: "The roles to set. `admin` or `user` by default",
description:
"The roles to set. `admin` or `user` by default",
}),
),
]),
])
.meta({
description:
"The role to set, this can be a string or an array of strings. Eg: `admin` or `[admin, user]`",
}),
}),
requireHeaders: true,
use: [adminMiddleware],
metadata: {
openapi: {
@@ -248,6 +274,21 @@ export const admin = <O extends AdminOptions>(options?: O) => {
});
},
),
/**
* ### Endpoint
*
* POST `/admin/create-user`
*
* ### API Methods
*
* **server:**
* `auth.api.createUser`
*
* **client:**
* `authClient.admin.createUser`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-create-user)
*/
createUser: createAuthEndpoint(
"/admin/create-user",
{
@@ -273,16 +314,17 @@ export const admin = <O extends AdminOptions>(options?: O) => {
}),
),
])
.optional(),
.optional()
.meta({
description: `A string or array of strings representing the roles to apply to the new user. Eg: \"user\"`,
}),
/**
* extra fields for user
*/
data: z.optional(
z.record(z.any(), z.any()).meta({
data: z.record(z.string(), z.any()).optional().meta({
description:
"Extra fields for the user. Including custom additional fields.",
}),
),
}),
metadata: {
openapi: {
@@ -384,30 +426,42 @@ export const admin = <O extends AdminOptions>(options?: O) => {
});
},
),
/**
* ### Endpoint
*
* GET `/admin/list-users`
*
* ### API Methods
*
* **server:**
* `auth.api.listUsers`
*
* **client:**
* `authClient.admin.listUsers`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-list-users)
*/
listUsers: createAuthEndpoint(
"/admin/list-users",
{
method: "GET",
use: [adminMiddleware],
query: z.object({
searchValue: z
.string()
.meta({
description: "The value to search for",
})
.optional(),
searchValue: z.string().optional().meta({
description: 'The value to search for. Eg: "some name"',
}),
searchField: z
.enum(["email", "name"])
.meta({
description:
"The field to search in, defaults to email. Can be `email` or `name`",
'The field to search in, defaults to email. Can be `email` or `name`. Eg: "name"',
})
.optional(),
searchOperator: z
.enum(["contains", "starts_with", "ends_with"])
.meta({
description:
"The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`",
'The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`. Eg: "contains"',
})
.optional(),
limit: z
@@ -558,6 +612,21 @@ export const admin = <O extends AdminOptions>(options?: O) => {
}
},
),
/**
* ### Endpoint
*
* POST `/admin/list-user-sessions`
*
* ### API Methods
*
* **server:**
* `auth.api.listUserSessions`
*
* **client:**
* `authClient.admin.listUserSessions`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-list-user-sessions)
*/
listUserSessions: createAuthEndpoint(
"/admin/list-user-sessions",
{
@@ -621,6 +690,21 @@ export const admin = <O extends AdminOptions>(options?: O) => {
};
},
),
/**
* ### Endpoint
*
* POST `/admin/unban-user`
*
* ### API Methods
*
* **server:**
* `auth.api.unbanUser`
*
* **client:**
* `authClient.admin.unbanUser`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-unban-user)
*/
unbanUser: createAuthEndpoint(
"/admin/unban-user",
{
@@ -686,6 +770,21 @@ export const admin = <O extends AdminOptions>(options?: O) => {
});
},
),
/**
* ### Endpoint
*
* POST `/admin/ban-user`
*
* ### API Methods
*
* **server:**
* `auth.api.banUser`
*
* **client:**
* `authClient.admin.banUser`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-ban-user)
*/
banUser: createAuthEndpoint(
"/admin/ban-user",
{
@@ -782,6 +881,21 @@ export const admin = <O extends AdminOptions>(options?: O) => {
});
},
),
/**
* ### Endpoint
*
* POST `/admin/impersonate-user`
*
* ### API Methods
*
* **server:**
* `auth.api.impersonateUser`
*
* **client:**
* `authClient.admin.impersonateUser`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-impersonate-user)
*/
impersonateUser: createAuthEndpoint(
"/admin/impersonate-user",
{
@@ -892,10 +1006,26 @@ export const admin = <O extends AdminOptions>(options?: O) => {
});
},
),
/**
* ### Endpoint
*
* POST `/admin/stop-impersonating`
*
* ### API Methods
*
* **server:**
* `auth.api.stopImpersonating`
*
* **client:**
* `authClient.admin.stopImpersonating`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-stop-impersonating)
*/
stopImpersonating: createAuthEndpoint(
"/admin/stop-impersonating",
{
method: "POST",
requireHeaders: true,
},
async (ctx) => {
const session = await getSessionFromCtx<
@@ -948,6 +1078,21 @@ export const admin = <O extends AdminOptions>(options?: O) => {
return ctx.json(adminSession);
},
),
/**
* ### Endpoint
*
* POST `/admin/revoke-user-session`
*
* ### API Methods
*
* **server:**
* `auth.api.revokeUserSession`
*
* **client:**
* `authClient.admin.revokeUserSession`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-revoke-user-session)
*/
revokeUserSession: createAuthEndpoint(
"/admin/revoke-user-session",
{
@@ -1008,6 +1153,21 @@ export const admin = <O extends AdminOptions>(options?: O) => {
});
},
),
/**
* ### Endpoint
*
* POST `/admin/revoke-user-sessions`
*
* ### API Methods
*
* **server:**
* `auth.api.revokeUserSessions`
*
* **client:**
* `authClient.admin.revokeUserSessions`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-revoke-user-sessions)
*/
revokeUserSessions: createAuthEndpoint(
"/admin/revoke-user-sessions",
{
@@ -1066,6 +1226,21 @@ export const admin = <O extends AdminOptions>(options?: O) => {
});
},
),
/**
* ### Endpoint
*
* POST `/admin/remove-user`
*
* ### API Methods
*
* **server:**
* `auth.api.removeUser`
*
* **client:**
* `authClient.admin.removeUser`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-remove-user)
*/
removeUser: createAuthEndpoint(
"/admin/remove-user",
{
@@ -1133,6 +1308,21 @@ export const admin = <O extends AdminOptions>(options?: O) => {
});
},
),
/**
* ### Endpoint
*
* POST `/admin/set-user-password`
*
* ### API Methods
*
* **server:**
* `auth.api.setUserPassword`
*
* **client:**
* `authClient.admin.setUserPassword`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-set-user-password)
*/
setUserPassword: createAuthEndpoint(
"/admin/set-user-password",
{
@@ -1198,14 +1388,33 @@ export const admin = <O extends AdminOptions>(options?: O) => {
});
},
),
/**
* ### Endpoint
*
* POST `/admin/has-permission`
*
* ### API Methods
*
* **server:**
* `auth.api.userHasPermission`
*
* **client:**
* `authClient.admin.hasPermission`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-has-permission)
*/
userHasPermission: createAuthEndpoint(
"/admin/has-permission",
{
method: "POST",
body: z
.object({
userId: z.coerce.string().optional(),
role: z.string().optional(),
userId: z.coerce.string().optional().meta({
description: `The user id. Eg: "user-id"`,
}),
role: z.string().optional().meta({
description: `The role to check permission for. Eg: "admin"`,
}),
})
.and(
z.union([

View File

@@ -227,12 +227,112 @@ export const apiKey = (options?: ApiKeyOptions) => {
],
},
endpoints: {
/**
* ### Endpoint
*
* POST `/api-key/create`
*
* ### API Methods
*
* **server:**
* `auth.api.createApiKey`
*
* **client:**
* `authClient.apiKey.create`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-create)
*/
createApiKey: routes.createApiKey,
/**
* ### Endpoint
*
* POST `/api-key/verify`
*
* ### API Methods
*
* **server:**
* `auth.api.verifyApiKey`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-verify)
*/
verifyApiKey: routes.verifyApiKey,
/**
* ### Endpoint
*
* GET `/api-key/get`
*
* ### API Methods
*
* **server:**
* `auth.api.getApiKey`
*
* **client:**
* `authClient.apiKey.get`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-get)
*/
getApiKey: routes.getApiKey,
/**
* ### Endpoint
*
* POST `/api-key/update`
*
* ### API Methods
*
* **server:**
* `auth.api.updateApiKey`
*
* **client:**
* `authClient.apiKey.update`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-update)
*/
updateApiKey: routes.updateApiKey,
/**
* ### Endpoint
*
* POST `/api-key/delete`
*
* ### API Methods
*
* **server:**
* `auth.api.deleteApiKey`
*
* **client:**
* `authClient.apiKey.delete`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-delete)
*/
deleteApiKey: routes.deleteApiKey,
/**
* ### Endpoint
*
* GET `/api-key/list`
*
* ### API Methods
*
* **server:**
* `auth.api.listApiKeys`
*
* **client:**
* `authClient.apiKey.list`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-list)
*/
listApiKeys: routes.listApiKeys,
/**
* ### Endpoint
*
* POST `/api-key/delete-all-expired-api-keys`
*
* ### API Methods
*
* **server:**
* `auth.api.deleteAllExpiredApiKeys`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-delete-all-expired-api-keys)
*/
deleteAllExpiredApiKeys: routes.deleteAllExpiredApiKeys,
},
schema,
} satisfies BetterAuthPlugin;

View File

@@ -48,7 +48,7 @@ export function createApiKey({
.string()
.meta({
description:
"User Id of the user that the Api Key belongs to. Useful for server-side only.",
'User Id of the user that the Api Key belongs to. server-only. Eg: "user-id"',
})
.optional(),
prefix: z
@@ -73,7 +73,7 @@ export function createApiKey({
.number()
.meta({
description:
"Amount to refill the remaining count of the Api Key. Server Only Property",
"Amount to refill the remaining count of the Api Key. server-only. Eg: 100",
})
.min(1)
.optional(),
@@ -81,31 +81,36 @@ export function createApiKey({
.number()
.meta({
description:
"Interval to refill the Api Key in milliseconds. Server Only Property.",
"Interval to refill the Api Key in milliseconds. server-only. Eg: 1000",
})
.optional(),
rateLimitTimeWindow: z
.number()
.meta({
description:
"The duration in milliseconds where each request is counted. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. Server Only Property.",
"The duration in milliseconds where each request is counted. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 1000",
})
.optional(),
rateLimitMax: z
.number()
.meta({
description:
"Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. Server Only Property.",
"Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 100",
})
.optional(),
rateLimitEnabled: z
.boolean()
.meta({
description:
"Whether the key has rate limiting enabled. Server Only Property.",
"Whether the key has rate limiting enabled. server-only. Eg: true",
})
.optional(),
permissions: z
.record(z.string(), z.array(z.string()))
.meta({
description: "Permissions of the Api Key.",
})
.optional(),
permissions: z.record(z.string(), z.array(z.string())).optional(),
}),
metadata: {
openapi: {

View File

@@ -15,6 +15,7 @@ export function deleteAllExpiredApiKeysEndpoint({
method: "POST",
metadata: {
SERVER_ONLY: true,
client: false,
},
},
async (ctx) => {

View File

@@ -28,7 +28,13 @@ export function updateApiKey({
keyId: z.string().meta({
description: "The id of the Api Key",
}),
userId: z.coerce.string().optional(),
userId: z.coerce
.string()
.meta({
description:
'The id of the user which the api key belongs to. server-only. Eg: "some-user-id"',
})
.optional(),
name: z
.string()
.meta({
@@ -79,28 +85,30 @@ export function updateApiKey({
.number()
.meta({
description:
"The duration in milliseconds where each request is counted.",
"The duration in milliseconds where each request is counted. server-only. Eg: 1000",
})
.optional(),
rateLimitMax: z
.number()
.meta({
description:
"Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.",
"Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 100",
})
.optional(),
permissions: z
.record(z.string(), z.array(z.string()))
.meta({
description: "Update the permissions on the API Key. server-only.",
})
.optional()
.nullable(),
}),
metadata: {
openapi: {
description: "Retrieve an existing API key by ID",
description: "Update an existing API key by ID",
responses: {
"200": {
description: "API key retrieved successfully",
description: "API key updated successfully",
content: {
"application/json": {
schema: {

View File

@@ -204,7 +204,12 @@ export function verifyApiKey({
key: z.string().meta({
description: "The key to verify",
}),
permissions: z.record(z.string(), z.array(z.string())).optional(),
permissions: z
.record(z.string(), z.array(z.string()))
.meta({
description: "The permissions to verify.",
})
.optional(),
}),
metadata: {
SERVER_ONLY: true,

View File

@@ -152,6 +152,21 @@ export const emailOTP = (options: EmailOTPOptions) => {
return otp === storedOtp;
}
const endpoints = {
/**
* ### Endpoint
*
* POST `/email-otp/send-verification-otp`
*
* ### API Methods
*
* **server:**
* `auth.api.sendVerificationOTP`
*
* **client:**
* `authClient.emailOtp.sendVerificationOtp`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-send-verification-otp)
*/
sendVerificationOTP: createAuthEndpoint(
"/email-otp/send-verification-otp",
{
@@ -261,6 +276,7 @@ export const emailOTP = (options: EmailOTPOptions) => {
},
),
};
return {
id: "email-otp",
init(ctx) {
@@ -339,6 +355,18 @@ export const emailOTP = (options: EmailOTPOptions) => {
return otp;
},
),
/**
* ### Endpoint
*
* GET `/email-otp/get-verification-otp`
*
* ### API Methods
*
* **server:**
* `auth.api.getVerificationOTP`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-get-verification-otp)
*/
getVerificationOTP: createAuthEndpoint(
"/email-otp/get-verification-otp",
{
@@ -421,6 +449,21 @@ export const emailOTP = (options: EmailOTPOptions) => {
});
},
),
/**
* ### Endpoint
*
* POST `/email-otp/verify-email`
*
* ### API Methods
*
* **server:**
* `auth.api.verifyEmailOTP`
*
* **client:**
* `authClient.emailOtp.verifyEmail`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-verify-email)
*/
verifyEmailOTP: createAuthEndpoint(
"/email-otp/verify-email",
{
@@ -581,6 +624,21 @@ export const emailOTP = (options: EmailOTPOptions) => {
});
},
),
/**
* ### Endpoint
*
* POST `/sign-in/email-otp`
*
* ### API Methods
*
* **server:**
* `auth.api.signInEmailOTP`
*
* **client:**
* `authClient.signIn.emailOtp`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-sign-in-email-otp)
*/
signInEmailOTP: createAuthEndpoint(
"/sign-in/email-otp",
{
@@ -735,6 +793,21 @@ export const emailOTP = (options: EmailOTPOptions) => {
});
},
),
/**
* ### Endpoint
*
* POST `/forget-password/email-otp`
*
* ### API Methods
*
* **server:**
* `auth.api.forgetPasswordEmailOTP`
*
* **client:**
* `authClient.forgetPassword.emailOtp`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-forget-password-email-otp)
*/
forgetPasswordEmailOTP: createAuthEndpoint(
"/forget-password/email-otp",
{
@@ -803,6 +876,21 @@ export const emailOTP = (options: EmailOTPOptions) => {
});
},
),
/**
* ### Endpoint
*
* POST `/email-otp/reset-password`
*
* ### API Methods
*
* **server:**
* `auth.api.resetPasswordEmailOTP`
*
* **client:**
* `authClient.emailOtp.resetPassword`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-reset-password)
*/
resetPasswordEmailOTP: createAuthEndpoint(
"/email-otp/reset-password",
{

View File

@@ -333,6 +333,21 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
};
},
endpoints: {
/**
* ### Endpoint
*
* POST `/sign-in/oauth2`
*
* ### API Methods
*
* **server:**
* `auth.api.signInWithOAuth2`
*
* **client:**
* `authClient.signIn.oauth2`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/sign-in#api-method-sign-in-oauth2)
*/
signInWithOAuth2: createAuthEndpoint(
"/sign-in/oauth2",
{
@@ -357,7 +372,7 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
.string()
.meta({
description:
"The URL to redirect to after login if the user is new",
'The URL to redirect to after login if the user is new. Eg: "/welcome"',
})
.optional(),
disableRedirect: z
@@ -377,7 +392,7 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
.boolean()
.meta({
description:
"Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider",
"Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider. Eg: false",
})
.optional(),
}),
@@ -526,6 +541,7 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
.optional(),
}),
metadata: {
client: false,
openapi: {
description: "OAuth2 callback",
responses: {
@@ -753,6 +769,21 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
throw ctx.redirect(toRedirectTo);
},
),
/**
* ### Endpoint
*
* POST `/oauth2/link`
*
* ### API Methods
*
* **server:**
* `auth.api.oAuth2LinkAccount`
*
* **client:**
* `authClient.oauth2.link`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/generic-oauth#api-method-oauth2-link)
*/
oAuth2LinkAccount: createAuthEndpoint(
"/oauth2/link",
{

View File

@@ -84,6 +84,21 @@ export const magicLink = (options: MagicLinkopts) => {
return {
id: "magic-link",
endpoints: {
/**
* ### Endpoint
*
* POST `/sign-in/magic-link`
*
* ### API Methods
*
* **server:**
* `auth.api.signInMagicLink`
*
* **client:**
* `authClient.signIn.magicLink`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/sign-in#api-method-sign-in-magic-link)
*/
signInMagicLink: createAuthEndpoint(
"/sign-in/magic-link",
{
@@ -100,7 +115,7 @@ export const magicLink = (options: MagicLinkopts) => {
.string()
.meta({
description:
"User display name. Only used if the user is registering for the first time.",
'User display name. Only used if the user is registering for the first time. Eg: "my-name"',
})
.optional(),
callbackURL: z
@@ -179,6 +194,21 @@ export const magicLink = (options: MagicLinkopts) => {
});
},
),
/**
* ### Endpoint
*
* GET `/magic-link/verify`
*
* ### API Methods
*
* **server:**
* `auth.api.magicLinkVerify`
*
* **client:**
* `authClient.magicLink.verify`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/magic-link#api-method-magic-link-verify)
*/
magicLinkVerify: createAuthEndpoint(
"/magic-link/verify",
{
@@ -191,7 +221,7 @@ export const magicLink = (options: MagicLinkopts) => {
.string()
.meta({
description:
"URL to redirect after magic link verification, if not provided will return session",
'URL to redirect after magic link verification, if not provided the user will be redirected to the root URL. Eg: "/dashboard"',
})
.optional(),
}),

View File

@@ -37,6 +37,21 @@ export const multiSession = (options?: MultiSessionConfig) => {
return {
id: "multi-session",
endpoints: {
/**
* ### Endpoint
*
* GET `/multi-session/list-device-sessions`
*
* ### API Methods
*
* **server:**
* `auth.api.listDeviceSessions`
*
* **client:**
* `authClient.multiSession.listDeviceSessions`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/multi-session#api-method-multi-session-list-device-sessions)
*/
listDeviceSessions: createAuthEndpoint(
"/multi-session/list-device-sessions",
{
@@ -78,6 +93,21 @@ export const multiSession = (options?: MultiSessionConfig) => {
return ctx.json(uniqueUserSessions);
},
),
/**
* ### Endpoint
*
* POST `/multi-session/set-active`
*
* ### API Methods
*
* **server:**
* `auth.api.setActiveSession`
*
* **client:**
* `authClient.multiSession.setActive`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/multi-session#api-method-multi-session-set-active)
*/
setActiveSession: createAuthEndpoint(
"/multi-session/set-active",
{
@@ -141,6 +171,21 @@ export const multiSession = (options?: MultiSessionConfig) => {
return ctx.json(session);
},
),
/**
* ### Endpoint
*
* POST `/multi-session/revoke`
*
* ### API Methods
*
* **server:**
* `auth.api.revokeDeviceSession`
*
* **client:**
* `authClient.multiSession.revoke`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/multi-session#api-method-multi-session-revoke)
*/
revokeDeviceSession: createAuthEndpoint(
"/multi-session/revoke",
{

View File

@@ -975,14 +975,36 @@ export const oidcProvider = (options: OIDCOptions) => {
});
},
),
/**
* ### Endpoint
*
* POST `/oauth2/register`
*
* ### API Methods
*
* **server:**
* `auth.api.registerOAuthApplication`
*
* **client:**
* `authClient.oauth2.register`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/oidc-provider#api-method-oauth2-register)
*/
registerOAuthApplication: createAuthEndpoint(
"/oauth2/register",
{
method: "POST",
body: z.object({
redirect_uris: z.array(z.string()),
redirect_uris: z.array(z.string()).meta({
description:
'A list of redirect URIs. Eg: ["https://client.example.com/callback"]',
}),
token_endpoint_auth_method: z
.enum(["none", "client_secret_basic", "client_secret_post"])
.meta({
description:
'The authentication method for the token endpoint. Eg: "client_secret_basic"',
})
.default("client_secret_basic")
.optional(),
grant_types: z
@@ -997,25 +1019,109 @@ export const oidcProvider = (options: OIDCOptions) => {
"urn:ietf:params:oauth:grant-type:saml2-bearer",
]),
)
.meta({
description:
'The grant types supported by the application. Eg: ["authorization_code"]',
})
.default(["authorization_code"])
.optional(),
response_types: z
.array(z.enum(["code", "token"]))
.meta({
description:
'The response types supported by the application. Eg: ["code"]',
})
.default(["code"])
.optional(),
client_name: z.string().optional(),
client_uri: z.string().optional(),
logo_uri: z.string().optional(),
scope: z.string().optional(),
contacts: z.array(z.string()).optional(),
tos_uri: z.string().optional(),
policy_uri: z.string().optional(),
jwks_uri: z.string().optional(),
jwks: z.record(z.any(), z.any()).optional(),
metadata: z.record(z.any(), z.any()).optional(),
software_id: z.string().optional(),
software_version: z.string().optional(),
software_statement: z.string().optional(),
client_name: z
.string()
.meta({
description: 'The name of the application. Eg: "My App"',
})
.optional(),
client_uri: z
.string()
.meta({
description:
'The URI of the application. Eg: "https://client.example.com"',
})
.optional(),
logo_uri: z
.string()
.meta({
description:
'The URI of the application logo. Eg: "https://client.example.com/logo.png"',
})
.optional(),
scope: z
.string()
.meta({
description:
'The scopes supported by the application. Separated by spaces. Eg: "profile email"',
})
.optional(),
contacts: z
.array(z.string())
.meta({
description:
'The contact information for the application. Eg: ["admin@example.com"]',
})
.optional(),
tos_uri: z
.string()
.meta({
description:
'The URI of the application terms of service. Eg: "https://client.example.com/tos"',
})
.optional(),
policy_uri: z
.string()
.meta({
description:
'The URI of the application privacy policy. Eg: "https://client.example.com/policy"',
})
.optional(),
jwks_uri: z
.string()
.meta({
description:
'The URI of the application JWKS. Eg: "https://client.example.com/jwks"',
})
.optional(),
jwks: z
.record(z.any(), z.any())
.meta({
description:
'The JWKS of the application. Eg: {"keys": [{"kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..."}]}',
})
.optional(),
metadata: z
.record(z.any(), z.any())
.meta({
description:
'The metadata of the application. Eg: {"key": "value"}',
})
.optional(),
software_id: z
.string()
.meta({
description:
'The software ID of the application. Eg: "my-software"',
})
.optional(),
software_version: z
.string()
.meta({
description:
'The software version of the application. Eg: "1.0.0"',
})
.optional(),
software_statement: z
.string()
.meta({
description: "The software statement of the application.",
})
.optional(),
}),
metadata: {
openapi: {

View File

@@ -65,6 +65,21 @@ export const oneTimeToken = (options?: OneTimeTokenopts) => {
return {
id: "one-time-token",
endpoints: {
/**
* ### Endpoint
*
* GET `/one-time-token/generate`
*
* ### API Methods
*
* **server:**
* `auth.api.generateOneTimeToken`
*
* **client:**
* `authClient.oneTimeToken.generate`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/one-time-token#api-method-one-time-token-generate)
*/
generateOneTimeToken: createAuthEndpoint(
"/one-time-token/generate",
{
@@ -94,12 +109,29 @@ export const oneTimeToken = (options?: OneTimeTokenopts) => {
return c.json({ token });
},
),
/**
* ### Endpoint
*
* POST `/one-time-token/verify`
*
* ### API Methods
*
* **server:**
* `auth.api.verifyOneTimeToken`
*
* **client:**
* `authClient.oneTimeToken.verify`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/one-time-token#api-method-one-time-token-verify)
*/
verifyOneTimeToken: createAuthEndpoint(
"/one-time-token/verify",
{
method: "POST",
body: z.object({
token: z.string(),
token: z.string().meta({
description: 'The token to verify. Eg: "some-token"',
}),
}),
},
async (c) => {

View File

@@ -20,6 +20,10 @@ export const orgMiddleware = createAuthMiddleware(async (ctx) => {
};
});
/**
* The middleware forces the endpoint to require a valid session by utilizing the `sessionMiddleware`.
* It also appends additional types to the session type regarding organizations.
*/
export const orgSessionMiddleware = createAuthMiddleware(
{
use: [sessionMiddleware],

View File

@@ -74,32 +74,363 @@ export const organization = <O extends OrganizationOptions>(
options?: OrganizationOptions & O,
) => {
let endpoints = {
createOrganization,
updateOrganization,
deleteOrganization,
/**
* ### Endpoint
*
* POST `/organization/create`
*
* ### API Methods
*
* **server:**
* `auth.api.createOrganization`
*
* **client:**
* `authClient.organization.create`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-create)
*/
createOrganization: createOrganization,
/**
* ### Endpoint
*
* POST `/organization/update`
*
* ### API Methods
*
* **server:**
* `auth.api.updateOrganization`
*
* **client:**
* `authClient.organization.update`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-update)
*/
updateOrganization: updateOrganization,
/**
* ### Endpoint
*
* POST `/organization/delete`
*
* ### API Methods
*
* **server:**
* `auth.api.deleteOrganization`
*
* **client:**
* `authClient.organization.delete`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-delete)
*/
deleteOrganization: deleteOrganization,
/**
* ### Endpoint
*
* POST `/organization/set-active`
*
* ### API Methods
*
* **server:**
* `auth.api.setActiveOrganization`
*
* **client:**
* `authClient.organization.setActive`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-set-active)
*/
setActiveOrganization: setActiveOrganization<O>(),
/**
* ### Endpoint
*
* GET `/organization/get-full-organization`
*
* ### API Methods
*
* **server:**
* `auth.api.getFullOrganization`
*
* **client:**
* `authClient.organization.getFullOrganization`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-get-full-organization)
*/
getFullOrganization: getFullOrganization<O>(),
listOrganizations,
/**
* ### Endpoint
*
* GET `/organization/list`
*
* ### API Methods
*
* **server:**
* `auth.api.listOrganizations`
*
* **client:**
* `authClient.organization.list`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-list)
*/
listOrganizations: listOrganizations,
/**
* ### Endpoint
*
* POST `/organization/invite-member`
*
* ### API Methods
*
* **server:**
* `auth.api.createInvitation`
*
* **client:**
* `authClient.organization.inviteMember`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-invite-member)
*/
createInvitation: createInvitation(options as O),
cancelInvitation,
acceptInvitation,
getInvitation,
rejectInvitation,
listUserInvitations,
checkOrganizationSlug,
/**
* ### Endpoint
*
* POST `/organization/cancel-invitation`
*
* ### API Methods
*
* **server:**
* `auth.api.cancelInvitation`
*
* **client:**
* `authClient.organization.cancelInvitation`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-cancel-invitation)
*/
cancelInvitation: cancelInvitation,
/**
* ### Endpoint
*
* POST `/organization/accept-invitation`
*
* ### API Methods
*
* **server:**
* `auth.api.acceptInvitation`
*
* **client:**
* `authClient.organization.acceptInvitation`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-accept-invitation)
*/
acceptInvitation: acceptInvitation,
/**
* ### Endpoint
*
* GET `/organization/get-invitation`
*
* ### API Methods
*
* **server:**
* `auth.api.getInvitation`
*
* **client:**
* `authClient.organization.getInvitation`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-get-invitation)
*/
getInvitation: getInvitation,
/**
* ### Endpoint
*
* POST `/organization/reject-invitation`
*
* ### API Methods
*
* **server:**
* `auth.api.rejectInvitation`
*
* **client:**
* `authClient.organization.rejectInvitation`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-reject-invitation)
*/
rejectInvitation: rejectInvitation,
/**
* ### Endpoint
*
* GET `/organization/list-invitations`
*
* ### API Methods
*
* **server:**
* `auth.api.listInvitations`
*
* **client:**
* `authClient.organization.listInvitations`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-list-invitations)
*/
listInvitations: listInvitations,
/**
* ### Endpoint
*
* GET `/organization/get-active-member`
*
* ### API Methods
*
* **server:**
* `auth.api.getActiveMember`
*
* **client:**
* `authClient.organization.getActiveMember`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-get-active-member)
*/
getActiveMember: getActiveMember,
/**
* ### Endpoint
*
* POST `/organization/check-slug`
*
* ### API Methods
*
* **server:**
* `auth.api.checkOrganizationSlug`
*
* **client:**
* `authClient.organization.checkSlug`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-check-slug)
*/
checkOrganizationSlug: checkOrganizationSlug,
/**
* ### Endpoint
*
* POST `/organization/add-member`
*
* ### API Methods
*
* **server:**
* `auth.api.addMember`
*
* **client:**
* `authClient.organization.addMember`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-add-member)
*/
addMember: addMember<O>(),
removeMember,
/**
* ### Endpoint
*
* POST `/organization/remove-member`
*
* ### API Methods
*
* **server:**
* `auth.api.removeMember`
*
* **client:**
* `authClient.organization.removeMember`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-remove-member)
*/
removeMember: removeMember,
/**
* ### Endpoint
*
* POST `/organization/update-member-role`
*
* ### API Methods
*
* **server:**
* `auth.api.updateMemberRole`
*
* **client:**
* `authClient.organization.updateMemberRole`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-update-member-role)
*/
updateMemberRole: updateMemberRole(options as O),
getActiveMember,
leaveOrganization,
listInvitations,
/**
* ### Endpoint
*
* POST `/organization/leave`
*
* ### API Methods
*
* **server:**
* `auth.api.leaveOrganization`
*
* **client:**
* `authClient.organization.leave`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-leave)
*/
leaveOrganization: leaveOrganization,
listUserInvitations,
};
const teamSupport = options?.teams?.enabled;
const teamEndpoints = {
/**
* ### Endpoint
*
* POST `/organization/create-team`
*
* ### API Methods
*
* **server:**
* `auth.api.createTeam`
*
* **client:**
* `authClient.organization.createTeam`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-create-team)
*/
createTeam: createTeam(options as O),
listOrganizationTeams,
removeTeam,
updateTeam,
/**
* ### Endpoint
*
* GET `/organization/list-teams`
*
* ### API Methods
*
* **server:**
* `auth.api.listOrganizationTeams`
*
* **client:**
* `authClient.organization.listTeams`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-list-teams)
*/
listOrganizationTeams: listOrganizationTeams,
/**
* ### Endpoint
*
* POST `/organization/remove-team`
*
* ### API Methods
*
* **server:**
* `auth.api.removeTeam`
*
* **client:**
* `authClient.organization.removeTeam`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-remove-team)
*/
removeTeam: removeTeam,
/**
* ### Endpoint
*
* POST `/organization/update-team`
*
* ### API Methods
*
* **server:**
* `auth.api.updateTeam`
*
* **client:**
* `authClient.organization.updateTeam`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-update-team)
*/
updateTeam: updateTeam,
};
if (teamSupport) {
endpoints = {

View File

@@ -22,7 +22,8 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
email: z.string().meta({
description: "The email address of the user to invite",
}),
role: z.union([
role: z
.union([
z.string().meta({
description: "The role to assign to the user",
}),
@@ -31,7 +32,11 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
description: "The roles to assign to the user",
}),
),
]),
])
.meta({
description:
'The role(s) to assign to the user. It can be `admin`, `member`, or `guest`. Eg: "member"',
}),
organizationId: z
.string()
.meta({
@@ -42,7 +47,7 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
.boolean()
.meta({
description:
"Resend the invitation email, if the user is already invited",
"Resend the invitation email, if the user is already invited. Eg: true",
})
.optional(),
teamId: z
@@ -443,6 +448,7 @@ export const acceptInvitation = createAuthEndpoint(
});
},
);
export const rejectInvitation = createAuthEndpoint(
"/organization/reject-invitation",
{

View File

@@ -17,9 +17,28 @@ export const addMember = <O extends OrganizationOptions>() =>
{
method: "POST",
body: z.object({
userId: z.coerce.string(),
role: z.union([z.string(), z.array(z.string())]),
organizationId: z.string().optional(),
userId: z.coerce.string().meta({
description:
'The user Id which represents the user to be added as a member. If `null` is provided, then it\'s expected to provide session headers. Eg: "user-id"',
}),
role: z.union([z.string(), z.array(z.string())]).meta({
description:
'The role(s) to assign to the new member. Eg: ["admin", "sale"]',
}),
organizationId: z
.string()
.meta({
description:
'An optional organization ID to pass. If not provided, will default to the user\'s active organization. Eg: "org-id"',
})
.optional(),
teamId: z
.string()
.meta({
description:
'An optional team ID to add the member to. Eg: "team-id"',
})
.optional(),
}),
use: [orgMiddleware],
metadata: {
@@ -133,13 +152,10 @@ export const removeMember = createAuthEndpoint(
/**
* If not provided, the active organization will be used
*/
organizationId: z
.string()
.meta({
organizationId: z.string().meta({
description:
"The ID of the organization to remove the member from. If not provided, the active organization will be used",
})
.optional(),
'The ID of the organization to remove the member from. If not provided, the active organization will be used. Eg: "org-id"',
}),
}),
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
@@ -281,9 +297,21 @@ export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
{
method: "POST",
body: z.object({
role: z.union([z.string(), z.array(z.string())]),
memberId: z.string(),
organizationId: z.string().optional(),
role: z.union([z.string(), z.array(z.string())]).meta({
description:
'The new role to be applied. This can be a string or array of strings representing the roles. Eg: ["admin", "sale"]',
}),
memberId: z.string().meta({
description:
'The member id to apply the role update to. Eg: "member-id"',
}),
organizationId: z
.string()
.meta({
description:
'An optional organization ID which the member is a part of to apply the role update. If not provided, you must provide session headers to get the active organization. Eg: "organization-id"',
})
.optional(),
}),
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
@@ -430,6 +458,7 @@ export const getActiveMember = createAuthEndpoint(
{
method: "GET",
use: [orgMiddleware, orgSessionMiddleware],
requireHeaders: true,
metadata: {
openapi: {
description: "Get the member details of the active organization",
@@ -496,8 +525,12 @@ export const leaveOrganization = createAuthEndpoint(
{
method: "POST",
body: z.object({
organizationId: z.string(),
organizationId: z.string().meta({
description:
'The organization Id for the member to leave. Eg: "organization-id"',
}),
}),
requireHeaders: true,
use: [sessionMiddleware, orgMiddleware],
},
async (ctx) => {

View File

@@ -31,7 +31,7 @@ export const createOrganization = createAuthEndpoint(
.string()
.meta({
description:
"The user id of the organization creator. If not provided, the current user will be used. Should only be used by admins or when called by the server.",
'The user id of the organization creator. If not provided, the current user will be used. Should only be used by admins or when called by the server. server-only. Eg: "user-id"',
})
.optional(),
logo: z
@@ -50,7 +50,7 @@ export const createOrganization = createAuthEndpoint(
.boolean()
.meta({
description:
"Whether to keep the current active organization active after creating a new one",
"Whether to keep the current active organization active after creating a new one. Eg: true",
})
.optional(),
}),
@@ -228,7 +228,9 @@ export const checkOrganizationSlug = createAuthEndpoint(
{
method: "POST",
body: z.object({
slug: z.string(),
slug: z.string().meta({
description: 'The organization slug to check. Eg: "my-org"',
}),
}),
use: [requestOnlySessionMiddleware, orgMiddleware],
},
@@ -279,7 +281,12 @@ export const updateOrganization = createAuthEndpoint(
.optional(),
})
.partial(),
organizationId: z.string().optional(),
organizationId: z
.string()
.meta({
description: 'The organization ID. Eg: "org-id"',
})
.optional(),
}),
requireHeaders: true,
use: [orgMiddleware],
@@ -552,7 +559,7 @@ export const setActiveOrganization = <O extends OrganizationOptions>() => {
.string()
.meta({
description:
"The organization id to set as active. It can be null to unset the active organization",
'The organization id to set as active. It can be null to unset the active organization. Eg: "org-id"',
})
.nullable()
.optional(),
@@ -560,7 +567,7 @@ export const setActiveOrganization = <O extends OrganizationOptions>() => {
.string()
.meta({
description:
"The organization slug to set as active. It can be null to unset the active organization if organizationId is not provided",
'The organization slug to set as active. It can be null to unset the active organization if organizationId is not provided. Eg: "org-slug"',
})
.optional(),
}),

View File

@@ -15,8 +15,16 @@ export const createTeam = <O extends OrganizationOptions>(options: O) =>
{
method: "POST",
body: z.object({
organizationId: z.string().optional(),
name: z.string(),
name: z.string().meta({
description: 'The name of the team. Eg: "my-team"',
}),
organizationId: z
.string()
.meta({
description:
'The organization ID which the team will be created in. Defaults to the active organization. Eg: "organization-id"',
})
.optional(),
}),
use: [orgMiddleware],
metadata: {
@@ -143,8 +151,15 @@ export const removeTeam = createAuthEndpoint(
{
method: "POST",
body: z.object({
teamId: z.string(),
organizationId: z.string().optional(),
teamId: z.string().meta({
description: `The team ID of the team to remove. Eg: "team-id"`,
}),
organizationId: z
.string()
.meta({
description: `The organization ID which the team falls under. If not provided, it will default to the user's active organization. Eg: "organization-id"`,
})
.optional(),
}),
use: [orgMiddleware],
metadata: {
@@ -246,9 +261,12 @@ export const updateTeam = createAuthEndpoint(
{
method: "POST",
body: z.object({
teamId: z.string(),
teamId: z.string().meta({
description: `The ID of the team to be updated. Eg: "team-id"`,
}),
data: teamSchema.partial(),
}),
requireHeaders: true,
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
openapi: {
@@ -363,9 +381,15 @@ export const listOrganizationTeams = createAuthEndpoint(
method: "GET",
query: z.optional(
z.object({
organizationId: z.string().optional(),
organizationId: z
.string()
.meta({
description: `The organization ID which the teams are under to list. Defaults to the users active organization. Eg: "organziation-id"`,
})
.optional(),
}),
),
requireHeaders: true,
metadata: {
openapi: {
description: "List all teams in an organization",

View File

@@ -773,6 +773,21 @@ export const passkey = (options?: PasskeyOptions) => {
}
},
),
/**
* ### Endpoint
*
* GET `/passkey/list-user-passkeys`
*
* ### API Methods
*
* **server:**
* `auth.api.listPasskeys`
*
* **client:**
* `authClient.passkey.listUserPasskeys`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/passkey#api-method-passkey-list-user-passkeys)
*/
listPasskeys: createAuthEndpoint(
"/passkey/list-user-passkeys",
{
@@ -818,12 +833,30 @@ export const passkey = (options?: PasskeyOptions) => {
});
},
),
/**
* ### Endpoint
*
* POST `/passkey/delete-passkey`
*
* ### API Methods
*
* **server:**
* `auth.api.deletePasskey`
*
* **client:**
* `authClient.passkey.deletePasskey`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/passkey#api-method-passkey-delete-passkey)
*/
deletePasskey: createAuthEndpoint(
"/passkey/delete-passkey",
{
method: "POST",
body: z.object({
id: z.string(),
id: z.string().meta({
description:
'The ID of the passkey to delete. Eg: "some-passkey-id"',
}),
}),
use: [sessionMiddleware],
metadata: {
@@ -867,16 +900,31 @@ export const passkey = (options?: PasskeyOptions) => {
});
},
),
/**
* ### Endpoint
*
* POST `/passkey/update-passkey`
*
* ### API Methods
*
* **server:**
* `auth.api.updatePasskey`
*
* **client:**
* `authClient.passkey.updatePasskey`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/passkey#api-method-passkey-update-passkey)
*/
updatePasskey: createAuthEndpoint(
"/passkey/update-passkey",
{
method: "POST",
body: z.object({
id: z.string().meta({
description: "The ID of the passkey to update",
description: `The ID of the passkey which will be updated. Eg: \"passkey-id\"`,
}),
name: z.string().meta({
description: "The name of the passkey",
description: `The new name which the passkey will be updated to. Eg: \"my-new-passkey-name\"`,
}),
}),
use: [sessionMiddleware],

View File

@@ -144,21 +144,36 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
return {
id: "phone-number",
endpoints: {
/**
* ### Endpoint
*
* POST `/sign-in/phone-number`
*
* ### API Methods
*
* **server:**
* `auth.api.signInPhoneNumber`
*
* **client:**
* `authClient.signIn.phoneNumber`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/phone-number#api-method-sign-in-phone-number)
*/
signInPhoneNumber: createAuthEndpoint(
"/sign-in/phone-number",
{
method: "POST",
body: z.object({
phoneNumber: z.string().meta({
description: "Phone number to sign in",
description: 'Phone number to sign in. Eg: "+1234567890"',
}),
password: z.string().meta({
description: "Password to use for sign in",
description: "Password to use for sign in.",
}),
rememberMe: z
.boolean()
.meta({
description: "Remember the session",
description: "Remember the session. Eg: true",
})
.optional(),
}),
@@ -309,13 +324,28 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
});
},
),
/**
* ### Endpoint
*
* POST `/phone-number/send-otp`
*
* ### API Methods
*
* **server:**
* `auth.api.sendPhoneNumberOTP`
*
* **client:**
* `authClient.phoneNumber.sendOtp`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/phone-number#api-method-phone-number-send-otp)
*/
sendPhoneNumberOTP: createAuthEndpoint(
"/phone-number/send-otp",
{
method: "POST",
body: z.object({
phoneNumber: z.string().meta({
description: "Phone number to send OTP",
description: 'Phone number to send OTP. Eg: "+1234567890"',
}),
}),
metadata: {
@@ -380,6 +410,22 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
return ctx.json({ message: "code sent" });
},
),
/**
* ### Endpoint
*
* POST `/phone-number/verify`
*
* ### API Methods
*
* **server:**
* `auth.api.verifyPhoneNumber`
*
* **client:**
* `authClient.phoneNumber.verify`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/phone-number#api-method-phone-number-verify)
*/
verifyPhoneNumber: createAuthEndpoint(
"/phone-number/verify",
{
@@ -389,13 +435,13 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
* Phone number
*/
phoneNumber: z.string().meta({
description: "Phone number to verify",
description: 'Phone number to verify. Eg: "+1234567890"',
}),
/**
* OTP code
*/
code: z.string().meta({
description: "OTP code",
description: 'OTP code. Eg: "123456"',
}),
/**
* Disable session creation after verification
@@ -404,7 +450,8 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
disableSession: z
.boolean()
.meta({
description: "Disable session creation after verification",
description:
"Disable session creation after verification. Eg: false",
})
.optional(),
/**
@@ -416,7 +463,7 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
.boolean()
.meta({
description:
"Check if there is a session and update the phone number",
"Check if there is a session and update the phone number. Eg: true",
})
.optional(),
}),
@@ -716,7 +763,9 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
{
method: "POST",
body: z.object({
phoneNumber: z.string(),
phoneNumber: z.string().meta({
description: `The phone number which is associated with the user. Eg: "+1234567890"`,
}),
}),
metadata: {
openapi: {
@@ -856,9 +905,17 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
{
method: "POST",
body: z.object({
otp: z.string(),
phoneNumber: z.string(),
newPassword: z.string(),
otp: z.string().meta({
description:
'The one time password to reset the password. Eg: "123456"',
}),
phoneNumber: z.string().meta({
description:
'The phone number to the account which intends to reset the password for. Eg: "+1234567890"',
}),
newPassword: z.string().meta({
description: `The new password. Eg: "new-and-secure-password"`,
}),
}),
metadata: {
openapi: {

View File

@@ -77,6 +77,21 @@ export const sso = (options?: SSOOptions) => {
return {
id: "sso",
endpoints: {
/**
* ### Endpoint
*
* POST `/sso/register`
*
* ### API Methods
*
* **server:**
* `auth.api.createOIDCProvider`
*
* **client:**
* `authClient.sso.register`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/sso#api-method-sso-register)
*/
createOIDCProvider: createAuthEndpoint(
"/sso/register",
{
@@ -84,47 +99,55 @@ export const sso = (options?: SSOOptions) => {
body: z.object({
providerId: z.string().meta({
description:
"The ID of the provider. This is used to identify the provider during login and callback",
'The ID of the provider. This is used to identify the provider during login and callback. Eg: "example-provider"',
}),
issuer: z.string().meta({
description:
"The issuer url of the provider (e.g. https://idp.example.com)",
'The issuer url of the provider. Eg: "https://idp.example.com"',
}),
domain: z.string().meta({
description:
"The domain of the provider. This is used for email matching",
'The domain of the provider. This is used for email matching. Eg: "example.com"',
}),
clientId: z.string().meta({
description: "The client ID",
description: 'The client ID. Eg: "1234567890"',
}),
clientSecret: z.string().meta({
description: "The client secret",
description: 'The client secret. Eg: "1234567890"',
}),
authorizationEndpoint: z
.string()
.meta({
description: "The authorization endpoint",
description:
'The authorization endpoint. Eg: "https://idp.example.com/authorize"',
})
.optional(),
tokenEndpoint: z
.string()
.meta({
description: "The token endpoint",
description:
'The token endpoint. Eg: "https://idp.example.com/token"',
})
.optional(),
userInfoEndpoint: z
.string()
.meta({
description: "The user info endpoint",
description:
'The user info endpoint. Eg: "https://idp.example.com/userinfo"',
})
.optional(),
tokenEndpointAuthentication: z
.enum(["client_secret_post", "client_secret_basic"])
.meta({
description:
"The authentication method for the token endpoint. Defaults to 'client_secret_post'. Eg: \"client_secret_post\"",
})
.optional(),
jwksEndpoint: z
.string()
.meta({
description: "The JWKS endpoint",
description:
'The JWKS endpoint. Eg: "https://idp.example.com/.well-known/jwks.json"',
})
.optional(),
discoveryEndpoint: z.string().optional(),
@@ -132,13 +155,14 @@ export const sso = (options?: SSOOptions) => {
.array(z.string())
.meta({
description:
"The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']",
"The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']. Eg: ['openid', 'email', 'profile']",
})
.optional(),
pkce: z
.boolean()
.meta({
description: "Whether to use PKCE for the authorization flow",
description:
"Whether to use PKCE for the authorization flow. Eg: true",
})
.default(true)
.optional(),
@@ -146,28 +170,28 @@ export const sso = (options?: SSOOptions) => {
.object({
id: z.string().meta({
description:
"The field in the user info response that contains the id. Defaults to 'sub'",
"The field in the user info response that contains the id. Defaults to 'sub'. Eg: \"sub\"",
}),
email: z.string().meta({
description:
"The field in the user info response that contains the email. Defaults to 'email'",
"The field in the user info response that contains the email. Defaults to 'email'. Eg: \"email\"",
}),
emailVerified: z
.string()
.meta({
description:
"The field in the user info response that contains whether the email is verified. defaults to 'email_verified'",
"The field in the user info response that contains whether the email is verified. defaults to 'email_verified'. Eg: \"email_verified\"",
})
.optional(),
name: z.string().meta({
description:
"The field in the user info response that contains the name. Defaults to 'name'",
"The field in the user info response that contains the name. Defaults to 'name'. Eg: \"name\"",
}),
image: z
.string()
.meta({
description:
"The field in the user info response that contains the image. Defaults to 'picture'",
"The field in the user info response that contains the image. Defaults to 'picture'. Eg: \"picture\"",
})
.optional(),
extraFields: z.record(z.string(), z.any()).optional(),
@@ -177,7 +201,7 @@ export const sso = (options?: SSOOptions) => {
.string()
.meta({
description:
"If organization plugin is enabled, the organization id to link the provider to",
'If organization plugin is enabled, the organization id to link the provider to. Eg: "some-org-id"',
})
.optional(),
overrideUserInfo: z
@@ -414,6 +438,21 @@ export const sso = (options?: SSOOptions) => {
});
},
),
/**
* ### Endpoint
*
* POST `/sign-in/sso`
*
* ### API Methods
*
* **server:**
* `auth.api.signInSSO`
*
* **client:**
* `authClient.signIn.sso`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/sign-in#api-method-sign-in-sso)
*/
signInSSO: createAuthEndpoint(
"/sign-in/sso",
{
@@ -423,42 +462,45 @@ export const sso = (options?: SSOOptions) => {
.string()
.meta({
description:
"The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided",
'The email address to sign in with. This is used to identify the issuer to sign in with. It\'s optional if the issuer is provided. Eg: "john@example.com"',
})
.optional(),
organizationSlug: z
.string()
.meta({
description: "The slug of the organization to sign in with",
description:
'The slug of the organization to sign in with. Eg: "example-org"',
})
.optional(),
providerId: z
.string()
.meta({
description:
"The ID of the provider to sign in with. This can be provided instead of email or issuer",
'The ID of the provider to sign in with. This can be provided instead of email or issuer. Eg: "example-provider"',
})
.optional(),
domain: z
.string()
.meta({
description: "The domain of the provider.",
description: 'The domain of the provider. Eg: "example.com"',
})
.optional(),
callbackURL: z.string().meta({
description: "The URL to redirect to after login",
description:
'The URL to redirect to after login. Eg: "https://example.com/callback"',
}),
errorCallbackURL: z
.string()
.meta({
description: "The URL to redirect to after login",
description:
'The URL to redirect to after login. Eg: "https://example.com/callback"',
})
.optional(),
newUserCallbackURL: z
.string()
.meta({
description:
"The URL to redirect to after login if the user is new",
'The URL to redirect to after login if the user is new. Eg: "https://example.com/new-user"',
})
.optional(),
scopes: z
@@ -471,7 +513,7 @@ export const sso = (options?: SSOOptions) => {
.boolean()
.meta({
description:
"Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider",
"Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider. Eg: true",
})
.optional(),
}),

View File

@@ -145,13 +145,30 @@ export const backupCode2fa = (options?: BackupCodeOptions) => {
return {
id: "backup_code",
endpoints: {
/**
* ### Endpoint
*
* POST `/two-factor/verify-backup-code`
*
* ### API Methods
*
* **server:**
* `auth.api.verifyBackupCode`
*
* **client:**
* `authClient.twoFactor.verifyBackupCode`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-verify-backup-code)
*/
verifyBackupCode: createAuthEndpoint(
"/two-factor/verify-backup-code",
{
method: "POST",
body: z.object({
code: z.string(),
code: z.string().meta({
description: `A backup code to verify. Eg: "123456"`,
}),
/**
* Disable setting the session cookie
*/
@@ -170,7 +187,7 @@ export const backupCode2fa = (options?: BackupCodeOptions) => {
.boolean()
.meta({
description:
"If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.",
"If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true",
})
.optional(),
}),
@@ -353,12 +370,29 @@ export const backupCode2fa = (options?: BackupCodeOptions) => {
});
},
),
/**
* ### Endpoint
*
* POST `/two-factor/generate-backup-codes`
*
* ### API Methods
*
* **server:**
* `auth.api.generateBackupCodes`
*
* **client:**
* `authClient.twoFactor.generateBackupCodes`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-generate-backup-codes)
*/
generateBackupCodes: createAuthEndpoint(
"/two-factor/generate-backup-codes",
{
method: "POST",
body: z.object({
password: z.string(),
password: z.string().meta({
description: "The users password.",
}),
}),
use: [sessionMiddleware],
metadata: {
@@ -429,12 +463,29 @@ export const backupCode2fa = (options?: BackupCodeOptions) => {
});
},
),
/**
* ### Endpoint
*
* GET `/two-factor/view-backup-codes`
*
* ### API Methods
*
* **server:**
* `auth.api.viewBackupCodes`
*
* **client:**
* `authClient.twoFactor.viewBackupCodes`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-view-backup-codes)
*/
viewBackupCodes: createAuthEndpoint(
"/two-factor/view-backup-codes",
{
method: "GET",
body: z.object({
userId: z.coerce.string(),
userId: z.coerce.string().meta({
description: `The user ID to view all backup codes. Eg: "user-id"`,
}),
}),
metadata: {
SERVER_ONLY: true,

View File

@@ -34,6 +34,21 @@ export const twoFactor = (options?: TwoFactorOptions) => {
...totp.endpoints,
...otp.endpoints,
...backupCode.endpoints,
/**
* ### Endpoint
*
* POST `/two-factor/enable`
*
* ### API Methods
*
* **server:**
* `auth.api.enableTwoFactor`
*
* **client:**
* `authClient.twoFactor.enable`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-enable)
*/
enableTwoFactor: createAuthEndpoint(
"/two-factor/enable",
{
@@ -157,6 +172,21 @@ export const twoFactor = (options?: TwoFactorOptions) => {
return ctx.json({ totpURI, backupCodes: backupCodes.backupCodes });
},
),
/**
* ### Endpoint
*
* POST `/two-factor/disable`
*
* ### API Methods
*
* **server:**
* `auth.api.disableTwoFactor`
*
* **client:**
* `authClient.twoFactor.disable`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-disable)
*/
disableTwoFactor: createAuthEndpoint(
"/two-factor/disable",
{

View File

@@ -136,7 +136,10 @@ export const otp2fa = (options?: OTPOptions) => {
* for 30 days. It'll be refreshed on
* every sign in request within this time.
*/
trustDevice: z.boolean().optional(),
trustDevice: z.boolean().optional().meta({
description:
"If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true",
}),
})
.optional(),
metadata: {
@@ -211,14 +214,17 @@ export const otp2fa = (options?: OTPOptions) => {
method: "POST",
body: z.object({
code: z.string().meta({
description: "The otp code to verify",
description: 'The otp code to verify. Eg: "012345"',
}),
/**
* if true, the device will be trusted
* for 30 days. It'll be refreshed on
* every sign in request within this time.
*/
trustDevice: z.boolean().optional(),
trustDevice: z.boolean().optional().meta({
description:
"If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true",
}),
}),
metadata: {
openapi: {
@@ -387,7 +393,37 @@ export const otp2fa = (options?: OTPOptions) => {
return {
id: "otp",
endpoints: {
/**
* ### Endpoint
*
* POST `/two-factor/send-otp`
*
* ### API Methods
*
* **server:**
* `auth.api.send2FaOTP`
*
* **client:**
* `authClient.twoFactor.sendOtp`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-send-otp)
*/
sendTwoFactorOTP: send2FaOTP,
/**
* ### Endpoint
*
* POST `/two-factor/verify-otp`
*
* ### API Methods
*
* **server:**
* `auth.api.verifyOTP`
*
* **client:**
* `authClient.twoFactor.verifyOtp`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-verify-otp)
*/
verifyTwoFactorOTP: verifyOTP,
},
} satisfies TwoFactorProvider;

View File

@@ -180,7 +180,7 @@ export const totp2fa = (options?: TOTPOptions) => {
method: "POST",
body: z.object({
code: z.string().meta({
description: "The otp code to verify",
description: 'The otp code to verify. Eg: "012345"',
}),
/**
* if true, the device will be trusted
@@ -191,7 +191,7 @@ export const totp2fa = (options?: TOTPOptions) => {
.boolean()
.meta({
description:
"If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time.",
"If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true",
})
.optional(),
}),
@@ -285,10 +285,41 @@ export const totp2fa = (options?: TOTPOptions) => {
return valid(ctx);
},
);
return {
id: "totp",
endpoints: {
/**
* ### Endpoint
*
* POST `/totp/generate`
*
* ### API Methods
*
* **server:**
* `auth.api.generateTOTP`
*
* **client:**
* `authClient.totp.generate`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/totp#api-method-totp-generate)
*/
generateTOTP: generateTOTP,
/**
* ### Endpoint
*
* POST `/two-factor/get-totp-uri`
*
* ### API Methods
*
* **server:**
* `auth.api.getTOTPURI`
*
* **client:**
* `authClient.twoFactor.getTotpUri`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/two-factor#api-method-two-factor-get-totp-uri)
*/
getTOTPURI: getTOTPURI,
verifyTOTP,
},

View File

@@ -109,6 +109,21 @@ export const stripe = <O extends StripeOptions>(options: O) => {
});
const subscriptionEndpoints = {
/**
* ### Endpoint
*
* POST `/subscription/upgrade`
*
* ### API Methods
*
* **server:**
* `auth.api.upgradeSubscription`
*
* **client:**
* `authClient.subscription.upgrade`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-upgrade)
*/
upgradeSubscription: createAuthEndpoint(
"/subscription/upgrade",
{
@@ -118,7 +133,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
* The name of the plan to subscribe
*/
plan: z.string().meta({
description: "The name of the plan to upgrade to",
description: 'The name of the plan to upgrade to. Eg: "pro"',
}),
/**
* If annual plan should be applied.
@@ -126,7 +141,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
annual: z
.boolean()
.meta({
description: "Whether to upgrade to an annual plan",
description: "Whether to upgrade to an annual plan. Eg: true",
})
.optional(),
/**
@@ -137,7 +152,8 @@ export const stripe = <O extends StripeOptions>(options: O) => {
referenceId: z
.string()
.meta({
description: "Reference id of the subscription to upgrade",
description:
'Reference id of the subscription to upgrade. Eg: "123"',
})
.optional(),
/**
@@ -148,7 +164,8 @@ export const stripe = <O extends StripeOptions>(options: O) => {
subscriptionId: z
.string()
.meta({
description: "The id of the subscription to upgrade",
description:
'The id of the subscription to upgrade. Eg: "sub_123"',
})
.optional(),
/**
@@ -162,7 +179,8 @@ export const stripe = <O extends StripeOptions>(options: O) => {
seats: z
.number()
.meta({
description: "Number of seats to upgrade to (if applicable)",
description:
"Number of seats to upgrade to (if applicable). Eg: 1",
})
.optional(),
/**
@@ -172,7 +190,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
.string()
.meta({
description:
"Callback URL to redirect back after successful subscription",
'Callback URL to redirect back after successful subscription. Eg: "https://example.com/success"',
})
.default("/"),
/**
@@ -182,17 +200,27 @@ export const stripe = <O extends StripeOptions>(options: O) => {
.string()
.meta({
description:
"Callback URL to redirect back after successful subscription",
'Callback URL to redirect back after successful subscription. Eg: "https://example.com/success"',
})
.default("/"),
/**
* Return URL
*/
returnUrl: z.string().optional(),
returnUrl: z
.string({
description:
'Return URL to redirect back after successful subscription. Eg: "https://example.com/success"',
})
.optional(),
/**
* Disable Redirect
*/
disableRedirect: z.boolean().default(false),
disableRedirect: z
.boolean({
description:
"Disable redirect after successful subscription. Eg: true",
})
.default(false),
}),
use: [
sessionMiddleware,
@@ -552,14 +580,42 @@ export const stripe = <O extends StripeOptions>(options: O) => {
throw ctx.redirect(getUrl(ctx, callbackURL));
},
),
/**
* ### Endpoint
*
* POST `/subscription/cancel`
*
* ### API Methods
*
* **server:**
* `auth.api.cancelSubscription`
*
* **client:**
* `authClient.subscription.cancel`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-cancel)
*/
cancelSubscription: createAuthEndpoint(
"/subscription/cancel",
{
method: "POST",
body: z.object({
referenceId: z.string().optional(),
subscriptionId: z.string().optional(),
returnUrl: z.string(),
referenceId: z
.string({
description:
"Reference id of the subscription to cancel. Eg: '123'",
})
.optional(),
subscriptionId: z
.string({
description:
"The id of the subscription to cancel. Eg: 'sub_123'",
})
.optional(),
returnUrl: z.string({
description:
"Return URL to redirect back after successful subscription. Eg: 'https://example.com/success'",
}),
}),
use: [
sessionMiddleware,
@@ -686,8 +742,18 @@ export const stripe = <O extends StripeOptions>(options: O) => {
{
method: "POST",
body: z.object({
referenceId: z.string().optional(),
subscriptionId: z.string().optional(),
referenceId: z
.string({
description:
"Reference id of the subscription to restore. Eg: '123'",
})
.optional(),
subscriptionId: z
.string({
description:
"The id of the subscription to restore. Eg: 'sub_123'",
})
.optional(),
}),
use: [sessionMiddleware, referenceMiddleware("restore-subscription")],
},
@@ -787,13 +853,33 @@ export const stripe = <O extends StripeOptions>(options: O) => {
}
},
),
/**
* ### Endpoint
*
* GET `/subscription/list`
*
* ### API Methods
*
* **server:**
* `auth.api.listActiveSubscriptions`
*
* **client:**
* `authClient.subscription.list`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-list)
*/
listActiveSubscriptions: createAuthEndpoint(
"/subscription/list",
{
method: "GET",
query: z.optional(
z.object({
referenceId: z.string().optional(),
referenceId: z
.string({
description:
"Reference id of the subscription to list. Eg: '123'",
})
.optional(),
}),
),
use: [sessionMiddleware, referenceMiddleware("list-subscription")],