feat: open api docs plugin

This commit is contained in:
Bereket Engida
2024-11-22 22:12:09 +03:00
parent 6a387f1bbf
commit 1e27706234
57 changed files with 1846 additions and 10661 deletions

View File

@@ -8,7 +8,7 @@ import {
twoFactor,
oneTap,
oAuthProxy,
createAuthEndpoint,
openAPI,
} from "better-auth/plugins";
import { reactInvitationEmail } from "./email/invitation";
import { LibsqlDialect } from "@libsql/kysely-libsql";
@@ -18,7 +18,6 @@ import { MysqlDialect } from "kysely";
import { createPool } from "mysql2/promise";
import { nextCookies } from "better-auth/next-js";
import { customSession } from "./auth/plugins/custom-session";
import { openAPI } from "@better-auth/open-api";
const from = process.env.BETTER_AUTH_EMAIL || "delivered@resend.dev";
const to = process.env.TEST_EMAIL || "";

View File

@@ -11,7 +11,6 @@
"lint": "next lint"
},
"dependencies": {
"@better-auth/open-api": "workspace:1.0.0-canary.12",
"@better-fetch/fetch": "1.1.12",
"@hookform/resolvers": "^3.9.0",
"@libsql/client": "^0.12.0",

View File

@@ -14,7 +14,6 @@ import { ForkButton } from "@/components/fork-button";
import Link from "next/link";
import defaultMdxComponents from "fumadocs-ui/mdx";
import { AutoTypeTable } from "fumadocs-typescript/ui";
import { openapi } from '@/app/source';
export default async function Page({
params,
@@ -74,11 +73,9 @@ export default async function Page({
Features,
ForkButton,
DatabaseTable,
APIPage: openapi.APIPage,
iframe: (props) => (
<iframe {...props} className="w-full h-[500px]" />
),
APIPage: openapi.APIPage,
}}
/>
</DocsBody>
@@ -87,48 +84,48 @@ export default async function Page({
}
export async function generateStaticParams() {
// const res = source.getPages().map((page) => ({
// slug: page.slugs,
// }));
const res = source.getPages().map((page) => ({
slug: page.slugs,
}));
return source.generateParams();
}
// export async function generateMetadata({
// params,
// }: { params: Promise<{ slug?: string[] }> }) {
// const { slug } = await params;
// const page = source.getPage(slug);
// if (page == null) notFound();
// const baseUrl = process.env.NEXT_PUBLIC_URL || process.env.VERCEL_URL;
// const url = new URL(`${baseUrl}/api/og`);
// const { title, description } = page.data;
// const pageSlug = page.file.path;
// url.searchParams.set("type", "Documentation");
// url.searchParams.set("mode", "dark");
// url.searchParams.set("heading", `${title}`);
export async function generateMetadata({
params,
}: { params: Promise<{ slug?: string[] }> }) {
const { slug } = await params;
const page = source.getPage(slug);
if (page == null) notFound();
const baseUrl = process.env.NEXT_PUBLIC_URL || process.env.VERCEL_URL;
const url = new URL(`${baseUrl}/api/og`);
const { title, description } = page.data;
const pageSlug = page.file.path;
url.searchParams.set("type", "Documentation");
url.searchParams.set("mode", "dark");
url.searchParams.set("heading", `${title}`);
// return {
// title,
// description,
// openGraph: {
// title,
// description,
// type: "website",
// url: absoluteUrl(`docs/${pageSlug}`),
// images: [
// {
// url: url.toString(),
// width: 1200,
// height: 630,
// alt: title,
// },
// ],
// },
// twitter: {
// card: "summary_large_image",
// title,
// description,
// images: [url.toString()],
// },
// };
// }
return {
title,
description,
openGraph: {
title,
description,
type: "website",
url: absoluteUrl(`docs/${pageSlug}`),
images: [
{
url: url.toString(),
width: 1200,
height: 630,
alt: title,
},
],
},
twitter: {
card: "summary_large_image",
title,
description,
images: [url.toString()],
},
};
}

View File

@@ -6,9 +6,6 @@ import { createOpenAPI } from "fumadocs-openapi/server";
export const source = loader({
baseUrl: "/docs",
source: createMDXSource(docs, meta),
pageTree: {
attachFile,
},
});
export const changelog = loader({

View File

@@ -877,6 +877,13 @@ export const contents: Content[] = [
</svg>
),
},
{
title: "Open API",
href: "/docs/plugins/open-api",
icon: ()=>(
<svg xmlns="http://www.w3.org/2000/svg" width="1.1em" height="1.1em" viewBox="0 0 32 32"><path fill="currentColor" d="M16 0C7.177 0 0 7.177 0 16s7.177 16 16 16s16-7.177 16-16S24.823 0 16 0m0 1.527c7.995 0 14.473 6.479 14.473 14.473S23.994 30.473 16 30.473S1.527 23.994 1.527 16S8.006 1.527 16 1.527m-4.839 6.296c-.188-.005-.375 0-.568.005c-1.307.079-2.093.693-2.312 1.964c-.151.891-.125 1.796-.188 2.692a9 9 0 0 1-.156 1.38c-.177.813-.525 1.068-1.353 1.109q-.167.018-.324.057v1.948c1.5.073 1.704.605 1.823 2.172c.048.573-.015 1.147.021 1.719q.042.816.208 1.6c.344 1.432 1.745 1.911 3.433 1.624V22.38c-.272 0-.511.005-.74 0c-.579-.016-.792-.161-.844-.713c-.079-.713-.057-1.437-.099-2.156c-.089-1.339-.235-2.651-1.541-3.5c.672-.495 1.161-1.084 1.312-1.865c.109-.547.177-1.099.219-1.651s-.025-1.12.021-1.667c.077-.885.135-1.249 1.197-1.213c.161 0 .317-.021.495-.036V7.834c-.213 0-.411-.005-.604-.011m10.126.016a5.4 5.4 0 0 0-1.089.079v1.697c.329 0 .584 0 .833.005c.439.005.772.177.813.661c.041.443.041.891.083 1.339c.089.896.136 1.796.292 2.677c.136.724.636 1.265 1.255 1.713c-1.088.729-1.411 1.776-1.463 2.953c-.032.801-.052 1.615-.093 2.427c-.037.74-.297.979-1.043.995c-.208.011-.411.027-.64.041v1.74c.432 0 .833.027 1.235 0c1.239-.073 1.995-.677 2.239-1.885a15 15 0 0 0 .183-2.005c.041-.615.036-1.235.099-1.844c.093-.953.532-1.349 1.484-1.411q.133-.018.267-.057v-1.953c-.161-.021-.271-.037-.391-.041c-.713-.032-1.068-.272-1.251-.948a6.6 6.6 0 0 1-.197-1.324c-.052-.823-.047-1.656-.099-2.479c-.109-1.588-1.063-2.339-2.516-2.38zm-9.188 7.036c-1.432 0-1.536 2.109-.115 2.245h.079a1.103 1.103 0 0 0 1.167-1.037v-.061a1.13 1.13 0 0 0-1.104-1.147zm3.88 0a1.083 1.083 0 0 0-1.115 1.043c0 .036 0 .067.005.104c0 .672.459 1.099 1.147 1.099c.677 0 1.104-.443 1.104-1.136c-.005-.672-.459-1.115-1.141-1.109zm3.948 0a1.15 1.15 0 0 0-1.167 1.115c0 .625.505 1.131 1.136 1.131h.011c.567.099 1.135-.448 1.172-1.104c.031-.609-.521-1.141-1.152-1.141z"></path></svg>
)
},
{
title: "JWT",
icon: () => (

View File

@@ -1,13 +0,0 @@
---
title: Link social account
full: true
_openapi:
method: POST
route: /link-social
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/link-social","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: List all accounts
full: true
_openapi:
method: GET
route: /list-accounts
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/list-accounts","method":"get"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Callback
full: true
_openapi:
method: GET
route: /callback/:id
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/callback/:id","method":"get"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Send verification email
full: true
_openapi:
method: POST
route: /send-verification-email
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/send-verification-email","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Verify email
full: true
_openapi:
method: GET
route: /verify-email
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/verify-email","method":"get"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Forget password
full: true
_openapi:
method: POST
route: /forget-password
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/forget-password","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Reset password with token
full: true
_openapi:
method: GET
route: /reset-password/:token
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/reset-password/:token","method":"get"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Reset password
full: true
_openapi:
method: POST
route: /reset-password
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/reset-password","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Get current session
full: true
_openapi:
method: GET
route: /get-session
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/get-session","method":"get"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: List all sessions
full: true
_openapi:
method: GET
route: /list-sessions
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/list-sessions","method":"get"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Revoke other sessions
full: true
_openapi:
method: POST
route: /revoke-other-sessions
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/revoke-other-sessions","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Revoke a session
full: true
_openapi:
method: POST
route: /revoke-session
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/revoke-session","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Revoke all sessions
full: true
_openapi:
method: POST
route: /revoke-sessions
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/revoke-sessions","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Sign in with email
full: true
_openapi:
method: POST
route: /sign-in/email
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/sign-in/email","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Sign in with social account
full: true
_openapi:
method: POST
route: /sign-in/social
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/sign-in/social","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Sign out
full: true
_openapi:
method: POST
route: /sign-out
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/sign-out","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Sign up with email
full: true
_openapi:
method: POST
route: /sign-up/email
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/sign-up/email","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Change email
full: true
_openapi:
method: POST
route: /change-email
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/change-email","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Change password
full: true
_openapi:
method: POST
route: /change-password
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/change-password","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Delete user
full: true
_openapi:
method: POST
route: /delete-user
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/delete-user","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Set password
full: true
_openapi:
method: POST
route: /set-password
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/set-password","method":"post"}]} hasHead={false} />

View File

@@ -1,13 +0,0 @@
---
title: Update user
full: true
_openapi:
method: POST
route: /update-user
toc: []
structuredData:
headings: []
contents: []
---
<APIPage document={"./open-api.json"} operations={[{"path":"/update-user","method":"post"}]} hasHead={false} />

View File

@@ -0,0 +1,38 @@
---
title: Open API
description: Open API reference for Better Auth.
---
This is a plugin that provides an Open API reference for Better Auth. It shows all endpoints added by plugins and the core. It also provides a way to test the endpoints. It uses [Scalar](https://scalar.com/) to display the Open API reference.
<Callout>
This plugin is still in the early stages of development. We are working on adding more features to it and filling in the gaps.
</Callout>
## Installation
<Steps>
<Step>
### Add the plugin to your **auth** config
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { openAPI } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [ // [!code highlight]
openAPI(), // [!code highlight]
] // [!code highlight]
})
```
</Step>
<Step>
### Navigate to `/api/auth/reference` to view the Open API reference
Each plugin endpoints are grouped by the plugin name. The core endpoints are grouped under the `Default` group. And Model schemas are grouped under the `Models` group.
![Open API reference](/open-api-reference.png)
</Step>
</Steps>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,6 @@
"dev": "next dev",
"start": "next start",
"build:docs": "node ./scripts/generate-docs.mjs",
"build:docs": "node ./scripts/generate-docs.mjs",
"typecheck": "tsc --noEmit",
"postinstall": "fumadocs-mdx"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -405,7 +405,7 @@
"@noble/hashes": "^1.5.0",
"@simplewebauthn/browser": "^10.0.0",
"@simplewebauthn/server": "^10.0.1",
"better-call": "0.3.1",
"better-call": "0.3.2",
"consola": "^3.2.3",
"defu": "^6.1.4",
"jose": "^5.9.4",

View File

@@ -10,6 +10,34 @@ export const listUserAccounts = createAuthEndpoint(
{
method: "GET",
use: [sessionMiddleware],
metadata: {
openapi: {
description: "List all accounts linked to the user",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "array",
items: {
type: "object",
properties: {
id: {
type: "string",
},
provider: {
type: "string",
},
},
},
},
},
},
},
},
},
},
},
async (c) => {
const session = c.context.session;
@@ -45,13 +73,45 @@ export const linkSocialAccount = createAuthEndpoint(
/**
* Callback URL to redirect to after the user has signed in.
*/
callbackURL: z.string().optional(),
callbackURL: z
.string({
description: "The URL to redirect to after the user has signed in",
})
.optional(),
/**
* OAuth2 provider to use`
*/
provider: z.enum(socialProviderList),
provider: z.enum(socialProviderList, {
description: "The OAuth2 provider to use",
}),
}),
use: [sessionMiddleware],
metadata: {
openapi: {
description: "Link a social account to the user",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
url: {
type: "string",
},
redirect: {
type: "boolean",
},
},
required: ["url", "redirect"],
},
},
},
},
},
},
},
},
async (c) => {
const session = c.context.session;

View File

@@ -38,13 +38,68 @@ export const sendVerificationEmail = createAuthEndpoint(
method: "POST",
query: z
.object({
currentURL: z.string().optional(),
currentURL: z
.string({
description: "The URL to use for email verification callback",
})
.optional(),
})
.optional(),
body: z.object({
email: z.string().email(),
callbackURL: z.string().optional(),
email: z
.string({
description: "The email to send the verification email to",
})
.email(),
callbackURL: z
.string({
description: "The URL to use for email verification callback",
})
.optional(),
}),
metadata: {
openapi: {
description: "Send a verification email to the user",
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
email: {
type: "string",
description: "The email to send the verification email to",
},
callbackURL: {
type: "string",
description:
"The URL to use for email verification callback",
},
},
required: ["email"],
},
},
},
},
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
if (!ctx.context.options.emailVerification?.sendVerificationEmail) {
@@ -85,9 +140,41 @@ export const verifyEmail = createAuthEndpoint(
{
method: "GET",
query: z.object({
token: z.string(),
callbackURL: z.string().optional(),
token: z.string({
description: "The token to verify the email",
}),
callbackURL: z
.string({
description: "The URL to redirect to after email verification",
})
.optional(),
}),
metadata: {
openapi: {
description: "Verify the email of the user",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
user: {
type: "object",
},
status: {
type: "boolean",
},
},
required: ["user", "status"],
},
},
},
},
},
},
},
},
async (ctx) => {
function redirectOnError(error: string) {

View File

@@ -87,7 +87,24 @@ export const error = createAuthEndpoint(
"/error",
{
method: "GET",
metadata: HIDE_METADATA,
metadata: {
...HIDE_METADATA,
openapi: {
description: "Displays an error page",
responses: {
"200": {
description: "Success",
content: {
"text/html": {
schema: {
type: "string",
},
},
},
},
},
},
},
},
async (c) => {
const query =

View File

@@ -37,15 +37,47 @@ export const forgetPassword = createAuthEndpoint(
/**
* The email address of the user to send a password reset email to.
*/
email: z.string().email(),
email: z
.string({
description:
"The email address of the user to send a password reset email to",
})
.email(),
/**
* 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: z.string().optional(),
redirectTo: z
.string({
description:
"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",
})
.optional(),
}),
metadata: {
openapi: {
description: "Send a password reset email to the user",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
if (!ctx.context.options.emailAndPassword?.sendResetPassword) {
@@ -108,8 +140,32 @@ export const forgetPasswordCallback = createAuthEndpoint(
{
method: "GET",
query: z.object({
callbackURL: z.string(),
callbackURL: z.string({
description: "The URL to redirect the user to reset their password",
}),
}),
metadata: {
openapi: {
description: "Redirects the user to the callback URL with the token",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
token: {
type: "string",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
const { token } = ctx.params;
@@ -144,9 +200,37 @@ export const resetPassword = createAuthEndpoint(
),
method: "POST",
body: z.object({
newPassword: z.string(),
token: z.string().optional(),
newPassword: z.string({
description: "The new password to set",
}),
token: z
.string({
description: "The token to reset the password",
})
.optional(),
}),
metadata: {
openapi: {
description: "Reset the password for a user",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
const token =

View File

@@ -5,7 +5,29 @@ export const ok = createAuthEndpoint(
"/ok",
{
method: "GET",
metadata: HIDE_METADATA,
metadata: {
...HIDE_METADATA,
openapi: {
description: "Check if the API is working",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
ok: {
type: "boolean",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
return ctx.json({

View File

@@ -26,13 +26,50 @@ export const getSession = <Option extends BetterAuthOptions>() =>
* If cookie cache is enabled, it will disable the cache
* and fetch the session from the database
*/
disableCookieCache: z.boolean().optional(),
disableCookieCache: z
.boolean({
description:
"Disable cookie cache and fetch session from database",
})
.optional(),
}),
),
requireHeaders: true,
metadata: {
openapi: {
tags: ["Session"],
description: "Get the current session",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
session: {
type: "object",
properties: {
token: {
type: "string",
},
userId: {
type: "string",
},
expiresAt: {
type: "string",
},
},
},
user: {
type: "object",
$ref: "#/components/schemas/User",
},
},
},
},
},
},
},
},
},
},
@@ -243,6 +280,37 @@ export const listSessions = <Option extends BetterAuthOptions>() =>
method: "GET",
use: [sessionMiddleware],
requireHeaders: true,
metadata: {
openapi: {
description: "List all active sessions for the user",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "array",
items: {
type: "object",
properties: {
token: {
type: "string",
},
userId: {
type: "string",
},
expiresAt: {
type: "string",
},
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
const sessions = await ctx.context.internalAdapter.listSessions(
@@ -265,10 +333,32 @@ export const revokeSession = createAuthEndpoint(
{
method: "POST",
body: z.object({
token: z.string(),
token: z.string({
description: "The token to revoke",
}),
}),
use: [sessionMiddleware],
requireHeaders: true,
metadata: {
openapi: {
description: "Revoke a single session",
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
token: {
type: "string",
},
},
required: ["token"],
},
},
},
},
},
},
},
async (ctx) => {
const token = ctx.body.token;
@@ -306,6 +396,29 @@ export const revokeSessions = createAuthEndpoint(
method: "POST",
use: [sessionMiddleware],
requireHeaders: true,
metadata: {
openapi: {
description: "Revoke all sessions for the user",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
},
},
required: ["status"],
},
},
},
},
},
},
},
},
async (ctx) => {
try {
@@ -333,6 +446,29 @@ export const revokeOtherSessions = createAuthEndpoint(
method: "POST",
requireHeaders: true,
use: [sessionMiddleware],
metadata: {
openapi: {
description:
"Revoke all other sessions for the user except the current one",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
const session = ctx.context.session;

View File

@@ -25,25 +25,41 @@ export const signInSocial = createAuthEndpoint(
* Callback URL to redirect to after the user
* has signed in.
*/
callbackURL: z.string().optional(),
callbackURL: z
.string({
description:
"Callback URL to redirect to after the user has signed in",
})
.optional(),
/**
* Callback url to redirect to if an error happens
*
* If it's initiated from the client sdk this defaults to
* the current url.
*/
errorCallbackURL: z.string().optional(),
errorCallbackURL: z
.string({
description: "Callback URL to redirect to if an error happens",
})
.optional(),
/**
* OAuth2 provider to use`
*/
provider: z.enum(socialProviderList),
provider: z.enum(socialProviderList, {
description: "OAuth2 provider to use",
}),
/**
* Disable automatic redirection to the provider
*
* This is useful if you want to handle the redirection
* yourself like in a popup or a different tab.
*/
disableRedirect: z.boolean().optional(),
disableRedirect: z
.boolean({
description:
"Disable automatic redirection to the provider. Useful for handling the redirection yourself",
})
.optional(),
/**
* ID token from the provider
*
@@ -60,26 +76,80 @@ export const signInSocial = createAuthEndpoint(
/**
* ID token from the provider
*/
token: z.string(),
token: z.string({
description: "ID token from the provider",
}),
/**
* The nonce used to generate the token
*/
nonce: z.string().optional(),
nonce: z
.string({
description: "Nonce used to generate the token",
})
.optional(),
/**
* Access token from the provider
*/
accessToken: z.string().optional(),
accessToken: z
.string({
description: "Access token from the provider",
})
.optional(),
/**
* Refresh token from the provider
*/
refreshToken: z.string().optional(),
refreshToken: z
.string({
description: "Refresh token from the provider",
})
.optional(),
/**
* Expiry date of the token
*/
expiresAt: z.number().optional(),
expiresAt: z
.number({
description: "Expiry date of the token",
})
.optional(),
}),
{
description:
"ID token from the provider to sign in the user with id token",
},
),
}),
metadata: {
openapi: {
description: "Sign in with a social provider",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
session: {
type: "string",
},
user: {
type: "object",
},
url: {
type: "string",
},
redirect: {
type: "boolean",
},
},
required: ["session", "user", "url", "redirect"],
},
},
},
},
},
},
},
},
async (c) => {
const provider = c.context.socialProviders.find(
@@ -192,22 +262,69 @@ export const signInEmail = createAuthEndpoint(
/**
* Email of the user
*/
email: z.string(),
email: z.string({
description: "Email of the user",
}),
/**
* Password of the user
*/
password: z.string(),
password: z.string({
description: "Password of the user",
}),
/**
* Callback URL to use as a redirect for email
* verification and for possible redirects
*/
callbackURL: z.string().optional(),
callbackURL: z
.string({
description:
"Callback URL to use as a redirect for email verification",
})
.optional(),
/**
* If this is false, the session will not be remembered
* @default true
*/
rememberMe: z.boolean().default(true).optional(),
rememberMe: z
.boolean({
description:
"If this is false, the session will not be remembered. Default is `true`.",
})
.default(true)
.optional(),
}),
metadata: {
openapi: {
description: "Sign in with email and password",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
session: {
type: "string",
},
user: {
type: "object",
},
url: {
type: "string",
},
redirect: {
type: "boolean",
},
},
required: ["session", "user", "url", "redirect"],
},
},
},
},
},
},
},
},
async (ctx) => {
if (!ctx.context.options?.emailAndPassword?.enabled) {

View File

@@ -8,6 +8,28 @@ export const signOut = createAuthEndpoint(
{
method: "POST",
requireHeaders: true,
metadata: {
openapi: {
description: "Sign out the current user",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: {
type: "boolean",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
const sessionCookieToken = await ctx.getSignedCookie(

View File

@@ -29,6 +29,60 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
password: ZodString;
}> &
toZod<AdditionalUserFieldsInput<O>>,
metadata: {
openapi: {
description: "Sign up a user using email and password",
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: {
type: "string",
description: "The name of the user",
},
email: {
type: "string",
description: "The email of the user",
},
password: {
type: "string",
description: "The password of the user",
},
callbackURL: {
type: "string",
description:
"The URL to use for email verification callback",
},
},
required: ["name", "email", "password"],
},
},
},
},
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
user: {
type: "object",
},
session: {
type: "object",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
if (!ctx.context.options.emailAndPassword?.enabled) {

View File

@@ -20,6 +20,47 @@ export const updateUser = <O extends BetterAuthOptions>() =>
}> &
toZod<AdditionalUserFieldsInput<O>>,
use: [sessionMiddleware],
metadata: {
openapi: {
description: "Update the current user",
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: {
type: "string",
description: "The name of the user",
},
image: {
type: "string",
description: "The image of the user",
},
},
},
},
},
},
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
user: {
type: "object",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
const body = ctx.body as {
@@ -74,18 +115,49 @@ export const changePassword = createAuthEndpoint(
/**
* The new password to set
*/
newPassword: z.string(),
newPassword: z.string({
description: "The new password to set",
}),
/**
* The current password of the user
*/
currentPassword: z.string(),
currentPassword: z.string({
description: "The current password",
}),
/**
* revoke all sessions that are not the
* current one logged in by the user
*/
revokeOtherSessions: z.boolean().optional(),
revokeOtherSessions: z
.boolean({
description: "Revoke all other sessions",
})
.optional(),
}),
use: [sessionMiddleware],
metadata: {
openapi: {
description: "Change the password of the user",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
user: {
description: "The user object",
$ref: "#/components/schemas/User",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
const { newPassword, currentPassword, revokeOtherSessions } = ctx.body;
@@ -215,9 +287,28 @@ export const deleteUser = createAuthEndpoint(
{
method: "POST",
body: z.object({
password: z.string(),
password: z.string({
description: "The password of the user",
}),
}),
use: [sessionMiddleware],
metadata: {
openapi: {
description: "Delete the user",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
},
},
},
},
},
},
},
},
async (ctx) => {
const { password } = ctx.body;
@@ -259,10 +350,42 @@ export const changeEmail = createAuthEndpoint(
})
.optional(),
body: z.object({
newEmail: z.string().email(),
callbackURL: z.string().optional(),
newEmail: z
.string({
description: "The new email to set",
})
.email(),
callbackURL: z
.string({
description: "The URL to redirect to after email verification",
})
.optional(),
}),
use: [sessionMiddleware],
metadata: {
openapi: {
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
user: {
type: "object",
},
status: {
type: "boolean",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
if (!ctx.context.options.user?.changeEmail?.enabled) {

View File

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

View File

@@ -0,0 +1,434 @@
import type { Endpoint, EndpointOptions } from "better-call";
import { ZodObject, ZodOptional, ZodSchema } from "zod";
import type { OpenAPISchemaType, OpenAPIParameter } from "better-call";
import type { AuthContext, BetterAuthOptions } from "better-auth";
import { getEndpoints } from "better-auth/api";
import { getAuthTables } from "../../db";
interface Path {
get?: {
tags?: string[];
operationId?: string;
description?: string;
security?: [{ bearerAuth: string[] }];
parameters?: OpenAPIParameter[];
responses?: {
[key in string]: {
description?: string;
content: {
"application/json": {
schema: {
type?: OpenAPISchemaType;
properties?: Record<string, any>;
required?: string[];
$ref?: string;
};
};
};
};
};
};
post?: {
tags?: string[];
operationId?: string;
description?: string;
security?: [{ bearerAuth: string[] }];
parameters?: OpenAPIParameter[];
requestBody?: {
content: {
"application/json": {
schema: {
type?: OpenAPISchemaType;
properties?: Record<string, any>;
required?: string[];
$ref?: string;
};
};
};
};
responses?: {
[key in string]: {
description?: string;
content: {
"application/json": {
schema: {
type?: OpenAPISchemaType;
properties?: Record<string, any>;
required?: string[];
$ref?: string;
};
};
};
};
};
};
}
const paths: Record<string, Path> = {};
function getTypeFromZodType(zodType: ZodSchema) {
switch (zodType.constructor.name) {
case "ZodString":
return "string";
case "ZodNumber":
return "number";
case "ZodBoolean":
return "boolean";
case "ZodObject":
return "object";
case "ZodArray":
return "array";
default:
return "string";
}
}
function getParameters(options: EndpointOptions) {
const parameters: OpenAPIParameter[] = [];
if (options.metadata?.openapi?.parameters) {
parameters.push(...options.metadata.openapi.parameters);
return parameters;
}
if (options.query instanceof ZodObject) {
Object.entries(options.query.shape).forEach(([key, value]) => {
if (value instanceof ZodSchema) {
parameters.push({
name: key,
in: "query",
schema: {
type: getTypeFromZodType(value),
...("minLength" in value && value.minLength
? {
minLength: value.minLength as number,
}
: {}),
description: value.description,
},
});
}
});
}
return parameters;
}
function getRequestBody(options: EndpointOptions): any {
if (options.metadata?.openapi?.requestBody) {
return options.metadata.openapi.requestBody;
}
if (!options.body) return undefined;
if (
options.body instanceof ZodObject ||
options.body instanceof ZodOptional
) {
// @ts-ignore
const shape = options.body.shape;
if (!shape) return undefined;
const properties: Record<string, any> = {};
const required: string[] = [];
Object.entries(shape).forEach(([key, value]) => {
if (value instanceof ZodSchema) {
properties[key] = {
type: getTypeFromZodType(value),
description: value.description,
};
if (!(value instanceof ZodOptional)) {
required.push(key);
}
}
});
return {
required:
options.body instanceof ZodOptional
? false
: options.body
? true
: false,
content: {
"application/json": {
schema: {
type: "object",
properties,
required,
},
},
},
};
}
return undefined;
}
function getResponse(responses?: Record<string, any>) {
return {
"400": {
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
},
},
required: ["message"],
},
},
},
description:
"Bad Request. Usually due to missing parameters, or invalid parameters.",
},
"401": {
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
},
},
required: ["message"],
},
},
},
description: "Unauthorized. Due to missing or invalid authentication.",
},
"403": {
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
},
},
},
},
},
description:
"Forbidden. You do not have permission to access this resource or to perform this action.",
},
"404": {
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
},
},
},
},
},
description: "Not Found. The requested resource was not found.",
},
"429": {
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
},
},
},
},
},
description:
"Too Many Requests. You have exceeded the rate limit. Try again later.",
},
"500": {
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
},
},
},
},
},
description:
"Internal Server Error. This is a problem with the server that you cannot fix.",
},
...responses,
} as any;
}
export async function generator(ctx: AuthContext, options: BetterAuthOptions) {
const baseEndpoints = getEndpoints(ctx, {
...options,
plugins: [],
});
const tables = getAuthTables(options);
const models = Object.entries(tables).reduce((acc, [key, value]) => {
const modelName = key.charAt(0).toUpperCase() + key.slice(1);
// @ts-ignore
acc[modelName] = {
type: "object",
properties: Object.entries(value.fields).reduce(
(acc, [key, value]) => {
acc[key] = {
type: value.type,
};
return acc;
},
{} as Record<string, any>,
),
};
return acc;
}, {});
const components = {
schemas: {
...models,
},
};
Object.entries(baseEndpoints.api).forEach(([_, value]) => {
const options = value.options as EndpointOptions;
if (options.metadata?.SERVER_ONLY) return;
if (options.method === "GET") {
paths[value.path] = {
get: {
tags: ["Default", ...(options.metadata?.openapi?.tags || [])],
description: options.metadata?.openapi?.description,
operationId: options.metadata?.openapi?.operationId,
security: [
{
bearerAuth: [],
},
],
parameters: getParameters(options),
responses: getResponse(options.metadata?.openapi?.responses),
},
};
}
if (options.method === "POST") {
const body = getRequestBody(options);
paths[value.path] = {
post: {
tags: ["Default", ...(options.metadata?.openapi?.tags || [])],
description: options.metadata?.openapi?.description,
operationId: options.metadata?.openapi?.operationId,
security: [
{
bearerAuth: [],
},
],
parameters: getParameters(options),
...(body
? { requestBody: body }
: {
requestBody: {
//set body none
content: {
"application/json": {
schema: {
type: "object",
properties: {},
},
},
},
},
}),
responses: getResponse(options.metadata?.openapi?.responses),
},
};
}
});
for (const plugin of options.plugins || []) {
if (plugin.id === "open-api") {
continue;
}
const pluginEndpoints = getEndpoints(ctx, {
...options,
plugins: [plugin],
});
const api = Object.keys(pluginEndpoints.api)
.map((key) => {
if (
baseEndpoints.api[key as keyof typeof baseEndpoints.api] === undefined
) {
return pluginEndpoints.api[key as keyof typeof pluginEndpoints.api];
}
return null;
})
.filter((x) => x !== null) as Endpoint[];
Object.entries(api).forEach(([key, value]) => {
const options = value.options as EndpointOptions;
if (options.metadata?.SERVER_ONLY) return;
if (options.method === "GET") {
paths[value.path] = {
get: {
tags: options.metadata?.openapi?.tags || [
plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1),
],
description: options.metadata?.openapi?.description,
operationId: options.metadata?.openapi?.operationId,
security: [
{
bearerAuth: [],
},
],
parameters: getParameters(options),
responses: getResponse(options.metadata?.openapi?.responses),
},
};
}
if (options.method === "POST") {
paths[value.path] = {
post: {
tags: options.metadata?.openapi?.tags || [
plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1),
],
description: options.metadata?.openapi?.description,
operationId: options.metadata?.openapi?.operationId,
security: [
{
bearerAuth: [],
},
],
parameters: getParameters(options),
requestBody: getRequestBody(options),
responses: getResponse(options.metadata?.openapi?.responses),
},
};
}
});
}
const res = {
openapi: "3.1.1",
info: {
title: "Better Auth",
description: "API Reference for your Better Auth Instance",
},
components,
security: [
{
apiKeyCookie: [],
},
],
servers: [
{
url: ctx.baseURL,
},
],
tags: [
{
name: "Default",
description:
"Default endpoints that are included with Better Auth by default. These endpoints are not part of any plugin.",
},
],
paths,
};
return res;
}

