mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 12:27:43 +00:00
feat(docs): APIMethod, documents all server & client auth examples (#2577)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
715
docs/components/api-method.tsx
Normal file
715
docs/components/api-method.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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` won’t be set to `true` until the user verifies their TOTP code.
|
||||
Note: `twoFactorEnabled` won’t 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 isn’t 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 isn’t 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 you’re 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]
|
||||
]
|
||||
})
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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({
|
||||
}
|
||||
}
|
||||
}
|
||||
})]
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
491
docs/scripts/endpoint-to-doc/index.ts
Normal file
491
docs/scripts/endpoint-to-doc/index.ts
Normal 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}`;
|
||||
}
|
||||
112
docs/scripts/endpoint-to-doc/input.ts
Normal file
112
docs/scripts/endpoint-to-doc/input.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
42
docs/scripts/endpoint-to-doc/output.mdx
Normal file
42
docs/scripts/endpoint-to-doc/output.mdx
Normal 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)
|
||||
*/
|
||||
29
docs/scripts/endpoint-to-doc/readme.md
Normal file
29
docs/scripts/endpoint-to-doc/readme.md
Normal 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.
|
||||
@@ -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) {
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -15,6 +15,7 @@ export function deleteAllExpiredApiKeysEndpoint({
|
||||
method: "POST",
|
||||
metadata: {
|
||||
SERVER_ONLY: true,
|
||||
client: false,
|
||||
},
|
||||
},
|
||||
async (ctx) => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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")],
|
||||
|
||||
Reference in New Issue
Block a user