View File

@@ -1,7 +1,7 @@
import type { BetterAuthPlugin } from "better-auth";
import { createAuthEndpoint } from "better-auth/plugins";
import { getEndpoints } from "better-auth/api";
import { generator } from "./generator";
import { logo } from "./logo";
const getHTML = (apiReference: Record<string, any>) => `<!doctype html>
<html>
@@ -18,7 +18,20 @@ const getHTML = (apiReference: Record<string, any>) => `<!doctype html>
type="application/json">
${JSON.stringify(apiReference)}
</script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
<script>
var configuration = {
favicon: "data:image/svg+xml;utf8,${encodeURIComponent(logo)}",
theme: "saturn",
metaData: {
title: "Better Auth API",
description: "API Reference for your Better Auth Instance",
}
}
document.getElementById('api-reference').dataset.configuration =
JSON.stringify(configuration)
</script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>`;
@@ -27,7 +40,7 @@ export const openAPI = () => {
id: "open-api",
endpoints: {
openAPI: createAuthEndpoint(
"/api-reference",
"/reference",
{
method: "GET",
},

File diff suppressed because one or more lines are too long

View File

@@ -260,6 +260,48 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
}>;
}>,
use: [orgSessionMiddleware],
metadata: {
openapi: {
description: "Check if the user has permission",
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
permission: {
type: "object",
description: "The permission to check",
},
},
required: ["permission"],
},
},
},
},
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
error: {
type: "string",
},
success: {
type: "boolean",
},
},
required: ["success"],
},
},
},
},
},
},
},
},
async (ctx) => {
if (!ctx.context.session.session.activeOrganizationId) {

View File

@@ -16,11 +16,73 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
method: "POST",
use: [orgMiddleware, orgSessionMiddleware],
body: z.object({
email: z.string(),
role: z.string() as unknown as InferRolesFromOption<O>,
organizationId: z.string().optional(),
resend: z.boolean().optional(),
email: z.string({
description: "The email address of the user to invite",
}),
role: z.string({
description: "The role to assign to the user",
}) as unknown as InferRolesFromOption<O>,
organizationId: z
.string({
description: "The organization ID to invite the user to",
})
.optional(),
resend: z
.boolean({
description:
"Resend the invitation email, if the user is already invited",
})
.optional(),
}),
metadata: {
openapi: {
description: "Invite a user to an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
id: {
type: "string",
},
email: {
type: "string",
},
role: {
type: "string",
},
organizationId: {
type: "string",
},
inviterId: {
type: "string",
},
status: {
type: "string",
},
expiresAt: {
type: "string",
},
},
required: [
"id",
"email",
"role",
"organizationId",
"inviterId",
"status",
"expiresAt",
],
},
},
},
},
},
},
},
},
async (ctx) => {
if (!ctx.context.orgOptions.sendInvitationEmail) {
@@ -121,9 +183,36 @@ export const acceptInvitation = createAuthEndpoint(
{
method: "POST",
body: z.object({
invitationId: z.string(),
invitationId: z.string({
description: "The ID of the invitation to accept",
}),
}),
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
openapi: {
description: "Accept an invitation to an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
invitation: {
type: "object",
},
member: {
type: "object",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
const session = ctx.context.session;
@@ -176,9 +265,36 @@ export const rejectInvitation = createAuthEndpoint(
{
method: "POST",
body: z.object({
invitationId: z.string(),
invitationId: z.string({
description: "The ID of the invitation to reject",
}),
}),
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
openapi: {
description: "Reject an invitation to an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
invitation: {
type: "object",
},
member: {
type: "null",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
const session = ctx.context.session;
@@ -214,9 +330,31 @@ export const cancelInvitation = createAuthEndpoint(
{
method: "POST",
body: z.object({
invitationId: z.string(),
invitationId: z.string({
description: "The ID of the invitation to cancel",
}),
}),
use: [orgMiddleware, orgSessionMiddleware],
openapi: {
description: "Cancel an invitation to an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
invitation: {
type: "object",
},
},
},
},
},
},
},
},
},
async (ctx) => {
const session = ctx.context.session;
@@ -259,8 +397,71 @@ export const getInvitation = createAuthEndpoint(
use: [orgMiddleware],
requireHeaders: true,
query: z.object({
id: z.string(),
id: z.string({
description: "The ID of the invitation to get",
}),
}),
metadata: {
openapi: {
description: "Get an invitation by ID",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
id: {
type: "string",
},
email: {
type: "string",
},
role: {
type: "string",
},
organizationId: {
type: "string",
},
inviterId: {
type: "string",
},
status: {
type: "string",
},
expiresAt: {
type: "string",
},
organizationName: {
type: "string",
},
organizationSlug: {
type: "string",
},
inviterEmail: {
type: "string",
},
},
required: [
"id",
"email",
"role",
"organizationId",
"inviterId",
"status",
"expiresAt",
"organizationName",
"organizationSlug",
"inviterEmail",
],
},
},
},
},
},
},
},
},
async (ctx) => {
const session = await getSessionFromCtx(ctx);

View File

@@ -11,13 +11,58 @@ export const removeMember = createAuthEndpoint(
{
method: "POST",
body: z.object({
memberIdOrEmail: z.string(),
memberIdOrEmail: z.string({
description: "The ID or email of the member to remove",
}),
/**
* If not provided, the active organization will be used
*/
organizationId: z.string().optional(),
organizationId: z
.string({
description:
"The ID of the organization to remove the member from. If not provided, the active organization will be used",
})
.optional(),
}),
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
openapi: {
description: "Remove a member from an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
member: {
type: "object",
properties: {
id: {
type: "string",
},
userId: {
type: "string",
},
organizationId: {
type: "string",
},
role: {
type: "string",
},
},
required: ["id", "userId", "organizationId", "role"],
},
},
required: ["member"],
},
},
},
},
},
},
},
},
async (ctx) => {
const session = ctx.context.session;
@@ -110,6 +155,44 @@ export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
organizationId: z.string().optional(),
}),
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
openapi: {
description: "Update the role of a member in an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
member: {
type: "object",
properties: {
id: {
type: "string",
},
userId: {
type: "string",
},
organizationId: {
type: "string",
},
role: {
type: "string",
},
},
required: ["id", "userId", "organizationId", "role"],
},
},
required: ["member"],
},
},
},
},
},
},
},
},
async (ctx) => {
const session = ctx.context.session;
@@ -184,6 +267,38 @@ export const getActiveMember = createAuthEndpoint(
{
method: "GET",
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
openapi: {
description: "Get the active member in the organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
id: {
type: "string",
},
userId: {
type: "string",
},
organizationId: {
type: "string",
},
role: {
type: "string",
},
},
required: ["id", "userId", "organizationId", "role"],
},
},
},
},
},
},
},
},
async (ctx) => {
const session = ctx.context.session;

View File

@@ -11,13 +11,49 @@ export const createOrganization = createAuthEndpoint(
{
method: "POST",
body: z.object({
name: z.string(),
slug: z.string(),
userId: z.string().optional(),
logo: z.string().optional(),
metadata: z.record(z.string(), z.any()).optional(),
name: z.string({
description: "The name of the organization",
}),
slug: z.string({
description: "The slug of the organization",
}),
userId: z
.string({
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.",
})
.optional(),
logo: z
.string({
description: "The logo of the organization",
})
.optional(),
metadata: z
.record(z.string(), z.any(), {
description: "The metadata of the organization",
})
.optional(),
}),
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
openapi: {
description: "Create an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
description: "The organization that was created",
$ref: "#/components/schemas/Organization",
},
},
},
},
},
},
},
},
async (ctx) => {
const user = ctx.context.session.user;
@@ -89,15 +125,46 @@ export const updateOrganization = createAuthEndpoint(
body: z.object({
data: z
.object({
name: z.string().optional(),
slug: z.string().optional(),
logo: z.string().optional(),
name: z
.string({
description: "The name of the organization",
})
.optional(),
slug: z
.string({
description: "The slug of the organization",
})
.optional(),
logo: z
.string({
description: "The logo of the organization",
})
.optional(),
})
.partial(),
organizationId: z.string().optional(),
}),
requireHeaders: true,
use: [orgMiddleware],
metadata: {
openapi: {
description: "Update an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
description: "The updated organization",
$ref: "#/components/schemas/Organization",
},
},
},
},
},
},
},
},
async (ctx) => {
const session = await ctx.context.getSession(ctx);
@@ -162,10 +229,30 @@ export const deleteOrganization = createAuthEndpoint(
{
method: "POST",
body: z.object({
organizationId: z.string(),
organizationId: z.string({
description: "The organization id to delete",
}),
}),
requireHeaders: true,
use: [orgMiddleware],
metadata: {
openapi: {
description: "Delete an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "string",
description: "The organization id that was deleted",
},
},
},
},
},
},
},
},
async (ctx) => {
const session = await ctx.context.getSession(ctx);
@@ -230,11 +317,34 @@ export const getFullOrganization = createAuthEndpoint(
method: "GET",
query: z.optional(
z.object({
organizationId: z.string().optional(),
organizationId: z
.string({
description: "The organization id to get",
})
.optional(),
}),
),
requireHeaders: true,
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
openapi: {
description: "Get the full organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
description: "The organization",
$ref: "#/components/schemas/Organization",
},
},
},
},
},
},
},
},
async (ctx) => {
const session = ctx.context.session;
@@ -261,9 +371,34 @@ export const setActiveOrganization = createAuthEndpoint(
{
method: "POST",
body: z.object({
organizationId: z.string().nullable().optional(),
organizationId: z
.string({
description:
"The organization id to set as active. Can be null to unset the active organization",
})
.nullable()
.optional(),
}),
use: [orgSessionMiddleware, orgMiddleware],
metadata: {
openapi: {
description: "Set the active organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
description: "The organization",
$ref: "#/components/schemas/Organization",
},
},
},
},
},
},
},
},
async (ctx) => {
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
@@ -319,6 +454,26 @@ export const listOrganizations = createAuthEndpoint(
{
method: "GET",
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
openapi: {
description: "List all organizations",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "array",
items: {
$ref: "#/components/schemas/Organization",
},
},
},
},
},
},
},
},
},
async (ctx) => {
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);

View File

@@ -1,28 +0,0 @@
{
"name": "@better-auth/open-api",
"version": "1.0.0-canary.12",
"description": "",
"main": "dist/index.js",
"module": "dist/index.mjs",
"scripts": {
"test": "vitest",
"build": "tsup --dts --minify --clean",
"dev": "tsup --watch --sourcemap --dts"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"better-auth": "workspace:*"
},
"dependencies": {
"better-call": "^0.3.0"
}
}

View File

@@ -1,227 +0,0 @@
import type { Endpoint, EndpointOptions } from "better-call";
import { ZodObject, ZodSchema } from "zod";
import type { OpenAPISchemaType, OpenAPIParameter } from "better-call";
import type { AuthContext, BetterAuthOptions } from "better-auth";
import { getEndpoints } from "better-auth/api";
interface Path {
get?: {
tags?: string[];
operationId?: string;
security?: [{ bearerAuth: string[] }];
parameters?: OpenAPIParameter[];
responses?: {
[key in string]: {
description?: string;
content: {
"application/json": {
schema: {
type?: OpenAPISchemaType;
properties?: Record<string, any>;
required?: string[];
$ref?: string;
};
};
};
};
};
};
}
const paths: Record<string, Path> = {};
function getTypeFromZodType(zodType: ZodSchema) {
switch (zodType.constructor.name) {
case "ZodString":
return "string";
case "ZodNumber":
return "number";
case "ZodBoolean":
return "boolean";
case "ZodObject":
return "object";
case "ZodArray":
return "array";
default:
return "string";
}
}
function getParameters(options: EndpointOptions) {
const parameters: OpenAPIParameter[] = [];
if (options.metadata?.openapi?.parameters) {
parameters.push(...options.metadata.openapi.parameters);
return parameters;
}
if (options.query instanceof ZodObject) {
Object.entries(options.query.shape).forEach(([key, value]) => {
if (value instanceof ZodSchema) {
parameters.push({
name: key,
in: "query",
schema: {
type: getTypeFromZodType(value),
...("minLength" in value && value.minLength
? {
minLength: value.minLength as number,
}
: {}),
description: value.description,
},
});
}
});
}
return parameters;
}
export async function generator(ctx: AuthContext, options: BetterAuthOptions) {
const baseEndpoints = getEndpoints(ctx, {
...options,
plugins: [],
});
Object.entries(baseEndpoints.api).forEach(([_, value]) => {
const options = value.options as EndpointOptions;
if (options.method === "GET") {
paths[value.path] = {
get: {
tags: ["core", ...(options.metadata?.openapi?.tags || [])],
operationId: options.metadata?.openapi?.operationId,
security: [
{
bearerAuth: [],
},
],
parameters: getParameters(options),
responses: options.metadata?.openapi?.responses || {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
},
},
required: ["message"],
},
},
},
},
"400": {
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
},
},
required: ["message"],
},
},
},
},
},
},
};
}
});
for (const plugin of options.plugins || []) {
const pluginEndpoints = getEndpoints(ctx, {
...options,
plugins: [plugin],
});
const api = Object.keys(pluginEndpoints.api)
.map((key) => {
if (
baseEndpoints.api[key as keyof typeof baseEndpoints.api] === undefined
) {
return pluginEndpoints.api[key as keyof typeof pluginEndpoints.api];
}
return null;
})
.filter((x) => x !== null) as Endpoint[];
Object.entries(api).forEach(([key, value]) => {
const options = value.options as EndpointOptions;
if (options.method === "GET") {
paths[value.path] = {
get: {
tags: options.metadata?.openapi?.tags || [plugin.id],
operationId: options.metadata?.openapi?.operationId,
security: [
{
bearerAuth: [],
},
],
parameters: getParameters(options),
responses: options.metadata?.openapi?.responses || {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
},
},
required: ["message"],
},
},
},
},
"400": {
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
},
},
required: ["message"],
},
},
},
},
},
},
};
}
});
}
const res = {
openapi: "3.1.1",
info: {
title: "Better Auth Api",
description: "API Reference for your Better Auth Instance",
},
security: [
{
apiKeyCookie: [],
},
],
servers: [
{
url: ctx.baseURL,
},
],
tags: [
{
name: "Authentication",
description:
"Some endpoints are public, but some require authentication. We provide all the required endpoints to create an account and authorize yourself.",
},
],
paths,
};
return res;
}

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"module": "ESNext",
"noEmit": true,
"moduleResolution": "Bundler",
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true
},
"exclude": ["node_modules"],
"include": ["src"]
}

View File

@@ -1,11 +0,0 @@
import { defineConfig } from "tsup";
export default defineConfig((env) => {
return {
entry: ["src/index.ts"],
format: ["esm", "cjs"],
bundle: true,
skipNodeModulesBundle: true,
external: ["better-call", "better-auth"],
};
});

34
pnpm-lock.yaml generated
View File

@@ -44,9 +44,6 @@ importers:
demo/nextjs:
dependencies:
'@better-auth/open-api':
specifier: workspace:1.0.0-canary.12
version: link:../../packages/open-api
'@better-fetch/fetch':
specifier: 1.1.12
version: 1.1.12
@@ -1350,8 +1347,8 @@ importers:
specifier: ^10.0.1
version: 10.0.1(encoding@0.1.13)
better-call:
specifier: 0.3.1
version: 0.3.1
specifier: 0.3.2
version: 0.3.2
consola:
specifier: ^3.2.3
version: 3.2.3
@@ -1560,16 +1557,6 @@ importers:
specifier: ^1.6.0
version: 1.6.0(@types/node@22.8.6)(happy-dom@15.8.0)(lightningcss@1.27.0)(terser@5.36.0)
packages/open-api:
dependencies:
better-call:
specifier: ^0.3.0
version: 0.3.0
devDependencies:
better-auth:
specifier: workspace:*
version: link:../better-auth
packages:
'@algolia/cache-browser-local-storage@4.24.0':
@@ -8109,11 +8096,8 @@ packages:
better-call@0.2.3-beta.2:
resolution: {integrity: sha512-ybOtGcR4pOsHI2XE+urR9zcmK+s0YnhJSx8KDj6ul7MUEyYOiMEnq/bylyH62/7qXuYb9q8Oqkp9NF9vWOZ4Mg==}
better-call@0.3.0:
resolution: {integrity: sha512-nBWeQl+O1NCPMkb958VQzVn0AfSWwILGoLCpLbTzz3p0QnsqFYSTe6z8Uzo4p/6Iah8zrRBoLBQvFLUFl80jxA==}
better-call@0.3.1:
resolution: {integrity: sha512-uJoTAVLHCIrRebBxu2rQ/CPpf7r1c7FrfFIL2OS5dVx1z/64nGMZ5sH2GUtYu22XRjbOGEn7KI9eNkLpIo41CQ==}
better-call@0.3.2:
resolution: {integrity: sha512-ZYUADh4S4JF3QOIgC0QAm4N9Ifb7v3rOEcwkIxr3UXxd+GCrGmEzv7VDe35o1ldX0Zcb9CX3QfT0jdJoMIKA9w==}
better-opn@3.0.2:
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
@@ -25761,15 +25745,7 @@ snapshots:
set-cookie-parser: 2.7.1
typescript: 5.6.3
better-call@0.3.0:
dependencies:
'@better-fetch/fetch': 1.1.12
rou3: 0.5.1
set-cookie-parser: 2.7.1
uncrypto: 0.1.3
zod: 3.23.8
better-call@0.3.1:
better-call@0.3.2:
dependencies:
'@better-fetch/fetch': 1.1.12
rou3: 0.5.1