mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-11 04:19:31 +00:00
docs: org and imporvements
This commit is contained in:
@@ -2,12 +2,12 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { CheckIcon, XIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -18,161 +18,169 @@ import { InvitationError } from "./invitation-error";
|
||||
import { Invitation } from "@/lib/auth-types";
|
||||
|
||||
export default function InvitationPage({
|
||||
params,
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const [invitationStatus, setInvitationStatus] = useState<
|
||||
"pending" | "accepted" | "rejected"
|
||||
>("pending");
|
||||
const router = useRouter();
|
||||
const [invitationStatus, setInvitationStatus] = useState<
|
||||
"pending" | "accepted" | "rejected"
|
||||
>("pending");
|
||||
|
||||
const handleAccept = async () => {
|
||||
await organization.acceptInvitation({
|
||||
invitationId: params.id,
|
||||
}).then((res) => {
|
||||
if (res.error) {
|
||||
setError(res.error.message || "An error occurred")
|
||||
} else {
|
||||
setInvitationStatus("accepted");
|
||||
router.push(`/dashboard`)
|
||||
}
|
||||
})
|
||||
};
|
||||
const handleAccept = async () => {
|
||||
await organization
|
||||
.acceptInvitation({
|
||||
invitationId: params.id,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
setError(res.error.message || "An error occurred");
|
||||
} else {
|
||||
setInvitationStatus("accepted");
|
||||
router.push(`/dashboard`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleReject = async () => {
|
||||
await organization.rejectInvitation({
|
||||
invitationId: params.id
|
||||
}).then((res) => {
|
||||
if (res.error) {
|
||||
setError(res.error.message || "An error occurred")
|
||||
} else {
|
||||
setInvitationStatus("rejected");
|
||||
}
|
||||
})
|
||||
};
|
||||
const handleReject = async () => {
|
||||
await organization
|
||||
.rejectInvitation({
|
||||
invitationId: params.id,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
setError(res.error.message || "An error occurred");
|
||||
} else {
|
||||
setInvitationStatus("rejected");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const [invitation, setInvitation] = useState<{
|
||||
organizationName: string;
|
||||
organizationSlug: string;
|
||||
inviterEmail: string;
|
||||
id: string;
|
||||
status: "pending" | "accepted" | "rejected" | "canceled";
|
||||
email: string;
|
||||
expiresAt: Date;
|
||||
organizationId: string;
|
||||
role: "member" | "admin" | "owner";
|
||||
inviterId: string;
|
||||
} | null>(null);
|
||||
const [invitation, setInvitation] = useState<{
|
||||
organizationName: string;
|
||||
organizationSlug: string;
|
||||
inviterEmail: string;
|
||||
id: string;
|
||||
status: "pending" | "accepted" | "rejected" | "canceled";
|
||||
email: string;
|
||||
expiresAt: Date;
|
||||
organizationId: string;
|
||||
role: "member" | "admin" | "owner";
|
||||
inviterId: string;
|
||||
} | null>(null);
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
client.organization.getActiveInvitation({
|
||||
query: {
|
||||
id: params.id
|
||||
}
|
||||
}).then((res) => {
|
||||
if (res.error) {
|
||||
setError(res.error.message || "An error occurred")
|
||||
} else {
|
||||
setInvitation(res.data)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
client.organization
|
||||
.getInvitation({
|
||||
query: {
|
||||
id: params.id,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
setError(res.error.message || "An error occurred");
|
||||
} else {
|
||||
setInvitation(res.data);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-[80vh] flex items-center justify-center">
|
||||
<div className="absolute pointer-events-none inset-0 flex items-center justify-center dark:bg-black bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)]"></div>
|
||||
{
|
||||
invitation ? <Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Organization Invitation</CardTitle>
|
||||
<CardDescription>
|
||||
You've been invited to join an organization
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{invitationStatus === "pending" && (
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
<strong>{invitation?.inviterEmail}</strong> has invited you
|
||||
to join <strong>{invitation?.organizationName}</strong>.
|
||||
</p>
|
||||
<p>
|
||||
This invitation was sent to{" "}
|
||||
<strong>{invitation?.email}</strong>.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{invitationStatus === "accepted" && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-center w-16 h-16 mx-auto bg-green-100 rounded-full">
|
||||
<CheckIcon className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-center">
|
||||
Welcome to {invitation?.organizationName}!
|
||||
</h2>
|
||||
<p className="text-center">
|
||||
You've successfully joined the organization. We're excited to
|
||||
have you on board!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{invitationStatus === "rejected" && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-center w-16 h-16 mx-auto bg-red-100 rounded-full">
|
||||
<XIcon className="w-8 h-8 text-red-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-center">
|
||||
Invitation Declined
|
||||
</h2>
|
||||
<p className="text-center">
|
||||
You‘ve declined the invitation to join{" "}
|
||||
{invitation?.organizationName}.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
{invitationStatus === "pending" && (
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline" onClick={handleReject}>
|
||||
Decline
|
||||
</Button>
|
||||
<Button onClick={handleAccept}>Accept Invitation</Button>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card> : error ? <InvitationError /> : <InvitationSkeleton />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="min-h-[80vh] flex items-center justify-center">
|
||||
<div className="absolute pointer-events-none inset-0 flex items-center justify-center dark:bg-black bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)]"></div>
|
||||
{invitation ? (
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Organization Invitation</CardTitle>
|
||||
<CardDescription>
|
||||
You've been invited to join an organization
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{invitationStatus === "pending" && (
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
<strong>{invitation?.inviterEmail}</strong> has invited you to
|
||||
join <strong>{invitation?.organizationName}</strong>.
|
||||
</p>
|
||||
<p>
|
||||
This invitation was sent to{" "}
|
||||
<strong>{invitation?.email}</strong>.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{invitationStatus === "accepted" && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-center w-16 h-16 mx-auto bg-green-100 rounded-full">
|
||||
<CheckIcon className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-center">
|
||||
Welcome to {invitation?.organizationName}!
|
||||
</h2>
|
||||
<p className="text-center">
|
||||
You've successfully joined the organization. We're excited to
|
||||
have you on board!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{invitationStatus === "rejected" && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-center w-16 h-16 mx-auto bg-red-100 rounded-full">
|
||||
<XIcon className="w-8 h-8 text-red-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-center">
|
||||
Invitation Declined
|
||||
</h2>
|
||||
<p className="text-center">
|
||||
You‘ve declined the invitation to join{" "}
|
||||
{invitation?.organizationName}.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
{invitationStatus === "pending" && (
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline" onClick={handleReject}>
|
||||
Decline
|
||||
</Button>
|
||||
<Button onClick={handleAccept}>Accept Invitation</Button>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
) : error ? (
|
||||
<InvitationError />
|
||||
) : (
|
||||
<InvitationSkeleton />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function InvitationSkeleton() {
|
||||
return (
|
||||
<Card className="w-full max-w-md mx-auto">
|
||||
<CardHeader>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Skeleton className="w-6 h-6 rounded-full" />
|
||||
<Skeleton className="h-6 w-24" />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-full mt-2" />
|
||||
<Skeleton className="h-4 w-2/3 mt-2" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end">
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Card className="w-full max-w-md mx-auto">
|
||||
<CardHeader>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Skeleton className="w-6 h-6 rounded-full" />
|
||||
<Skeleton className="h-6 w-24" />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-full mt-2" />
|
||||
<Skeleton className="h-4 w-2/3 mt-2" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end">
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-auth": "^0.0.4",
|
||||
"better-auth": "workspace:*",
|
||||
"nuxt": "^3.13.0",
|
||||
"vue": "latest"
|
||||
}
|
||||
|
||||
29
dev/nuxtjs/pages/organization.vue
Normal file
29
dev/nuxtjs/pages/organization.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">;
|
||||
export default {
|
||||
setup() {
|
||||
const organizations = client.useListOrganizations();
|
||||
const activeOrganization = client.useActiveOrganization();
|
||||
return { organizations, activeOrganization };
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Organizations</h1>
|
||||
<div v-if="organizations.isPending">Loading...</div>
|
||||
<div v-else-if="organizations.data === null">No organizations found.</div>
|
||||
<ul v-else>
|
||||
<li v-for="organization in organizations.data" :key="organization.id">
|
||||
{{ organization.name }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Active organization</h2>
|
||||
<div v-if="activeOrganization.isPending">Loading...</div>
|
||||
<div v-else-if="activeOrganization.data === null">No active organization.</div>
|
||||
<div v-else>
|
||||
{{ activeOrganization.data.name }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createAuthClient } from "better-auth/vue";
|
||||
import { organizationClient } from "better-auth/client";
|
||||
import { organizationClient } from "better-auth/client/plugins";
|
||||
export const client = createAuthClient({
|
||||
plugins: [organizationClient()],
|
||||
});
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"better-auth": "^0.0.4"
|
||||
"better-auth": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createAuthClient } from "better-auth/svelte";
|
||||
import { organizationClient } from "better-auth/client";
|
||||
import { organizationClient } from "better-auth/client/plugins";
|
||||
|
||||
export const client = createAuthClient({
|
||||
baseURL: "http://localhost:3000/api/auth",
|
||||
plugins: [organizationClient()],
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { client } from "$lib/client";
|
||||
import { client } from "$lib/client";
|
||||
|
||||
export let data;
|
||||
const organizations = client.useListOrganizations();
|
||||
const activeOrganization = client.useActiveOrganization();
|
||||
</script>
|
||||
|
||||
<h1>Organizations</h1>
|
||||
|
||||
{#if $organizations.isPending}
|
||||
<p>Loading...</p>
|
||||
{:else if $organizations.data === null}
|
||||
<p>No organizations found.</p>
|
||||
{:else}
|
||||
<ul>
|
||||
{#each $organizations.data as organization}
|
||||
<li>{organization.name}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<h2>Active Organization</h2>
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
---
|
||||
title: Email & Password
|
||||
description: Implementing email and password authentication with Better Auth
|
||||
---
|
||||
|
||||
Email and password authentication is a common method used by many applications. Better Auth provides a built-in email and password authenticator that you can easily integrate into your project.
|
||||
|
||||
<Callout type="info">
|
||||
If you prefer username-based authentication, check out the <Link href="/docs/plugins/username">username plugin</Link>. It extends the email and password authenticator with username support.
|
||||
</Callout>
|
||||
|
||||
## Setup
|
||||
|
||||
To enable email and password authentication, add the following configuration to your Better Auth instance:
|
||||
|
||||
```ts title="auth.ts" twoslash
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
emailAndPassword: { // [!code highlight]
|
||||
enabled: true // [!code highlight]
|
||||
} // [!code highlight]
|
||||
})
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
If it's not enabled, it'll not allow you to sign in or sign up with email and password.
|
||||
</Callout>
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### Signup
|
||||
|
||||
To signup a user, you can use the `signUp.email` function provided by the client. The `signUp` function takes an object with the following properties:
|
||||
|
||||
- `email`: The email address of the user.
|
||||
- `password`: The password of the user. It should be at least 8 characters long and max 32 by default.
|
||||
- `name`: The name of the user.
|
||||
- `image`: The image of the user. (optional)
|
||||
- `callbackURL`: The url to redirect to after the user has signed up. (optional)
|
||||
- `dontRememberMe`: If true, the user will be signed out when the browser is closed. (optional)
|
||||
|
||||
```ts title="client.ts" twoslash /
|
||||
/**
|
||||
* Make sure to import the client for your framework
|
||||
*/
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
|
||||
const client = createAuthClient()
|
||||
|
||||
const signup = async () => {
|
||||
const data = await client.signUp.email({
|
||||
email: "test@example.com",
|
||||
password: "password1234",
|
||||
name: "test",
|
||||
image: "https://example.com/image.png",
|
||||
callbackURL: "/"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The function returns a promise that resolves an object with `data` and `error` properties. The `data` property contains the user object that was created, and the `error` property contains any error that occurred during the signup process.
|
||||
|
||||
<Callout type="info">
|
||||
Hover over the `data` object to see the shape of the response.
|
||||
</Callout>
|
||||
|
||||
### Signin
|
||||
|
||||
To signin a user, you can use the `signIn.email` function provided by the client. The `signIn` function takes an object with the following properties:
|
||||
|
||||
- `email`: The email address of the user.
|
||||
- `password`: The password of the user.
|
||||
- `callbackURL`: The url to redirect to after the user has signed in. (optional)
|
||||
- `dontRememberMe`: If true, the user will be signed out when the browser is closed. (optional)
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
|
||||
const client = createAuthClient()
|
||||
|
||||
const signin = async () => {
|
||||
const data = await client.signIn.email({
|
||||
email: "test@example.com",
|
||||
password: "password1234",
|
||||
callbackURL: "/"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Email Verification
|
||||
|
||||
To enable email verification, you need to configure the email and password authenticator. You can do this by adding the following code to your better auth instance:
|
||||
|
||||
```ts title="auth.ts" twoslash
|
||||
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
// ---cut-start---
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite"
|
||||
},
|
||||
// ---cut-end---
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
async sendVerificationEmail(email, url){
|
||||
// send email to user.
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
on the client side you can use `sendVerificationEmail` function to send verification link to user.
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
const client = createAuthClient()
|
||||
// ---cut---
|
||||
const verifyEmail = async () => {
|
||||
const data = await client.sendVerificationEmail({
|
||||
email: "test@example.com",
|
||||
callbackURL: "/"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Password Reset
|
||||
|
||||
|
||||
to reset a password first you need to provider `sendResetPasswordToken` function to the email and password authenticator. The `sendResetPasswordToken` function takes an object with the following properties:
|
||||
|
||||
- `user`: The user object.
|
||||
- `token`: The token that was generated.
|
||||
|
||||
```ts title="auth.ts" twoslash
|
||||
async function sendResetEmail(email: string, url: string){
|
||||
// send email to user
|
||||
}
|
||||
// ---cut---
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
emailAndPassword: { // [!code highlight]
|
||||
enabled: true, // [!code highlight]
|
||||
async sendResetPasswordToken(token, user) { // [!code highlight]
|
||||
// send email to user // [!code highlight]
|
||||
const url = `https://example.com/reset-password?token=${token}` // [!code highlight]
|
||||
await sendResetEmail(user.email, url) //your function to send email to user // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
} // [!code highlight]
|
||||
})
|
||||
```
|
||||
|
||||
once you configured your server you can call `forgetPassword` function to send reset password link to user.
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
const client = createAuthClient()
|
||||
// ---cut---
|
||||
const forgetPassword = async () => {
|
||||
const data = await client.forgetPassword({
|
||||
email: "test@example.com",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
When user click on the link in the email he 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:
|
||||
|
||||
- `token`: The token that was generated.
|
||||
- `newPassword`: The new password of the user.
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
|
||||
const client = createAuthClient()
|
||||
function useSearchParams() {
|
||||
return {
|
||||
get: (key: string) => "" as string | null,
|
||||
}
|
||||
}
|
||||
// ---cut---
|
||||
const token = useSearchParams().get("token") // get token from url
|
||||
const resetPassword = async () => {
|
||||
if(!token) return
|
||||
const data = await client.resetPassword({
|
||||
token: token,
|
||||
newPassword: "password1234",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
enabled: {
|
||||
description:
|
||||
'Enable email and password authentication',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
minPasswordLength: {
|
||||
description: 'The minimum length of the password.',
|
||||
type: 'number',
|
||||
default: 8,
|
||||
},
|
||||
maxPasswordLength: {
|
||||
description: 'The maximum length of the password.',
|
||||
type: 'number',
|
||||
default: 32,
|
||||
},
|
||||
sendResetPasswordToken: {
|
||||
description: 'send reset password email. It takes a functions that takes two parameters: token and user.',
|
||||
type: 'function',
|
||||
},
|
||||
sendVerificationEmail: {
|
||||
description: 'send verification email. It takes a functions that takes two parameters: email and url.',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -1,411 +0,0 @@
|
||||
---
|
||||
title: Basic Usage
|
||||
description: Getting started with Better Auth
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
Better Auth provides built-in authentication support for:
|
||||
|
||||
- **Email and password**
|
||||
- **Social provider (Google, Github, Apple, and more)**
|
||||
|
||||
You can extend authentication options using plugins, such as: Username-based login, Passkeys, Email magic links, and more.
|
||||
|
||||
### Email and Password Authentication
|
||||
|
||||
To enable email and password authentication:
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = betterAuth({
|
||||
//...rest of the options
|
||||
emailAndPassword: { // [!code highlight]
|
||||
enabled: true // [!code highlight]
|
||||
} // [!code highlight]
|
||||
})
|
||||
```
|
||||
|
||||
### Signup
|
||||
|
||||
To signup a user, you can use the `signUp.email` function provided by the client. The `signUp` function takes an object with the following properties:
|
||||
|
||||
```ts title="signup.ts" twoslash
|
||||
// @filename: client.ts
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
|
||||
export const client = createAuthClient()
|
||||
|
||||
// ---cut---
|
||||
// @filename: signup.ts
|
||||
// ---cut---
|
||||
import { client } from "./client";
|
||||
|
||||
const res = await client.signUp.email({
|
||||
email: "test@example.com", // The email address of the user.
|
||||
password: "password1234", // The password of the user.
|
||||
name: "test", // The name of the user.
|
||||
image: "https://example.com/image.png", // The image of the user. (optional)
|
||||
callbackURL: "/" // The url to redirect to after the user has signed up. (optional)
|
||||
})
|
||||
```
|
||||
|
||||
The function returns a promise that resolves an object with `data` and `error` properties. The `data` property contains the user object that was created, and the `error` property contains any error that occurred during the signup process.
|
||||
|
||||
<Callout type="info">
|
||||
If you want to use username instead of email, you can use <Link href="/docs/plugins/username">username Plugin</Link>.
|
||||
</Callout>
|
||||
|
||||
### Signin
|
||||
|
||||
To signin a user, you can use the `signIn.email` function provided by the client. The `signIn` function takes an object with the following properties:
|
||||
|
||||
```ts title="client.ts" twoslash /
|
||||
// @filename: client.ts
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
|
||||
export const client = createAuthClient()
|
||||
|
||||
// ---cut---
|
||||
// @filename: signup.ts
|
||||
// ---cut---
|
||||
import { client } from "./client";
|
||||
|
||||
const data = await client.signIn.email({
|
||||
email: "test@example.com", // The email address of the user.
|
||||
password: "password1234", // The password of the user.
|
||||
callbackURL: "/" // The url to redirect to after the user has signed in. (optional)
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### Authentication with Social Providers
|
||||
|
||||
Better Auth supports multiple social providers, including Google, Github, Apple, Discord, and more. To use a social provider, you need to configure the ones you need in the `socialProvider` option on your `auth` object.
|
||||
|
||||
### Configure Social Providers
|
||||
|
||||
To configure social providers, you need to import the provider you want to use and pass it to the `socialProvider` option. For example, to configure the Github provider, you can use the following code:
|
||||
|
||||
```ts title="auth.ts" twoslash
|
||||
//@ts-expect-error
|
||||
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID as string
|
||||
//@ts-expect-error
|
||||
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET as string
|
||||
|
||||
// ---cut---
|
||||
|
||||
import { betterAuth } from "better-auth"
|
||||
import { github } from "better-auth/social-providers"
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
socialProvider: [ // [!code highlight]
|
||||
github({ // [!code highlight]
|
||||
clientId: GITHUB_CLIENT_ID, // [!code highlight]
|
||||
clientSecret: GITHUB_CLIENT_SECRET, // [!code highlight]
|
||||
}), // [!code highlight]
|
||||
], // [!code highlight]
|
||||
})
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
See the <Link href="/docs/providers">Provider</Link> section for more information on how to configure each provider.
|
||||
</Callout>
|
||||
|
||||
### Signin with social providers
|
||||
|
||||
```ts title="signin.ts" twoslash
|
||||
// @filename: client.ts
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
export const client = createAuthClient()
|
||||
|
||||
// ---cut---
|
||||
// @filename: signin.ts
|
||||
import { client } from "./client"
|
||||
|
||||
const signin = async () => {
|
||||
const data = await client.signIn.social({
|
||||
provider: "github"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Session
|
||||
|
||||
Once a user is signed in, you'll want to access their session. Better auth allows you easily to access the session data from the server and client side.
|
||||
|
||||
### Client Side
|
||||
|
||||
Better Auth provides a `useSession` hook to easily access session data on the client side. This hook is implemented in a reactive way for each supported framework, ensuring that any changes to the session (such as signing out) are immediately reflected in your UI.
|
||||
|
||||
<Tabs items={["React", "Vue","Svelte", "Solid"]} defaultValue="React">
|
||||
<Tab value="React">
|
||||
```tsx title="user.tsx"
|
||||
//make sure you're using the react client
|
||||
import { createAuthClient } from "better-auth/react"
|
||||
const { useSession } = createAuthClient() // [!code highlight]
|
||||
|
||||
export function User(){
|
||||
const {
|
||||
data: session,
|
||||
isPending, //loading state
|
||||
error //error object
|
||||
} = useSession()
|
||||
returns (
|
||||
//...
|
||||
)
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab value="Vue">
|
||||
```vue title="user.vue"
|
||||
<template>
|
||||
<div>
|
||||
<button v-if="!client.useSession().value" @click="() => client.signIn.social({
|
||||
provider: 'github'
|
||||
})">
|
||||
Continue with github
|
||||
</button>
|
||||
<div>
|
||||
<pre>{{ client.useSession().value }}</pre>
|
||||
<button v-if="client.useSession().value" @click="client.signOut()">
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab value="Svelte">
|
||||
```svelte title="user.svelte"
|
||||
<script lang="ts">
|
||||
import { client } from "$lib/client";
|
||||
const session = client.useSsession;
|
||||
</script>
|
||||
|
||||
<div
|
||||
style="display: flex; flex-direction: column; gap: 10px; border-radius: 10px; border: 1px solid #4B453F; padding: 20px; margin-top: 10px;"
|
||||
>
|
||||
<div>
|
||||
{#if $session}
|
||||
<div>
|
||||
<p>
|
||||
{$session?.user.name}
|
||||
</p>
|
||||
<p>
|
||||
{$session?.user.email}
|
||||
</p>
|
||||
<button
|
||||
on:click={async () => {
|
||||
await client.signOut();
|
||||
}}
|
||||
>
|
||||
Signout
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
on:click={async () => {
|
||||
await client.signIn.social({
|
||||
provider: "github",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Continue with github
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab value="Solid">
|
||||
```tsx title="user.tsx"
|
||||
import { client } from "~/lib/client";
|
||||
import { Show } from 'solid-js';
|
||||
|
||||
export default function Home() {
|
||||
const session = client.useSession()
|
||||
return (
|
||||
<Show
|
||||
when={session()}
|
||||
fallback={<button onClick={toggle}>Log in</button>}
|
||||
>
|
||||
<button onClick={toggle}>Log out</button>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Callout type="info">
|
||||
The `useSession` hook accepts an optional `initialData` parameter. This is particularly useful for server-side rendering (SSR) scenarios, such as when using Next.js or similar meta-frameworks.
|
||||
|
||||
By prefetching the session data on the server and passing it as `initialData`, you can avoid a flash of unauthenticated content.
|
||||
|
||||
**Example usage with Next.js:**
|
||||
|
||||
```tsx
|
||||
// a server component
|
||||
import { auth } from '@lib/auth';
|
||||
import { headers } from 'next/headers';
|
||||
export async function Page() {
|
||||
const session = await auth.api.getSession({
|
||||
headers: headers()
|
||||
})
|
||||
return { props: { initialSession: session } }
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
//a client component
|
||||
import { useSession, User, Sesssion } from '@lib/client';
|
||||
|
||||
function MyComponent({ initialSession }: {
|
||||
initialSession: {
|
||||
session: Session,
|
||||
user: User
|
||||
}}) {
|
||||
const { data: session } = useSession(initialSession)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
With this approach you'll get the initial load from the server-side but you get to keep the client-side reactivity.
|
||||
</Callout>
|
||||
### Server Side
|
||||
|
||||
The server provides a `session` object that you can use to access the session data.
|
||||
|
||||
|
||||
```ts title="server.ts" twoslash
|
||||
// @filename: auth.ts
|
||||
// ---cut---
|
||||
import { betterAuth } from "better-auth"
|
||||
export const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
//...rest of the options
|
||||
})
|
||||
// ---cut-start---
|
||||
// @filename: server.ts
|
||||
// ---cut-end---
|
||||
|
||||
// somewhere in your server code
|
||||
import { auth } from "./auth"
|
||||
async function addToCart(request: Request){
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers, //it requies a header to be passed
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
For next js on RSC and server actions you can use import `headers` from `next/headers` and pass it to the `getSession` function.
|
||||
</Callout>
|
||||
|
||||
## Two Factor
|
||||
|
||||
### Introduction to plugins
|
||||
|
||||
One of the unique features of better auth is a plugins ecosystem. It allows you to add complex auth realted functionilty with small lines of code. Better auth come with many 1st party plugins, but you can also create your own plugins.
|
||||
|
||||
Below is an example of how to add two factor authentication using two factor plugin.
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Server Configuration
|
||||
|
||||
To add a plugin, you need to import the plugin and pass it to the `plugins` option of the auth instance. For example, to add two facor authentication, you can use the following code:
|
||||
|
||||
```ts title="auth.ts" twoslash
|
||||
import { betterAuth } from "better-auth"
|
||||
import { twoFactor } from "better-auth/plugins"
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
//...rest of the options
|
||||
plugins: [ // [!code highlight]
|
||||
twoFactor({ // [!code highlight]
|
||||
issuer: "my-app" // [!code highlight]
|
||||
}) // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
now two factor related routes and method will be available on the server.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Migrate Database
|
||||
|
||||
once you have added the plugin, you need to migrate your database to add the necessary tables and fields. You can do this by running the following command:
|
||||
|
||||
```bash
|
||||
npx better-auth migrate
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Client Configuration
|
||||
|
||||
Once we're done with the server, we need to add the plugin to the client. To do this, you need to import the plugin and pass it to the `plugins` option of the auth client. For example, to add two facor authentication, you can use the following code:
|
||||
|
||||
```ts title="client.ts" twoslash /
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { twoFactorClient } from "better-auth/client/plugins";
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
twoFactorClient({ // [!code highlight]
|
||||
twoFactorPage: "/two-factor" // [!code highlight]
|
||||
}) // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
|
||||
now two factor related methods will be available on the client.
|
||||
|
||||
```ts title="profile.ts" twoslash
|
||||
// @filename: client.ts
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { twoFactorClient } from "better-auth/client/plugins";
|
||||
|
||||
export const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
twoFactorClient({ // [!code highlight]
|
||||
twoFactorPage: "/two-factor" // [!code highlight]
|
||||
}) // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
|
||||
// ---cut---
|
||||
|
||||
// @filename: profile.ts
|
||||
// ---cut---
|
||||
import { client } from "./client"
|
||||
|
||||
const enableTwoFactor = async() => {
|
||||
const data = await client.twoFactor.enable() // this will enable two factor authentication for the signed in user
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
Next Setp: See the <Link href="/docs/plugins/2fa">the two factor plugin documentation</Link>.
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -1,269 +0,0 @@
|
||||
---
|
||||
title: Getting Started with Better Auth
|
||||
description: A friendly guide to installing and setting up Better Auth
|
||||
---
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Install the Package
|
||||
|
||||
Let's start by adding Better Auth to your project:
|
||||
|
||||
```package-install
|
||||
better-auth
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
If you're using a separate client and server setup, make sure to install Better Auth in both parts of your project.
|
||||
</Callout>
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Set Environment Variables
|
||||
|
||||
Create a `.env` file in the root of your project and add the following environment variables:
|
||||
|
||||
1. **Secret Key**
|
||||
|
||||
Random value used by the library for encryption and generating hashes. **You can generate one using the button below** or you can use something like openssl.
|
||||
```txt title=".env"
|
||||
BETTER_AUTH_SECRET=
|
||||
```
|
||||
<GenerateSecret/>
|
||||
|
||||
2. **Set Base URL (optional)**
|
||||
|
||||
```txt title=".env"
|
||||
BETTER_AUTH_URL=http://localhost:3000 #Base URL of your app
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Create A Better Auth Instance
|
||||
|
||||
Create a file named `auth.ts` or `auth.config.ts` in one of these locations:
|
||||
- Project root
|
||||
- `lib/` folder
|
||||
- `utils/` folder
|
||||
|
||||
You can also nest any of these folders under `src/` folder. (e.g. `src/lib/auth.ts`)
|
||||
|
||||
And in this file, import Better Auth and create your instance.
|
||||
|
||||
<Callout type="warn">
|
||||
Make sure to export the auth instance with the variable name `auth` or as a `default` export.
|
||||
</Callout>
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = betterAuth({
|
||||
//...
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Configure Database
|
||||
|
||||
Better Auth requires a database to store user data. It currently only supports `sqlite`, `postgresql` and `mysql`.
|
||||
|
||||
You can pass a database provider (sqlite, mysql, postgresql) and connection string directly to the auth instance.
|
||||
|
||||
```ts twoslash title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: { // [!code highlight]
|
||||
provider: "sqlite", // or "mysql", "postgresql" // [!code highlight]
|
||||
url: "./db.sqlite", // path to your database or connection string // [!code highlight]
|
||||
} // [!code highlight]
|
||||
})
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Better auth uses <Link href="https://kysely.dev/">Kysely</Link> under the hood to connect to your database. You can also pass any dialect that is supported by Kysely to the database configration.
|
||||
</Callout>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Migrate Schema
|
||||
|
||||
Better Auth includes a CLI tool to migrate the required schema to your database. It introspects the database and creates the required tables. Run the following command to perform the migration:
|
||||
```bash title="Terminal"
|
||||
npx better-auth migrate
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Authentication Methods
|
||||
Configure the authentication methods you want to use. Better auth comes with built-in support for email/password, and social sign-on providers.
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth"
|
||||
import { github } from "better-auth/social-providers"
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
socialProvider: [ // [!code highlight]
|
||||
github({ // [!code highlight]
|
||||
clientId: process.env.GITHUB_CLIENT_ID as string, // [!code highlight]
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET as string, // [!code highlight]
|
||||
}), // [!code highlight]
|
||||
], // [!code highlight]
|
||||
emailAndPassword: { // [!code highlight]
|
||||
enabled: true // [!code highlight]
|
||||
}// [!code highlight]
|
||||
});
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
You can use even more authentication methods like passkey, username, magic link and more through plugins.
|
||||
</Callout>
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Mount Handler
|
||||
To handle api requests, you need to set up a route handler on your server.
|
||||
|
||||
Create a new file or route in your framework's designated catch-all route handler. This route should handle requests for the path `/api/auth/*` (unless you've configured a different base path).
|
||||
|
||||
<Tabs items={["next-js", "nuxt", "svelte-kit", "solid-start", "hono"]} defaultValue="react">
|
||||
<Tab value="next-js">
|
||||
```ts title="/app/api/[...auth]/route.ts"
|
||||
import { auth } from "@/lib/auth"; // path to your auth file
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
export const { POST, GET } = toNextJsHandler(auth);
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="nuxt">
|
||||
```ts title="/server/api/[...auth].ts"
|
||||
import { Hono } from "hono";
|
||||
import { auth } from "./auth"; // path to your auth file
|
||||
import { serve } from "@hono/node-server";
|
||||
import { cors } from "hono/cors";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.on(["POST", "GET"], "/api/auth/**", (c) => {
|
||||
return auth.handler(c.req.raw);
|
||||
});
|
||||
|
||||
serve(app);
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="svelte-kit">
|
||||
```ts title="hooks.server.ts"
|
||||
import { auth } from "$lib/auth"; // path to your auth file
|
||||
import { svelteKitHandler } from "better-auth/svelte-kit";
|
||||
|
||||
export async function handle({ event, resolve }) {
|
||||
return svelteKitHandler({ event, resolve, auth });
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="solid-start">
|
||||
```ts title="/routes/api/auth/*auth.ts"
|
||||
import { auth } from "~/lib/auth"; // path to your auth file
|
||||
import { toSolidStartHandler } from "better-auth/solid-start";
|
||||
|
||||
export const { GET, POST } = toSolidStartHandler(auth);
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="hono">
|
||||
```ts title="src/index.ts"
|
||||
import { Hono } from "hono";
|
||||
import { auth } from "./auth"; // path to your auth file
|
||||
import { serve } from "@hono/node-server";
|
||||
import { cors } from "hono/cors";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.on(["POST", "GET"], "/api/auth/**", (c) => {
|
||||
return auth.handler(c.req.raw);
|
||||
});
|
||||
|
||||
serve(app);
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Create Client Instance
|
||||
|
||||
The client-side library helps you interact with the auth server. Better Auth comes with a client for all the popular frameworks inlcuding for vanilla javascript.
|
||||
|
||||
1. Import `createAuthClient` from the package for your framework (e.g., "better-auth/react" for React).
|
||||
2. Call the function to create your client.
|
||||
3. Pass the base url of your auth server to the client.
|
||||
|
||||
<Callout type="info">
|
||||
If you're using a differnt base path other than `/api/auth` make sure to pass the whole url inlcuding the path. (e.g. `http://localhost:3000/custom-path/auth`)
|
||||
</Callout>
|
||||
|
||||
<Tabs items={["react", "vue", "svelte", "solid",
|
||||
"vanilla"]} defaultValue="react">
|
||||
<Tab value="vanilla">
|
||||
```ts title="lib/auth-client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
export const client = createAuthClient({
|
||||
baseURL: "http://localhost:3000" // the base url of your auth server // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="react" title="lib/auth-client.ts">
|
||||
```ts twoslash title="lib/auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/react"
|
||||
export const client = createAuthClient({
|
||||
baseURL: "http://localhost:3000" // the base url of your auth server // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="vue" title="lib/auth-client.ts">
|
||||
```ts twoslash title="lib/auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/vue"
|
||||
export const client = createAuthClient({
|
||||
baseURL: "http://localhost:3000" // the base url of your auth server // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="svelte" title="lib/auth-client.ts">
|
||||
```ts twoslash title="lib/auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/svelte"
|
||||
export const client = createAuthClient({
|
||||
baseURL: "http://localhost:3000" // the base url of your auth server // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="solid" title="lib/auth-client.ts">
|
||||
```ts title="lib/auth-client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/solid"
|
||||
export const client = createAuthClient({
|
||||
baseURL: "http://localhost:3000" // the base url of your auth server // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Callout type="info">
|
||||
Tip: You can also export specific methods if you prefer:
|
||||
</Callout>
|
||||
```ts
|
||||
export const { signIn, signUp, useSession } = createAuthClient()
|
||||
```
|
||||
</Step>
|
||||
|
||||
|
||||
|
||||
<Step>
|
||||
### 🎉 That's it!
|
||||
That's it! You're now ready to use better-auth in your application. Continue to [basic usage](/docs/basic-usage) to learn how to use the auth instance to sign in users.
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
title: Hono
|
||||
description: Hono Integration Guide
|
||||
---
|
||||
|
||||
This integration guide is assuming you are using Hono with node server.
|
||||
|
||||
## Installation
|
||||
|
||||
First, install Better Auth
|
||||
|
||||
```package-install
|
||||
npm install better-auth
|
||||
```
|
||||
|
||||
## Set Environment Variables
|
||||
|
||||
Create a `.env` file in the root of your project and add the following environment variables:
|
||||
|
||||
**Set Base URL**
|
||||
```txt title=".env"
|
||||
BETTER_AUTH_URL=http://localhost:3000 # Base URL of your Next.js app
|
||||
```
|
||||
|
||||
**Set Secret**
|
||||
|
||||
Random value used by the library for encryption and generating hashes. You can generate one using the button below or you can use something like openssl.
|
||||
```txt title=".env"
|
||||
BETTER_AUTH_SECRET=
|
||||
```
|
||||
<GenerateSecret/>
|
||||
|
||||
## Configure Server
|
||||
|
||||
### Create Better Auth instance
|
||||
|
||||
We recommend to create `auth.ts` file inside your `lib/` directory. This file will contain your Better Auth instance.
|
||||
|
||||
```ts twoslash title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
provider: "sqlite", //change this to your database provider
|
||||
url: "./db.sqlite", // path to your database or connection string
|
||||
}
|
||||
// Refer to the api documentation for more configuration options
|
||||
})
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
Better Auth currently supports only SQLite, MySQL, and PostgreSQL. It uses Kysely under the hood, so you can also pass any Kysely dialect directly to the database object.
|
||||
</Callout>
|
||||
|
||||
### Mount the handler
|
||||
|
||||
We need to mount the handler to hono endpoint.
|
||||
|
||||
```ts
|
||||
import { Hono } from "hono";
|
||||
import { auth } from "./auth";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { cors } from "hono/cors";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.on(["POST", "GET"], "/api/auth/**", (c) => {
|
||||
return auth.handler(c.req.raw);
|
||||
});
|
||||
|
||||
serve(app);
|
||||
```
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
---
|
||||
title: Next JS integration
|
||||
description: Learn how to integrate Better Auth with Next.js
|
||||
---
|
||||
|
||||
Better Auth can be easily integrated with Next.js. It'll also comes with utilities to make it easier to use Better Auth with Next.js.
|
||||
|
||||
## Installation
|
||||
|
||||
First, install Better Auth
|
||||
|
||||
```package-install
|
||||
npm install better-auth
|
||||
```
|
||||
|
||||
## Set Environment Variables
|
||||
|
||||
Create a `.env` file in the root of your project and add the following environment variables:
|
||||
|
||||
**Set Base URL**
|
||||
```txt title=".env"
|
||||
BETTER_AUTH_URL=http://localhost:3000 # Base URL of your app
|
||||
```
|
||||
|
||||
**Set Secret**
|
||||
|
||||
Random value used by the library for encryption and generating hashes. You can generate one using the button below or you can use something like openssl.
|
||||
```txt title=".env"
|
||||
BETTER_AUTH_SECRET=
|
||||
```
|
||||
<GenerateSecret/>
|
||||
|
||||
## Configure Server
|
||||
|
||||
### Create Better Auth instance
|
||||
|
||||
We recommend to create `auth.ts` file inside your `lib/` directory. This file will contain your Better Auth instance.
|
||||
|
||||
```ts twoslash title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
provider: "sqlite", //change this to your database provider
|
||||
url: "./db.sqlite", // path to your database or connection string
|
||||
}
|
||||
// Refer to the api documentation for more configuration options
|
||||
})
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
Better Auth currently supports only SQLite, MySQL, and PostgreSQL. It uses Kysely under the hood, so you can also pass any Kysely dialect directly to the database object.
|
||||
</Callout>
|
||||
|
||||
### Create API Route
|
||||
|
||||
We need to mount the handler to an API route. Create a route file inside `/api/[...auth]` directory. And add the following code:
|
||||
|
||||
```ts twoslash title="api/[...auth]/route.ts"
|
||||
//@filename: @/lib/auth.ts
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
provider: "sqlite", //change this to your database provider
|
||||
url: "./db.sqlite", // path to your database or connection string
|
||||
}
|
||||
// Refer to the api documentation for more configuration options
|
||||
})
|
||||
// ---cut---
|
||||
//@filename: api/[...auth]/route.ts
|
||||
//---cut---
|
||||
import { auth } from "@/lib/auth";
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
export const { GET, POST } = toNextJsHandler(auth.handler);
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
You can change the path on your better-auth configuration but it's recommended to keep it as `/api/[...auth]`
|
||||
</Callout>
|
||||
|
||||
### Migrate the database
|
||||
Run the following command to create the necessary tables in your database:
|
||||
|
||||
```bash
|
||||
npx better-auth migrate
|
||||
```
|
||||
|
||||
## Create a client
|
||||
|
||||
Create a client instance. You can name the file anything you want. Here we are creating `client.ts` file inside the `lib/` directory.
|
||||
|
||||
```ts twoslash title="client.ts"
|
||||
import { createAuthClient } from "better-auth/react" // make sure to import from better-auth/react
|
||||
|
||||
export const client = createAuthClient({
|
||||
//you can pass client configuration here
|
||||
})
|
||||
```
|
||||
|
||||
Once you have created the client, you can use it to sign up, sign in, and perform other actions.
|
||||
Some of the actinos are reactive. The client use [nano-store](https://github.com/nanostores/nanostores) to store the state and re-render the components when the state changes.
|
||||
|
||||
The client also uses [better-fetch](https://github.com/bekacru/better-fetch) to make the requests. You can pass the fetch configuration to the client.
|
||||
|
||||
## RSC and Server actions
|
||||
|
||||
The `api` object exported from the auth instance contains all the actions that you can perform on the server. Every endpoint made inside better auth is a invokable as a function. Including plugins endpoints.
|
||||
|
||||
**Example: Getting Session on a server action**
|
||||
|
||||
```tsx twoslash title="server.ts"
|
||||
//@filename: @/lib/auth.ts
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
provider: "sqlite", //change this to your database provider
|
||||
url: "./db.sqlite", // path to your database or connection string
|
||||
}
|
||||
// Refer to the api documentation for more configuration options
|
||||
})
|
||||
// ---cut---
|
||||
//@filename: server.ts
|
||||
//---cut---
|
||||
import { auth } from "@/lib/auth"
|
||||
import { headers } from "next/headers"
|
||||
|
||||
const someAuthenticatedAction = async () => {
|
||||
"use server";
|
||||
const session = await auth.api.getSession({
|
||||
headers: headers()
|
||||
})
|
||||
};
|
||||
```
|
||||
|
||||
**Example: Getting Session on a RSC**
|
||||
|
||||
```tsx
|
||||
import { auth } from "@/lib/auth"
|
||||
import { headers } from "next/headers"
|
||||
|
||||
export async function ServerComponent() {
|
||||
const session = await auth.api.getSession({
|
||||
headers: headers()
|
||||
})
|
||||
if(!session) {
|
||||
return <div>Not authenticated</div>
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome {session.user.name}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Middleware
|
||||
|
||||
You can use the `authMiddleware` to protect your routes. It's a wrapper around the Next.js middleware.
|
||||
|
||||
```ts twoslash title="middleware.ts"s
|
||||
import { authMiddleware } from "better-auth/next-js"
|
||||
|
||||
export default authMiddleware({
|
||||
redirectTo: "/sign-in" // redirect to this path if the user is not authenticated
|
||||
})
|
||||
|
||||
export const config = {
|
||||
matcher: ['/dashboard/:path*'],
|
||||
}
|
||||
```
|
||||
@@ -1,64 +0,0 @@
|
||||
---
|
||||
title: Solid Start
|
||||
description: Solid Start integratons guide
|
||||
---
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
First, install Better Auth
|
||||
|
||||
```package-install
|
||||
npm install better-auth
|
||||
```
|
||||
|
||||
## Set Environment Variables
|
||||
|
||||
Create a `.env` file in the root of your project and add the following environment variables:
|
||||
|
||||
**Set Base URL**
|
||||
```txt title=".env"
|
||||
BETTER_AUTH_URL=http://localhost:3000 # Base URL of your Next.js app
|
||||
```
|
||||
|
||||
**Set Secret**
|
||||
|
||||
Random value used by the library for encryption and generating hashes. You can generate one using the button below or you can use something like openssl.
|
||||
```txt title=".env"
|
||||
BETTER_AUTH_SECRET=
|
||||
```
|
||||
|
||||
<GenerateSecret/>
|
||||
|
||||
## Configure Server
|
||||
|
||||
### Create Better Auth instance
|
||||
|
||||
We recommend to create `auth.ts` file inside your `lib/` directory. This file will contain your Better Auth instance.
|
||||
|
||||
```ts twoslash title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
provider: "sqlite", //change this to your database provider
|
||||
url: "./db.sqlite", // path to your database or connection string
|
||||
}
|
||||
// Refer to the api documentation for more configuration options
|
||||
})
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
Better Auth currently supports only SQLite, MySQL, and PostgreSQL. It uses Kysely under the hood, so you can also pass any Kysely dialect directly to the database object.
|
||||
</Callout>
|
||||
|
||||
### Mount the handler
|
||||
|
||||
We need to mount the handler to Solid Start server. Put the following code in your `*auth.ts` file inside `/routes/api/auth` folder.
|
||||
|
||||
```ts title="*auth.ts"
|
||||
import { auth } from "~/lib/auth";
|
||||
import { toSolidStartHandler } from "better-auth/solid-start";
|
||||
|
||||
export const { GET, POST } = toSolidStartHandler(auth);
|
||||
```
|
||||
@@ -1,148 +0,0 @@
|
||||
---
|
||||
title: Svelte Kit
|
||||
description: Learn how to integrate Better Auth with Svelte Kit
|
||||
---
|
||||
|
||||
Better Auth has first class support for Svelte Kit. It provides utilities to make it easier to use Better Auth with Svelte Kit.
|
||||
|
||||
## Installation
|
||||
|
||||
First, install Better Auth
|
||||
|
||||
```package-install
|
||||
npm install better-auth
|
||||
```
|
||||
|
||||
## Set Environment Variables
|
||||
|
||||
Create a `.env` file in the root of your project and add the following environment variables:
|
||||
|
||||
**Set Base URL**
|
||||
```txt title=".env"
|
||||
BETTER_AUTH_URL=http://localhost:3000 # Base URL of your Next.js app
|
||||
```
|
||||
|
||||
**Set Secret**
|
||||
|
||||
Random value used by the library for encryption and generating hashes. You can generate one using the button below or you can use something like openssl.
|
||||
```txt title=".env"
|
||||
BETTER_AUTH_SECRET=
|
||||
```
|
||||
|
||||
<GenerateSecret/>
|
||||
|
||||
## Configure Server
|
||||
|
||||
### Create Better Auth instance
|
||||
|
||||
We recommend to create `auth.ts` file inside your `lib/` directory. This file will contain your Better Auth instance.
|
||||
|
||||
```ts twoslash title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
provider: "sqlite", //change this to your database provider
|
||||
url: "./db.sqlite", // path to your database or connection string
|
||||
}
|
||||
// Refer to the api documentation for more configuration options
|
||||
})
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
Better Auth currently supports only SQLite, MySQL, and PostgreSQL. It uses Kysely under the hood, so you can also pass any Kysely dialect directly to the database object.
|
||||
</Callout>
|
||||
|
||||
### Mount the handler
|
||||
|
||||
We need to mount the handler to svelte kit server hook.
|
||||
|
||||
```ts
|
||||
import { auth } from "$lib/auth";
|
||||
import { svelteKitHandler } from "better-auth/svelte-kit";
|
||||
|
||||
export async function handle({ event, resolve }) {
|
||||
return svelteKitHandler({ event, resolve, auth });
|
||||
}
|
||||
```
|
||||
|
||||
### Migrate the database
|
||||
Run the following command to create the necessary tables in your database:
|
||||
|
||||
```bash
|
||||
npx better-auth migrate
|
||||
```
|
||||
|
||||
|
||||
## Create a client
|
||||
|
||||
Create a client instance. You can name the file anything you want. Here we are creating `client.ts` file inside the `lib/` directory.
|
||||
|
||||
```ts twoslash title="client.ts"
|
||||
import { createAuthClient } from "better-auth/svelte" // make sure to import from better-auth/svlete
|
||||
|
||||
export const client = createAuthClient({
|
||||
//you can pass client configuration here
|
||||
})
|
||||
```
|
||||
|
||||
Once you have created the client, you can use it to sign up, sign in, and perform other actions.
|
||||
Some of the actinos are reactive. The client use [nano-store](https://github.com/nanostores/nanostores) to store the state and refelct changes when there is a change like a user signing in or out affecting the session state.
|
||||
|
||||
### Example usage
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { client } from "$lib/client";
|
||||
const session = client.$session;
|
||||
</script>
|
||||
<div>
|
||||
{#if $session}
|
||||
<div>
|
||||
<p>
|
||||
{$session?.user.name}
|
||||
</p>
|
||||
<button
|
||||
on:click={async () => {
|
||||
await client.signOut();
|
||||
}}
|
||||
>
|
||||
Signout
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
on:click={async () => {
|
||||
await client.signIn.social({
|
||||
provider: "github",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Continue with github
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Example: Getting Session on a loader
|
||||
|
||||
```ts title="+page.ts"
|
||||
import { auth } from "$lib/auth";
|
||||
|
||||
export async function load(request: Request) {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
if (!session) {
|
||||
return {
|
||||
status: 401,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
error: "Unauthorized",
|
||||
}),
|
||||
};
|
||||
}
|
||||
return session;
|
||||
}
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
title: Introduction
|
||||
description: Introduction to Better Auth.
|
||||
---
|
||||
|
||||
Better Auth is a type-safe, framework-agnostic authentication library for TypeScript. It provides a comprehensive set of features out of the box and includes a plugin ecosystem that simplifies adding advanced functionalities with minimal code. Whether you need 2FA, multi-tenant support, or other complex features. It lets you focus on building your app instead of reinventing the wheel.
|
||||
|
||||
## Why Better Auth?
|
||||
|
||||
Authentication feels like a partially solved problem, existing open-source libraries often require a lot of additional code for anything beyond a simple login. Third-party services, while convenient, force you to store user data on their servers, which in some ways strips you of ownership and it also comes with its own set of problems. And obviously, these services aren't free and can get really expensive.
|
||||
|
||||
Better Auth offers a different approach. It provides a comprehensive authentication library from the core accompanied by a growings plugin ecosystem, that allows you to add many authentication related features in just minutes. Need multi-factor authentication? Simply use our 2FA plugin. Looking to support workspaces, organizations, member roles, or access control for you multi tenant apps? You're a plugin away from having a fully featured auth system.
|
||||
|
||||
## Features
|
||||
|
||||
Better auth is aims to be the most comprehensive auth library. It provides a wide range of features out of the box and allows you to extend it with plugins. Here are some of the features:
|
||||
|
||||
<Features/>
|
||||
|
||||
and much more and even more to come...
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"title:": "guide",
|
||||
"root": true,
|
||||
"pages": [
|
||||
"introduction",
|
||||
"installation",
|
||||
"basic-usage",
|
||||
"email-password/sign-in-and-sign-up",
|
||||
"email-password/password-reset",
|
||||
"email-password/configuration",
|
||||
"social-sign-on/apple"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: Plugins
|
||||
description: Plugins
|
||||
---
|
||||
|
||||
@@ -1,387 +0,0 @@
|
||||
---
|
||||
title: Two-Factor Authentication (2FA)
|
||||
description: Enhance your app's security with two-factor authentication
|
||||
---
|
||||
|
||||
`OTP & TOTP` `Backup Codes` `Trusted Devices`
|
||||
|
||||
## What is Two-Factor Authentication?
|
||||
|
||||
Two-Factor Authentication (2FA) adds an extra security step when users log in. Instead of just using a password, they'll need to provide a second form of verification. This makes it much harder for unauthorized people to access accounts, even if they've somehow gotten the password.
|
||||
|
||||
This plugin offers two main methods of 2FA:
|
||||
|
||||
1. **OTP (One-Time Password)**: A temporary code sent to the user's email or phone.
|
||||
2. **TOTP (Time-based One-Time Password)**: A code generated by an app on the user's device.
|
||||
|
||||
**Additional features include:**
|
||||
- Generating backup codes for account recovery
|
||||
- Enabling/disabling 2FA
|
||||
- Managing trusted devices
|
||||
|
||||
|
||||
## Initial Setup
|
||||
|
||||
To get started with Two-Factor Authentication, follow these steps:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Add the plugin to your auth config:
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { twoFactor } from "better-auth/plugins"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
// ... other config options
|
||||
plugins: [
|
||||
twoFactor({
|
||||
issuer: "my-app"
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Migrate your database:
|
||||
|
||||
```bash
|
||||
npx better-auth migrate
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Add the client plugin:
|
||||
|
||||
```ts title="client.ts"
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { twoFactorClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [
|
||||
twoFactorClient({
|
||||
twoFactorPage: "/two-factor" //redirect for two factor verification if enabled // [!code highlight]
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Usage
|
||||
|
||||
### Enabling 2FA
|
||||
|
||||
To enable two-factor authentication for a user:
|
||||
|
||||
```ts title="two-factor.ts"
|
||||
const enableTwoFactor = async() => {
|
||||
const { data, error } = await client.twoFactor.enable()
|
||||
if (data) {
|
||||
// 2FA has been enabled successfully
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Sign In with 2FA
|
||||
|
||||
When a user with 2FA enabled tries to sign in, you'll need to verify their 2FA code. If they have 2FA enabled, they'll be redirected to the `twoFactorPage` where they can enter their 2FA code.
|
||||
|
||||
```ts
|
||||
const signin = async () => {
|
||||
const { data, error } = await client.signIn.email({
|
||||
email: "user@example.com",
|
||||
password: "password123",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
By default, the user will be redirected to the `twoFactorPage` if they have 2FA enabled. If you want to handle the 2FA verification in place, you can use the `options` parameter.
|
||||
|
||||
```ts
|
||||
const signin = async () => {
|
||||
const { data, error } = await client.signIn.email({
|
||||
email: "user@example.com",
|
||||
password: "password123",
|
||||
options: {
|
||||
async onSuccess(context) {
|
||||
if (context.data.twoFactorRedirect) {
|
||||
// Handle the 2FA verification in place
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### TOTP
|
||||
|
||||
TOTP is a time-based one-time password algorithm that generates a code based on the current time. It's a more secure method than OTP because it takes into account the time it takes to generate the code.
|
||||
|
||||
#### Getting TOTP URI
|
||||
|
||||
After enabling 2FA, you can get the TOTP URI to display to the user.
|
||||
|
||||
```ts
|
||||
const { data, error } = await client.twoFactor.getTotpUri()
|
||||
if (data) {
|
||||
// Use data.totpURI to generate a QR code or display to the user
|
||||
}
|
||||
```
|
||||
|
||||
#### Generating a QR Code
|
||||
|
||||
You can use a library like `qrcode` to generate a QR code from the TOTP URI.
|
||||
|
||||
```ts
|
||||
import { toCanvas } from "qrcode"
|
||||
|
||||
toCanvas(document.getElementById('canvas'), data.totpURI)
|
||||
```
|
||||
|
||||
#### Verifying TOTP
|
||||
|
||||
After the user has entered their 2FA code, you can verify it
|
||||
|
||||
```ts
|
||||
const verifyTotp = async (code: string) => {
|
||||
const { data, error } = await client.twoFactor.verifyTotp({ code })
|
||||
}
|
||||
```
|
||||
|
||||
### OTP
|
||||
|
||||
OTP is a one-time password algorithm that generates a code based on the current time. It's a more secure method than TOTP because it takes into account the time it takes to generate the code.
|
||||
|
||||
|
||||
Before using OTP, you need to setup `sendOTP` function.
|
||||
|
||||
```ts title="auth.ts" twoslash
|
||||
import { betterAuth } from "better-auth"
|
||||
import { twoFactor } from "better-auth/plugins"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
plugins: [
|
||||
twoFactor({
|
||||
otpOptions: {
|
||||
async sendOTP(user, otp) {
|
||||
console.log({ user, otp });
|
||||
},
|
||||
},
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
#### Sending OTP
|
||||
|
||||
sending otp is done by calling `sendOtp` function. This functino will call your `sendOTP` function that you provide in the `otpOptions` with the otp code and the user.
|
||||
|
||||
```ts
|
||||
const { data, error } = await client.twoFactor.sendOtp()
|
||||
if (data) {
|
||||
// Show the OTP to the user
|
||||
}
|
||||
```
|
||||
|
||||
#### Verifying OTP
|
||||
|
||||
After the user has entered their OTP code, you can verify it
|
||||
|
||||
```ts
|
||||
const verifyOtp = async (code: string) => {
|
||||
const { data, error } = await client.twoFactor.verifyOtp({ code })
|
||||
}
|
||||
```
|
||||
|
||||
### Backup Codes
|
||||
|
||||
|
||||
Backup codes are generated and stored in the database when the user enabled two factor authentication. This can be used to recover access to the account if the user loses access to their phone or email.
|
||||
|
||||
#### Generating Backup Codes
|
||||
Generate backup codes for account recovery:
|
||||
|
||||
```ts
|
||||
const { data, error } = await client.twoFactor.generateBackupCodes()
|
||||
if (data) {
|
||||
// Show the backup codes to the user
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Backup Codes
|
||||
|
||||
Backup codes can be used to recover access to the account if the user loses access to their phone or email.
|
||||
|
||||
```ts
|
||||
const { data, error } = await client.twoFactor.verifyBackupCode({code: ""})
|
||||
if (data) {
|
||||
// 2FA verified and account recovered
|
||||
}
|
||||
```
|
||||
|
||||
### Trusted Devices
|
||||
|
||||
You can mark a device as trusted by passing `trustDevice` to `verifyTotp` or `verifyOtp`.
|
||||
|
||||
```ts
|
||||
const verify2FA = async (code: string) => {
|
||||
const { data, error } = await client.twoFactor.verifyTotp({
|
||||
code,
|
||||
callbackURL: "/dashboard",
|
||||
trustDevice: true // Mark this device as trusted
|
||||
})
|
||||
if (data) {
|
||||
// 2FA verified and device trusted
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When `trustDevice` is set to `true`, the current device will be remembered for 60 days. During this period, the user won't be prompted for 2FA on subsequent sign-ins from this device. The trust period is refreshed each time the user signs in successfully.
|
||||
|
||||
<Callout type="info">
|
||||
Trusted devices enhance user convenience but should be used carefully, as they slightly reduce security. Encourage users to only trust personal, secure devices.
|
||||
</Callout>
|
||||
|
||||
## Configuration
|
||||
|
||||
### Server
|
||||
|
||||
To configure the two factor plugin, you need to add the following code to your better auth instance.
|
||||
|
||||
```ts title="auth.ts" twoslash
|
||||
import { betterAuth } from "better-auth"
|
||||
import { twoFactor } from "better-auth/plugins"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
plugins: [ // [!code highlight]
|
||||
twoFactor({ // [!code highlight]
|
||||
issuer: "my-app" // [!code highlight]
|
||||
}) // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
|
||||
**Issuer**: The issuer is the name of your application. It's used to generate totp codes. It'll be displayed in the authenticator apps.
|
||||
|
||||
**TOTP options**
|
||||
|
||||
these are options for TOTP.
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
digits:{
|
||||
description: 'The number of digits the otp to be',
|
||||
type: 'number',
|
||||
default: 6,
|
||||
},
|
||||
period: {
|
||||
description: 'The period for otp in seconds.',
|
||||
type: 'number',
|
||||
default: 30,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
**OTP options**
|
||||
|
||||
these are options for OTP.
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
sendOTP: {
|
||||
description: "a function that sends the otp to the user's email or phone number. It takes two parameters: user and otp",
|
||||
type: "function",
|
||||
},
|
||||
period: {
|
||||
description: 'The period for otp in seconds.',
|
||||
type: 'number',
|
||||
default: 30,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
**Backup Code Options**
|
||||
|
||||
backup codes are generated and stored in the database when the user enabled two factor authentication. This can be used to recover access to the account if the user loses access to their phone or email.
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
amount: {
|
||||
description: "The amount of backup codes to generate",
|
||||
type: "number",
|
||||
default: 10,
|
||||
},
|
||||
length: {
|
||||
description: "The length of the backup codes",
|
||||
type: "number",
|
||||
default: 10,
|
||||
},
|
||||
customBackupCodesGenerate: {
|
||||
description: "A function that generates custom backup codes. It takes no parameters and returns an array of strings.",
|
||||
type: "function",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### Client
|
||||
|
||||
To use the two factor plugin in the client, you need to add it on your plugins list.
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { twoFactorClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [
|
||||
twoFactorClient({ // [!code highlight]
|
||||
twoFactorPage: "/two-factor" // [!code highlight]
|
||||
}) // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
`twoFactorPage`: The page to redirect the user to after they have enabled 2-Factor. This is the page where the user will be redirected to verify their 2-Factor code.
|
||||
|
||||
`redirect`: If set to `false`, the user will not be redirected to the `twoFactorPage` after they have enabled 2-Factor.
|
||||
|
||||
|
||||
## Database Schema
|
||||
|
||||
Two factores requires additional fields on the user table. If you use better auth's migration system, it will automatically create this table for you.
|
||||
|
||||
```ts
|
||||
const schema = {
|
||||
user: {
|
||||
fields: {
|
||||
twoFactorEnabled: {
|
||||
type: "boolean",
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
twoFactorSecret: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
twoFactorBackupCodes: {
|
||||
type: "string",
|
||||
required: false,
|
||||
returned: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -1,428 +0,0 @@
|
||||
---
|
||||
title: Organization Plugin
|
||||
description: Organization Plugin
|
||||
---
|
||||
|
||||
Organizations Plugin offer a versatile and scalable approach to controlling user access and permissions within your application. By leveraging organizations, you can allocate distinct roles and permissions to individuals, streamlining the management of projects, team coordination, and partnership facilitation.
|
||||
|
||||
## Quick setup
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Add the plugin to your auth config
|
||||
|
||||
```ts title="auth.ts" twoslash
|
||||
import { betterAuth } from "better-auth"
|
||||
import { organization } from "better-auth/plugins"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
plugins: [ // [!code highlight]
|
||||
organization() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Migarate database
|
||||
Once you've added the plugin, you need to migrate your database to add the necessary tables and fields. You can do this by running the following command:
|
||||
|
||||
```bash
|
||||
npx better-auth migrate
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Add the client plugin
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { organizationClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
organizationClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Create an Organization
|
||||
|
||||
this will create an organization with the name `My Organization` and slug `my-org`. The user who creates the organization will be the `owner` of the organization by default.
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { organizationClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
organizationClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
const data = await client.organization.create({
|
||||
name: "My Organization",
|
||||
slug: "my-org"
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### List Organizations
|
||||
|
||||
To list all user's organization on the client you can use `useListOrganizations` hook or for svelte you can use `client.$listOrganizations`.
|
||||
|
||||
```tsx title="client.tsx"
|
||||
import { createAuthClient } from "better-auth/react"
|
||||
import { organizationClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [
|
||||
organizationClient()
|
||||
]
|
||||
})
|
||||
function App(){
|
||||
const organizations = client.useListOrganizations()
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
organizations.map(org => <p>{org.name}</p>)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Active Organization
|
||||
|
||||
#### Set Active Organization
|
||||
|
||||
Active organization is the organization that the user is currently working on. You can set the active organization by calling the `organization.setActive` function. It'll set the active organization for the user both on the client state and the session on the server.
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { organizationClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
organizationClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
|
||||
client.organization.setActive("organization-id")
|
||||
```
|
||||
<Callout type="info">
|
||||
`setActive` updates the active organization reactively. Ensure it's used according to your framework's rules for reactive calls. For React, call it within a component and follow the rules of hooks.
|
||||
</Callout>
|
||||
|
||||
#### Get Active Organization
|
||||
To retrieve the active organization for the user, you can call the `useActiveOrganization` hook. It returns the active organization for the user.
|
||||
|
||||
```tsx title="client.tsx"
|
||||
import { createAuthClient } from "better-auth/react"
|
||||
import { organizationClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [
|
||||
organizationClient()
|
||||
]
|
||||
})
|
||||
function App(){
|
||||
const activeOrganization = client.useActiveOrganization()
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
activeOrganization ? <p>{activeOrganization.name}</p> : null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
The above example should be roughly similar for all frameworks except for the svelte. For svelte, you can need to use `client.$activeOrganization`. instead. And make sure to call it with `$` to make it reactive.
|
||||
```svelte title="page.svelte"
|
||||
<script lang="ts">
|
||||
import { createAuthClient } from "better-auth/svelte"
|
||||
import { organizationClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [
|
||||
organizationClient()
|
||||
]
|
||||
})
|
||||
const activeOrganization = client.$activeOrganization
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if $activeOrganization}
|
||||
<p>{$activeOrganization.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Invite Users to Organization
|
||||
|
||||
#### Send Invitation
|
||||
|
||||
For member invitation to work we first need to provider `sendInvitationEmail` to the `better-auth` instance. This function is responsible for sending the invitation email to the user. The function takes an object with the following properties:
|
||||
- `email`: The email address of the user.
|
||||
- `invitation`: The invitation object that contains the organization id and the role of the user in the organization and the invitation id which serve as a token for the user to accept the invitation.
|
||||
|
||||
```ts title="auth.ts" twoslash
|
||||
|
||||
import { betterAuth } from "better-auth"
|
||||
import { organization } from "better-auth/plugins"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
plugins: [
|
||||
organization({
|
||||
sendInvitationEmail: async (invitation, email) => {
|
||||
const url = `http://example.com/accept-invitation?token={invitation.id}`
|
||||
// send email
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
````
|
||||
|
||||
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.
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { organizationClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
organizationClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
const data = await client.organization.inviteMember({
|
||||
email: "test@email.com",
|
||||
role: "admin",
|
||||
})
|
||||
```
|
||||
|
||||
#### Get Invitation
|
||||
|
||||
When a user clicks the invitation link, use setInvitationId with the invitation ID. Then, use the useInvitation hook to fetch the invitation object. If the token is valid, the hook returns the invitation object; if not, it returns null.
|
||||
|
||||
```tsx title="client.tsx"
|
||||
|
||||
import { createAuthClient } from "better-auth/react"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [
|
||||
organizationClient()
|
||||
]
|
||||
})
|
||||
function App(){
|
||||
useEffect(() => {
|
||||
client.setInvitationId(
|
||||
urlParams.get("token")
|
||||
)
|
||||
}, [])
|
||||
const invitation = client.useInvitation()
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
invitation ? <p>{invitation.email}</p> : null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
#### 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.
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { organizationClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
organizationClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
const data = await client.organization.acceptInvitation({
|
||||
invitationId: "invitation-id"
|
||||
})
|
||||
```
|
||||
<Callout>
|
||||
To accept or cancel an invitation, the invited user must sign in using the email address specified in the invitation. Upon accepting the invitation, the user will be added to the organization with the role outlined in the invitation.
|
||||
</Callout>
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Remove and Update Memeber
|
||||
To remove or update memebrs you can use `organization.removeMember` or `organization.updateMember`.
|
||||
</Step>
|
||||
<Step>
|
||||
🎉 Congrats you have setup organization sucessfully.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Access Control
|
||||
|
||||
The plugin providers a very flexible access control system. You can control the access of the user based on the role they have in the organization. You can define your own set of permissions based on the role of the user.
|
||||
|
||||
### Roles
|
||||
|
||||
`owner`: The user who created the organization by default. The owner has full control over the organization and can perform any action.
|
||||
|
||||
`admin`: Users with the admin role have full control over the organization except for deleting the organization or changing the owner.
|
||||
|
||||
`member`: Users with the member role have limited control over the organization. They can create projects, invite users, and manage projects they have created.
|
||||
|
||||
### Permissons
|
||||
|
||||
By defualt there are 3 resources and they have 2 to 3 actions.
|
||||
|
||||
**organization**:
|
||||
|
||||
`update` `delete`
|
||||
|
||||
**member**:
|
||||
|
||||
`create` `update` `delete`
|
||||
|
||||
**invitation**:
|
||||
|
||||
`create` `cancel`
|
||||
|
||||
The owner have full control over all the resources and actions. The admin have full control over all the resources except for deleting the organization or changing the owner. The member have no control over any of those action other than reading the data.
|
||||
|
||||
### Custom Permissions
|
||||
|
||||
the plugin providers easy way to define your own set of permission for each role.
|
||||
|
||||
**Terminologies**
|
||||
|
||||
`resource`: The resource for which you want to define the permission. For example, `project`, `task`, `comment`, etc.
|
||||
|
||||
`action`: The action that the user can perform on the resource. For example, `create`, `read`, `update`, `delete`.
|
||||
|
||||
`statement`: The statement that defines the permission for the role. The statement is an object with the resource as the key and an array of actions as the value.
|
||||
|
||||
`role`: The role for which you want to define the permission. For example, `admin`, `member`, `owner`.
|
||||
|
||||
<Callout type="warn">
|
||||
Currently the plugin only supports those 3 pre-defined roles. You can't add your own role.
|
||||
</Callout>
|
||||
|
||||
```ts title="permissions.ts" twoslash
|
||||
import { createAccessControl } from "better-auth/plugins/access";
|
||||
|
||||
const statement = {
|
||||
project: ["create", "share", "update", "delete"],
|
||||
};
|
||||
const ac = createAccessControl(statement);
|
||||
|
||||
const member = ac.newRole({
|
||||
project: ["create"],
|
||||
});
|
||||
const admin = ac.newRole({
|
||||
project: ["create", "update"],
|
||||
});
|
||||
const owner = ac.newRole({
|
||||
project: ["create", "update", "delete"],
|
||||
});
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
We need to share the permission with both the client and the server. You can define the permission in a separate file and import it in both the client and the server.
|
||||
</Callout>
|
||||
|
||||
**Add permissions to the serever**
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { organization } from "better-auth/plugins"
|
||||
import { ac, member, owner, admin } from "./permissions" // [!code highlight]
|
||||
|
||||
export const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
plugins: [
|
||||
organization({
|
||||
ac, // [!code highlight]
|
||||
roles: { // [!code highlight]
|
||||
member, // [!code highlight]
|
||||
admin, // [!code highlight]
|
||||
owner, // [!code highlight]
|
||||
} // [!code highlight]
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
### Server
|
||||
|
||||
**allowUserToCreateOrganization**: `boolean` | `((user: User) => Promise<boolean> | boolean)` - Allow users to create organizations. By default, it's `true`. You can set it to `false` to disable users from creating organizations. You can also provide a function that returns a boolean value to determine if the user can create an organization. For emxaple you can allow them based on their subscription plan.
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { organization } from "better-auth/plugins"
|
||||
|
||||
const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
plugins: [
|
||||
organization({
|
||||
allowUserToCreateOrganization: (user) => {
|
||||
const subscription = await getSubscription(user.id)
|
||||
return subscription.plan === "pro"
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
**creatorRole**: `admin | owner` - The role of the user who creates the organization. By default, it's `owner`. You can set it to `admin`.
|
||||
|
||||
**membershipLimit**: `number` - The maximum number of members allowed in an organization. By default, it's `100`. You can set it to any number you want.
|
||||
|
||||
**sendInvitationEmail**: `async (invitation: Invitation, email: string) => Promise<void>` - A function that sends an invitation email to the user. The function takes an invitation object and the email address of the user as arguments. You can use this function to send an email with an invitation link to the user. The invitation object contains the organization id, the role of the user in the organization, and the invitation id which serves as a token for the user to accept the invitation.
|
||||
|
||||
```ts title="auth.ts"
|
||||
|
||||
import { betterAuth } from "better-auth"
|
||||
import { organization } from "better-auth/plugins"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
plugins: [
|
||||
organization({
|
||||
sendInvitationEmail: async (invitation, email) => {
|
||||
const url = `http://example.com/accept-invitation?token={invitation.id}`
|
||||
// send email
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
@@ -1,158 +0,0 @@
|
||||
---
|
||||
title: Passkey
|
||||
description: Passkey
|
||||
---
|
||||
|
||||
Passkeys are a secure, passwordless authentication method using cryptographic key pairs, supported by WebAuthn and FIDO2 standards in web browsers. They replace passwords with unique key pairs: a private key stored on the user’s device and a public key shared with the website. Users can log in using biometrics, PINs, or security keys, providing strong, phishing-resistant authentication without traditional passwords.
|
||||
|
||||
The passkey plugin implementation is powered by [simple-web-authn](https://simplewebauthn.dev/) behind the scenes.
|
||||
|
||||
## Quick setup
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Add the plugin to your auth config
|
||||
To add the passkey plugin to your auth config, you need to import the plugin and pass it to the `plugins` option of the auth instance.
|
||||
|
||||
**Options**
|
||||
|
||||
`rpID`: A unique identifier for your website. 'localhost' is okay for local dev
|
||||
|
||||
`rpName`: Human-readable title for your website
|
||||
|
||||
`origin`: The URL at which registrations and authentications should occur. 'http://localhost' and 'http://localhost:PORT' are also valid.Do NOT include any trailing /
|
||||
|
||||
```ts title="auth.ts" twoslash
|
||||
import { betterAuth } from "better-auth"
|
||||
import { passkey } from "better-auth/plugins"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
plugins: [ // [!code highlight]
|
||||
passkey({ // [!code highlight]
|
||||
rpID: "localhost", // [!code highlight]
|
||||
rpName: "BetterAuth", // [!code highlight]
|
||||
origin: "http://localhost:3000", // [!code highlight]
|
||||
}), // [!code highlight]
|
||||
], // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Add the client plugin
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { passkeyClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
passkeyClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Register a passkey
|
||||
|
||||
To register a passkey make sure a user is authenticated and then call the `register` function provided by the client.
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { passkeyClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
passkeyClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
const data = await client.passkey.register()
|
||||
```
|
||||
This will prompt the user to register a passkey. And it'll add the passkey to the user's account.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Signin with a passkey
|
||||
|
||||
To signin with a passkey you can use the passkeySignIn 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)
|
||||
|
||||
`callbackURL`: The URL to redirect to after the user has signed in. (optional)
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { passkeyClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
passkeyClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
const data = await client.signIn.passkey()
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
## Passkey Configuration
|
||||
|
||||
**rpID**: A unique identifier for your website. 'localhost' is okay for local dev.
|
||||
|
||||
**rpName**: Human-readable title for your website.
|
||||
|
||||
**origin**: The URL at which registrations and authentications should occur. 'http://localhost' and 'http://localhost:PORT' are also valid. Do NOT include any trailing /.
|
||||
|
||||
|
||||
## Database Schema
|
||||
|
||||
Passkey requires a database table called `passkey` with the following fields. If you use better auth's migration system, it will automatically create this table for you.
|
||||
|
||||
```ts
|
||||
const schema = {
|
||||
passkey: {
|
||||
fields: {
|
||||
publicKey: {
|
||||
type: "string",
|
||||
},
|
||||
userId: {
|
||||
type: "string",
|
||||
references: {
|
||||
model: "user",
|
||||
field: "id",
|
||||
},
|
||||
},
|
||||
webauthnUserID: {
|
||||
type: "string",
|
||||
},
|
||||
counter: {
|
||||
type: "number",
|
||||
},
|
||||
deviceType: {
|
||||
type: "string",
|
||||
},
|
||||
backedUp: {
|
||||
type: "boolean",
|
||||
},
|
||||
transports: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: "date",
|
||||
defaultValue: new Date(),
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -1,119 +0,0 @@
|
||||
---
|
||||
title: Username
|
||||
description: Username plugin
|
||||
---
|
||||
|
||||
The username plugin wraps the email and password authenticator and adds username support. This allows users to sign in and sign up with their username instead of their email.
|
||||
|
||||
## Qiuck setup
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Add Plugin to the server
|
||||
|
||||
```ts title="auth.ts" twoslash
|
||||
import { betterAuth } from "better-auth"
|
||||
import { username } from "better-auth/plugins"
|
||||
|
||||
const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
plugins: [ // [!code highlight]
|
||||
username() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Add the client plugin
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { usernameClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
usernameClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Signup with username
|
||||
|
||||
To signup a user with username, you can use the `signUp.username` function provided by the client. The `signUp` function takes an object with the following properties:
|
||||
|
||||
- `username`: The username of the user.
|
||||
- `email`: The email address of the user.
|
||||
- `password`: The password of the user. It should be at least 8 characters long and max 32 by default.
|
||||
- `name`: The name of the user.
|
||||
- `image`: The image of the user. (optional)
|
||||
- `callbackURL`: The url to redirect to after the user has signed up. (optional)
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { usernameClient } from "better-auth/client/plugins"
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
usernameClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
|
||||
const data = await client.signUp.username({
|
||||
username: "test",
|
||||
email: "test@email.com",
|
||||
password: "password1234",
|
||||
name: "test",
|
||||
image: "https://example.com/image.png",
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Signin with username
|
||||
|
||||
To signin 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:
|
||||
|
||||
- `username`: The username of the user.
|
||||
- `password`: The password of the user.
|
||||
- `callbackURL`: The url to redirect to after the user has signed in. (optional)
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { usernameClient } from "better-auth/client/plugins"
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
usernameClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
|
||||
const data = await client.signIn.username({
|
||||
username: "test",
|
||||
password: "password1234",
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Configuration
|
||||
|
||||
The username plugin doesn't require any configuration. It just needs to be added to the server and client.
|
||||
|
||||
## Database Schema
|
||||
|
||||
The username plugin requires a `username` field in the user table. If you're using better auth migration tool it will automatically add the `username` field to the user table. If not you can add it manually.
|
||||
|
||||
```ts
|
||||
const shcmea = {
|
||||
user: {
|
||||
username: {
|
||||
type: "string",
|
||||
unique: true,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: Components
|
||||
description: Components
|
||||
---
|
||||
|
||||
## Code Block
|
||||
|
||||
```js
|
||||
console.log('Hello World');
|
||||
```
|
||||
|
||||
## Cards
|
||||
|
||||
<Cards>
|
||||
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
|
||||
<Card title="Learn more about Fumadocs" href="https://fumadocs.vercel.app" />
|
||||
</Cards>
|
||||
@@ -1,9 +1,9 @@
|
||||
.beta {
|
||||
background-color: transparent;
|
||||
border: 0.2px solid #e2dddd;
|
||||
border: 0.2px solid #e2d0fe;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
color: #000;
|
||||
color: #970bf5;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
@@ -11,7 +11,7 @@
|
||||
margin: 0;
|
||||
outline: none;
|
||||
overflow: visible;
|
||||
padding: 0.7em 3em;
|
||||
padding: 0.4em 2em;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
.beta {
|
||||
color: white;
|
||||
background: white;
|
||||
background: rgb(225, 228, 255);
|
||||
}
|
||||
|
||||
.beta:hover {
|
||||
@@ -91,7 +91,7 @@
|
||||
|
||||
.beta::before {
|
||||
width: 0.9375rem;
|
||||
background: black;
|
||||
background: rgb(39, 37, 42);
|
||||
}
|
||||
|
||||
.beta .text {
|
||||
|
||||
@@ -13,48 +13,48 @@ import { Cover } from "../ui/cover";
|
||||
import { PulicBetaBadge } from "../beta/badge";
|
||||
|
||||
function Glow() {
|
||||
const id = useId();
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 -z-10 overflow-hidden bg-gradient-to-tr from-transparent via-stone-800/5 to-transparent/1 lg:right-[calc(max(2rem,50%-38rem)+40rem)] lg:min-w-[1rem]">
|
||||
<svg
|
||||
className="absolute -bottom-48 left-[-40%] h-[2rem] w-[10%] lg:-right-40 lg:bottom-auto lg:left-auto lg:top-[-40%] lg:h-[180%] lg:w-[80rem]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<radialGradient id={`${id}-desktop`} cx="100%">
|
||||
<stop offset="0%" stopColor="rgba(214, 211, 209, 0.05)" />
|
||||
<stop offset="53.95%" stopColor="rgba(214, 200, 209, 0.02)" />
|
||||
<stop offset="100%" stopColor="rgba(10, 14, 23, 0)" />
|
||||
</radialGradient>
|
||||
<radialGradient id={`${id}-mobile`} cy="100%">
|
||||
<stop offset="0%" stopColor="rgba(56, 189, 248, 0.05)" />
|
||||
<stop offset="53.95%" stopColor="rgba(0, 71, 255, 0.02)" />
|
||||
<stop offset="100%" stopColor="rgba(10, 14, 23, 0)" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect
|
||||
width="40%"
|
||||
height="40%"
|
||||
fill={`url(#${id}-desktop)`}
|
||||
className="hidden lg:block"
|
||||
/>
|
||||
<rect
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill={`url(#${id}-mobile)`}
|
||||
className="lg:hidden"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-x-0 bottom-0 right-0 h-px bg-white/5 mix-blend-overlay lg:left-auto lg:top-0 lg:h-auto lg:w-px" />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="absolute inset-0 -z-10 overflow-hidden bg-gradient-to-tr from-transparent via-stone-800/5 to-transparent/1 lg:right-[calc(max(2rem,50%-38rem)+40rem)] lg:min-w-[1rem]">
|
||||
<svg
|
||||
className="absolute -bottom-48 left-[-40%] h-[2rem] w-[10%] lg:-right-40 lg:bottom-auto lg:left-auto lg:top-[-40%] lg:h-[180%] lg:w-[80rem]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<radialGradient id={`${id}-desktop`} cx="100%">
|
||||
<stop offset="0%" stopColor="rgba(214, 211, 209, 0.05)" />
|
||||
<stop offset="53.95%" stopColor="rgba(214, 200, 209, 0.02)" />
|
||||
<stop offset="100%" stopColor="rgba(10, 14, 23, 0)" />
|
||||
</radialGradient>
|
||||
<radialGradient id={`${id}-mobile`} cy="100%">
|
||||
<stop offset="0%" stopColor="rgba(56, 189, 248, 0.05)" />
|
||||
<stop offset="53.95%" stopColor="rgba(0, 71, 255, 0.02)" />
|
||||
<stop offset="100%" stopColor="rgba(10, 14, 23, 0)" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect
|
||||
width="40%"
|
||||
height="40%"
|
||||
fill={`url(#${id}-desktop)`}
|
||||
className="hidden lg:block"
|
||||
/>
|
||||
<rect
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill={`url(#${id}-mobile)`}
|
||||
className="lg:hidden"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-x-0 bottom-0 right-0 h-px bg-white/5 mix-blend-overlay lg:left-auto lg:top-0 lg:h-auto lg:w-px" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: "auth.ts",
|
||||
code: `export const auth = betterAuth({
|
||||
{
|
||||
name: "auth.ts",
|
||||
code: `export const auth = betterAuth({
|
||||
database: {
|
||||
provider: "postgresql",
|
||||
url: process.env.DATABASE_URL,
|
||||
@@ -67,386 +67,381 @@ const tabs = [
|
||||
twoFactor(),
|
||||
]
|
||||
})`,
|
||||
},
|
||||
{
|
||||
name: "client.ts",
|
||||
code: `const client = createAuthClient({
|
||||
},
|
||||
{
|
||||
name: "client.ts",
|
||||
code: `const client = createAuthClient({
|
||||
plugins: [passkeyClient()]
|
||||
});
|
||||
`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function TrafficLightsIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg aria-hidden="true" viewBox="0 0 42 10" fill="none" {...props}>
|
||||
<circle cx="5" cy="5" r="4.5" />
|
||||
<circle cx="21" cy="5" r="4.5" />
|
||||
<circle cx="37" cy="5" r="4.5" />
|
||||
</svg>
|
||||
);
|
||||
return (
|
||||
<svg aria-hidden="true" viewBox="0 0 42 10" fill="none" {...props}>
|
||||
<circle cx="5" cy="5" r="4.5" />
|
||||
<circle cx="21" cy="5" r="4.5" />
|
||||
<circle cx="37" cy="5" r="4.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const env = process.env.NODE_ENV;
|
||||
|
||||
export default function Hero() {
|
||||
const theme = useTheme();
|
||||
const [activeTab, setActiveTab] = useState("auth.ts");
|
||||
const code = tabs.find((tab) => tab.name === activeTab)?.code ?? "";
|
||||
93
|
||||
return (
|
||||
<section className="w-full mx-auto px-10 flex min-h-[85vh] py-16 items-center justify-center gap-20">
|
||||
<div className="overflow-hidden bg-transparent dark:-mb-32 dark:mt-[-4.75rem] dark:pb-32 dark:pt-[4.75rem] md:px-10">
|
||||
<div className="grid max-w-full mx-auto grid-cols-1 items-center gap-x-8 gap-y-16 px-4 lg:max-w-8xl lg:grid-cols-2 lg:px-8 xl:gap-x-16 xl:px-12">
|
||||
<div className="relative z-10 md:text-center lg:text-left">
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-2 relative">
|
||||
<Cover>
|
||||
<p className="inline dark:text-white opacity-90 2xl md:text-3xl lg:text-5xl tracking-tight relative">
|
||||
Better Auth.
|
||||
</p>
|
||||
</Cover>
|
||||
</div>
|
||||
const theme = useTheme();
|
||||
const [activeTab, setActiveTab] = useState("auth.ts");
|
||||
const code = tabs.find((tab) => tab.name === activeTab)?.code ?? "";
|
||||
93;
|
||||
return (
|
||||
<section className="w-full mx-auto px-10 flex min-h-[85vh] py-16 items-center justify-center gap-20">
|
||||
<div className="overflow-hidden bg-transparent dark:-mb-32 dark:mt-[-4.75rem] dark:pb-32 dark:pt-[4.75rem] md:px-10">
|
||||
<div className="grid max-w-full mx-auto grid-cols-1 items-center gap-x-8 gap-y-16 px-4 lg:max-w-8xl lg:grid-cols-2 lg:px-8 xl:gap-x-16 xl:px-12">
|
||||
<div className="relative z-10 md:text-center lg:text-left">
|
||||
<div className="relative">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<div className="flex items-center gap-2 relative">
|
||||
<Cover>
|
||||
<p className="inline dark:text-white opacity-90 2xl md:text-3xl lg:text-5xl tracking-tight relative">
|
||||
Better Auth.
|
||||
</p>
|
||||
</Cover>
|
||||
</div>
|
||||
<PulicBetaBadge text="Public Beta" />
|
||||
</div>
|
||||
|
||||
<p className="mt-3 md:text-2xl tracking-tight dark:text-zinc-300 text-zinc-800">
|
||||
The most comprehensive authentication library for typescript.
|
||||
</p>
|
||||
{
|
||||
env === "production" ? <div className="flex items-center gap-2 mt-4">
|
||||
<PulicBetaBadge text="Coming Soon" />
|
||||
</div> : (
|
||||
<>
|
||||
<div className="mt-8 flex gap-4 font-sans md:justify-center lg:justify-start flex-col md:flex-row">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="px-4 md:px-8 py-1.5 border-2 border-black dark:border-stone-100 uppercase bg-white text-black transition duration-200 text-sm shadow-[1px_1px_rgba(0,0,0),2px_2px_rgba(0,0,0),3px_3px_rgba(0,0,0),4px_4px_rgba(0,0,0),5px_5px_0px_0px_rgba(0,0,0)] dark:shadow-[1px_1px_rgba(255,255,255),2px_2px_rgba(255,255,255),3px_3px_rgba(255,255,255),4px_4px_rgba(255,255,255),5px_5px_0px_0px_rgba(255,255,255)] dark:hover:shadow-sm hover:shadow-sm"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="flex rounded-none items-center gap-2"
|
||||
>
|
||||
<Github size={16} />
|
||||
View on GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 md:text-2xl tracking-tight dark:text-zinc-300 text-zinc-800">
|
||||
The most comprehensive authentication library for typescript.
|
||||
</p>
|
||||
{
|
||||
<>
|
||||
<div className="mt-8 flex gap-4 font-sans md:justify-center lg:justify-start flex-col md:flex-row">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="px-4 md:px-8 py-1.5 border-2 border-black dark:border-stone-100 uppercase bg-white text-black transition duration-200 text-sm shadow-[1px_1px_rgba(0,0,0),2px_2px_rgba(0,0,0),3px_3px_rgba(0,0,0),4px_4px_rgba(0,0,0),5px_5px_0px_0px_rgba(0,0,0)] dark:shadow-[1px_1px_rgba(255,255,255),2px_2px_rgba(255,255,255),3px_3px_rgba(255,255,255),4px_4px_rgba(255,255,255),5px_5px_0px_0px_rgba(255,255,255)] dark:hover:shadow-sm hover:shadow-sm"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
|
||||
<div className="relative lg:static xl:pl-10 hidden md:block">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 rounded-none bg-gradient-to-tr from-sky-300 via-sky-300/70 to-blue-300 opacity-5 blur-lg" />
|
||||
<div className="absolute inset-0 rounded-none bg-gradient-to-tr from-stone-300 via-stone-300/70 to-blue-300 opacity-5" />
|
||||
<LayoutGroup>
|
||||
<motion.div
|
||||
layoutId="hero"
|
||||
className="relative rounded-sm bg-gradient-to-tr from-stone-100 to-stone-200 dark:from-stone-950/70 dark:to-stone-950/90 ring-1 ring-white/10 backdrop-blur-lg"
|
||||
>
|
||||
<div className="absolute -top-px left-0 right-0 h-px " />
|
||||
<div className="absolute -bottom-px left-11 right-20 h-px" />
|
||||
<div className="pl-4 pt-4">
|
||||
<TrafficLightsIcon className="h-2.5 w-auto stroke-slate-500/30" />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="flex rounded-none items-center gap-2"
|
||||
>
|
||||
<Github size={16} />
|
||||
View on GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex space-x-2 text-xs">
|
||||
{tabs.map((tab) => (
|
||||
<motion.div
|
||||
key={tab.name}
|
||||
layoutId={`tab-${tab.name}`}
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
transition: { duration: 1 },
|
||||
}}
|
||||
whileTap={{ scale: 0.1 }}
|
||||
onClick={() => setActiveTab(tab.name)}
|
||||
className={clsx(
|
||||
"flex h-6 rounded-full cursor-pointer",
|
||||
activeTab === tab.name
|
||||
? "bg-gradient-to-r from-stone-400/90 via-stone-400 to-orange-400/20 p-px font-medium text-stone-300"
|
||||
: "text-slate-500",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center rounded-full px-2.5",
|
||||
tab.name === activeTab && "bg-stone-800",
|
||||
)}
|
||||
>
|
||||
{tab.name}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative lg:static xl:pl-10 hidden md:block">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 rounded-none bg-gradient-to-tr from-sky-300 via-sky-300/70 to-blue-300 opacity-5 blur-lg" />
|
||||
<div className="absolute inset-0 rounded-none bg-gradient-to-tr from-stone-300 via-stone-300/70 to-blue-300 opacity-5" />
|
||||
<LayoutGroup>
|
||||
<motion.div
|
||||
layoutId="hero"
|
||||
className="relative rounded-sm bg-gradient-to-tr from-stone-100 to-stone-200 dark:from-stone-950/70 dark:to-stone-950/90 ring-1 ring-white/10 backdrop-blur-lg"
|
||||
>
|
||||
<div className="absolute -top-px left-0 right-0 h-px " />
|
||||
<div className="absolute -bottom-px left-11 right-20 h-px" />
|
||||
<div className="pl-4 pt-4">
|
||||
<TrafficLightsIcon className="h-2.5 w-auto stroke-slate-500/30" />
|
||||
|
||||
<div className="mt-6 flex items-start px-1 text-sm">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="select-none border-r border-slate-300/5 pr-4 font-mono text-slate-600"
|
||||
>
|
||||
{Array.from({
|
||||
length: code.split("\n").length,
|
||||
}).map((_, index) => (
|
||||
<Fragment key={index}>
|
||||
{(index + 1).toString().padStart(2, "0")}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<Highlight
|
||||
key={theme.resolvedTheme}
|
||||
code={code}
|
||||
language={"javascript"}
|
||||
theme={{
|
||||
...themes.synthwave84,
|
||||
plain: {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{({
|
||||
className,
|
||||
style,
|
||||
tokens,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
}) => (
|
||||
<pre
|
||||
className={clsx(
|
||||
className,
|
||||
"flex overflow-x-auto pb-6",
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<code className="px-4">
|
||||
{tokens.map((line, lineIndex) => (
|
||||
<div
|
||||
key={lineIndex}
|
||||
{...getLineProps({ line })}
|
||||
>
|
||||
{line.map((token, tokenIndex) => (
|
||||
<span
|
||||
key={tokenIndex}
|
||||
{...getTokenProps({ token })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-4 flex space-x-2 text-xs">
|
||||
{tabs.map((tab) => (
|
||||
<motion.div
|
||||
key={tab.name}
|
||||
layoutId={`tab-${tab.name}`}
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
transition: { duration: 1 },
|
||||
}}
|
||||
whileTap={{ scale: 0.1 }}
|
||||
onClick={() => setActiveTab(tab.name)}
|
||||
className={clsx(
|
||||
"flex h-6 rounded-full cursor-pointer",
|
||||
activeTab === tab.name
|
||||
? "bg-gradient-to-r from-stone-400/90 via-stone-400 to-orange-400/20 p-px font-medium text-stone-300"
|
||||
: "text-slate-500"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center rounded-full px-2.5",
|
||||
tab.name === activeTab && "bg-stone-800"
|
||||
)}
|
||||
>
|
||||
{tab.name}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</code>
|
||||
|
||||
</pre>
|
||||
|
||||
)}
|
||||
</Highlight>
|
||||
{/* <Link href="https://demo.better-auth.com" target="_blank" className="ml-auto mr-4 flex items-center gap-2 mt-auto mb-4 hover:underline cursor-pointer">
|
||||
<div className="mt-6 flex items-start px-1 text-sm">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="select-none border-r border-slate-300/5 pr-4 font-mono text-slate-600"
|
||||
>
|
||||
{Array.from({
|
||||
length: code.split("\n").length,
|
||||
}).map((_, index) => (
|
||||
<Fragment key={index}>
|
||||
{(index + 1).toString().padStart(2, "0")}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<Highlight
|
||||
key={theme.resolvedTheme}
|
||||
code={code}
|
||||
language={"javascript"}
|
||||
theme={{
|
||||
...themes.synthwave84,
|
||||
plain: {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{({
|
||||
className,
|
||||
style,
|
||||
tokens,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
}) => (
|
||||
<pre
|
||||
className={clsx(
|
||||
className,
|
||||
"flex overflow-x-auto pb-6"
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<code className="px-4">
|
||||
{tokens.map((line, lineIndex) => (
|
||||
<div
|
||||
key={lineIndex}
|
||||
{...getLineProps({ line })}
|
||||
>
|
||||
{line.map((token, tokenIndex) => (
|
||||
<span
|
||||
key={tokenIndex}
|
||||
{...getTokenProps({ token })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
{/* <Link href="https://demo.better-auth.com" target="_blank" className="ml-auto mr-4 flex items-center gap-2 mt-auto mb-4 hover:underline cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 14 14"><path fill="currentColor" fillRule="evenodd" d="M2.676.02a1.74 1.74 0 0 0-.845.218a1.64 1.64 0 0 0-.895 1.433v10.677a1.64 1.64 0 0 0 .895 1.433a1.74 1.74 0 0 0 1.718-.016l8.63-5.338a1.61 1.61 0 0 0-.001-2.876L3.548.253A1.74 1.74 0 0 0 2.676.02" clipRule="evenodd"></path></svg>
|
||||
<p className="text-sm">
|
||||
Demo
|
||||
</p>
|
||||
</Link> */}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</LayoutGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<GridPattern
|
||||
className="absolute inset-x-0 -top-14 -z-10 h-full w-full dark:fill-secondary/30 fill-neutral-100 dark:stroke-secondary/30 stroke-neutral-700/5 [mask-image:linear-gradient(to_bottom_left,white_40%,transparent_50%)]"
|
||||
yOffset={-96}
|
||||
interactive
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</LayoutGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<GridPattern
|
||||
className="absolute inset-x-0 -top-14 -z-10 h-full w-full dark:fill-secondary/30 fill-neutral-100 dark:stroke-secondary/30 stroke-neutral-700/5 [mask-image:linear-gradient(to_bottom_left,white_40%,transparent_50%)]"
|
||||
yOffset={-96}
|
||||
interactive
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroBackground(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
const id = useId();
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 668 1069"
|
||||
width={668}
|
||||
height={1069}
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<clipPath id={`${id}-clip-path`}>
|
||||
<path
|
||||
fill="#fff"
|
||||
transform="rotate(-180 334 534.4)"
|
||||
d="M0 0h668v1068.8H0z"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g opacity=".4" clipPath={`url(#${id}-clip-path)`} strokeWidth={4}>
|
||||
<path
|
||||
opacity=".3"
|
||||
d="M584.5 770.4v-474M484.5 770.4v-474M384.5 770.4v-474M283.5 769.4v-474M183.5 768.4v-474M83.5 767.4v-474"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<path
|
||||
d="M83.5 221.275v6.587a50.1 50.1 0 0 0 22.309 41.686l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M83.5 716.012v6.588a50.099 50.099 0 0 0 22.309 41.685l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M183.7 584.5v6.587a50.1 50.1 0 0 0 22.31 41.686l55.581 37.054a50.097 50.097 0 0 1 22.309 41.685v6.588M384.101 277.637v6.588a50.1 50.1 0 0 0 22.309 41.685l55.581 37.054a50.1 50.1 0 0 1 22.31 41.686v6.587M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<path
|
||||
d="M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588M484.3 594.937v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054A50.1 50.1 0 0 0 384.1 721.95v6.587M484.3 872.575v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054a50.098 50.098 0 0 0-22.309 41.686v6.582M584.501 663.824v39.988a50.099 50.099 0 0 1-22.31 41.685l-55.581 37.054a50.102 50.102 0 0 0-22.309 41.686v6.587M283.899 945.637v6.588a50.1 50.1 0 0 1-22.309 41.685l-55.581 37.05a50.12 50.12 0 0 0-22.31 41.69v6.59M384.1 277.637c0 19.946 12.763 37.655 31.686 43.962l137.028 45.676c18.923 6.308 31.686 24.016 31.686 43.962M183.7 463.425v30.69c0 21.564 13.799 40.709 34.257 47.529l134.457 44.819c18.922 6.307 31.686 24.016 31.686 43.962M83.5 102.288c0 19.515 13.554 36.412 32.604 40.645l235.391 52.309c19.05 4.234 32.605 21.13 32.605 40.646M83.5 463.425v-58.45M183.699 542.75V396.625M283.9 1068.8V945.637M83.5 363.225v-141.95M83.5 179.524v-77.237M83.5 60.537V0M384.1 630.425V277.637M484.301 830.824V594.937M584.5 1068.8V663.825M484.301 555.275V452.988M584.5 622.075V452.988M384.1 728.537v-56.362M384.1 1068.8v-20.88M384.1 1006.17V770.287M283.9 903.888V759.85M183.699 1066.71V891.362M83.5 1068.8V716.012M83.5 674.263V505.175"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="384.1"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 384.1)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="200.399"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 200.399)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="81.412"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 81.412)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="183.699"
|
||||
cy="375.75"
|
||||
r="10.438"
|
||||
transform="rotate(-180 183.699 375.75)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="183.699"
|
||||
cy="563.625"
|
||||
r="10.438"
|
||||
transform="rotate(-180 183.699 563.625)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="384.1"
|
||||
cy="651.3"
|
||||
r="10.438"
|
||||
transform="rotate(-180 384.1 651.3)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="484.301"
|
||||
cy="574.062"
|
||||
r="10.438"
|
||||
transform="rotate(-180 484.301 574.062)"
|
||||
fill="#0EA5E9"
|
||||
fillOpacity=".42"
|
||||
stroke="#0EA5E9"
|
||||
/>
|
||||
<circle
|
||||
cx="384.1"
|
||||
cy="749.412"
|
||||
r="10.438"
|
||||
transform="rotate(-180 384.1 749.412)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="384.1"
|
||||
cy="1027.05"
|
||||
r="10.438"
|
||||
transform="rotate(-180 384.1 1027.05)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="283.9"
|
||||
cy="924.763"
|
||||
r="10.438"
|
||||
transform="rotate(-180 283.9 924.763)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="183.699"
|
||||
cy="870.487"
|
||||
r="10.438"
|
||||
transform="rotate(-180 183.699 870.487)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="283.9"
|
||||
cy="738.975"
|
||||
r="10.438"
|
||||
transform="rotate(-180 283.9 738.975)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="695.138"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 695.138)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="484.3"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 484.3)"
|
||||
fill="#0EA5E9"
|
||||
fillOpacity=".42"
|
||||
stroke="#0EA5E9"
|
||||
/>
|
||||
<circle
|
||||
cx="484.301"
|
||||
cy="432.112"
|
||||
r="10.438"
|
||||
transform="rotate(-180 484.301 432.112)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="584.5"
|
||||
cy="432.112"
|
||||
r="10.438"
|
||||
transform="rotate(-180 584.5 432.112)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="584.5"
|
||||
cy="642.95"
|
||||
r="10.438"
|
||||
transform="rotate(-180 584.5 642.95)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="484.301"
|
||||
cy="851.699"
|
||||
r="10.438"
|
||||
transform="rotate(-180 484.301 851.699)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="384.1"
|
||||
cy="256.763"
|
||||
r="10.438"
|
||||
transform="rotate(-180 384.1 256.763)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
const id = useId();
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 668 1069"
|
||||
width={668}
|
||||
height={1069}
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<clipPath id={`${id}-clip-path`}>
|
||||
<path
|
||||
fill="#fff"
|
||||
transform="rotate(-180 334 534.4)"
|
||||
d="M0 0h668v1068.8H0z"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g opacity=".4" clipPath={`url(#${id}-clip-path)`} strokeWidth={4}>
|
||||
<path
|
||||
opacity=".3"
|
||||
d="M584.5 770.4v-474M484.5 770.4v-474M384.5 770.4v-474M283.5 769.4v-474M183.5 768.4v-474M83.5 767.4v-474"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<path
|
||||
d="M83.5 221.275v6.587a50.1 50.1 0 0 0 22.309 41.686l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M83.5 716.012v6.588a50.099 50.099 0 0 0 22.309 41.685l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M183.7 584.5v6.587a50.1 50.1 0 0 0 22.31 41.686l55.581 37.054a50.097 50.097 0 0 1 22.309 41.685v6.588M384.101 277.637v6.588a50.1 50.1 0 0 0 22.309 41.685l55.581 37.054a50.1 50.1 0 0 1 22.31 41.686v6.587M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<path
|
||||
d="M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588M484.3 594.937v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054A50.1 50.1 0 0 0 384.1 721.95v6.587M484.3 872.575v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054a50.098 50.098 0 0 0-22.309 41.686v6.582M584.501 663.824v39.988a50.099 50.099 0 0 1-22.31 41.685l-55.581 37.054a50.102 50.102 0 0 0-22.309 41.686v6.587M283.899 945.637v6.588a50.1 50.1 0 0 1-22.309 41.685l-55.581 37.05a50.12 50.12 0 0 0-22.31 41.69v6.59M384.1 277.637c0 19.946 12.763 37.655 31.686 43.962l137.028 45.676c18.923 6.308 31.686 24.016 31.686 43.962M183.7 463.425v30.69c0 21.564 13.799 40.709 34.257 47.529l134.457 44.819c18.922 6.307 31.686 24.016 31.686 43.962M83.5 102.288c0 19.515 13.554 36.412 32.604 40.645l235.391 52.309c19.05 4.234 32.605 21.13 32.605 40.646M83.5 463.425v-58.45M183.699 542.75V396.625M283.9 1068.8V945.637M83.5 363.225v-141.95M83.5 179.524v-77.237M83.5 60.537V0M384.1 630.425V277.637M484.301 830.824V594.937M584.5 1068.8V663.825M484.301 555.275V452.988M584.5 622.075V452.988M384.1 728.537v-56.362M384.1 1068.8v-20.88M384.1 1006.17V770.287M283.9 903.888V759.85M183.699 1066.71V891.362M83.5 1068.8V716.012M83.5 674.263V505.175"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="384.1"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 384.1)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="200.399"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 200.399)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="81.412"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 81.412)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="183.699"
|
||||
cy="375.75"
|
||||
r="10.438"
|
||||
transform="rotate(-180 183.699 375.75)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="183.699"
|
||||
cy="563.625"
|
||||
r="10.438"
|
||||
transform="rotate(-180 183.699 563.625)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="384.1"
|
||||
cy="651.3"
|
||||
r="10.438"
|
||||
transform="rotate(-180 384.1 651.3)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="484.301"
|
||||
cy="574.062"
|
||||
r="10.438"
|
||||
transform="rotate(-180 484.301 574.062)"
|
||||
fill="#0EA5E9"
|
||||
fillOpacity=".42"
|
||||
stroke="#0EA5E9"
|
||||
/>
|
||||
<circle
|
||||
cx="384.1"
|
||||
cy="749.412"
|
||||
r="10.438"
|
||||
transform="rotate(-180 384.1 749.412)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="384.1"
|
||||
cy="1027.05"
|
||||
r="10.438"
|
||||
transform="rotate(-180 384.1 1027.05)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="283.9"
|
||||
cy="924.763"
|
||||
r="10.438"
|
||||
transform="rotate(-180 283.9 924.763)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="183.699"
|
||||
cy="870.487"
|
||||
r="10.438"
|
||||
transform="rotate(-180 183.699 870.487)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="283.9"
|
||||
cy="738.975"
|
||||
r="10.438"
|
||||
transform="rotate(-180 283.9 738.975)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="695.138"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 695.138)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="83.5"
|
||||
cy="484.3"
|
||||
r="10.438"
|
||||
transform="rotate(-180 83.5 484.3)"
|
||||
fill="#0EA5E9"
|
||||
fillOpacity=".42"
|
||||
stroke="#0EA5E9"
|
||||
/>
|
||||
<circle
|
||||
cx="484.301"
|
||||
cy="432.112"
|
||||
r="10.438"
|
||||
transform="rotate(-180 484.301 432.112)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="584.5"
|
||||
cy="432.112"
|
||||
r="10.438"
|
||||
transform="rotate(-180 584.5 432.112)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="584.5"
|
||||
cy="642.95"
|
||||
r="10.438"
|
||||
transform="rotate(-180 584.5 642.95)"
|
||||
fill="#1E293B"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="484.301"
|
||||
cy="851.699"
|
||||
r="10.438"
|
||||
transform="rotate(-180 484.301 851.699)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
<circle
|
||||
cx="384.1"
|
||||
cy="256.763"
|
||||
r="10.438"
|
||||
transform="rotate(-180 384.1 256.763)"
|
||||
stroke="#334155"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,68 +6,67 @@ import { NavLink } from "./nav-link";
|
||||
import { Logo } from "./logo";
|
||||
import { PulicBetaBadge } from "./beta/badge";
|
||||
|
||||
|
||||
const hideNavbar = process.env.NODE_ENV === "production"
|
||||
const hideNavbar = false;
|
||||
|
||||
export const Navbar = () => {
|
||||
return (
|
||||
<nav className="md:grid grid-cols-12 border-b sticky top-0 flex items-center justify-end bg-background backdrop-blur-md z-50">
|
||||
<Link
|
||||
href="/"
|
||||
className="md:border-r md:px-5 px-2.5 py-4 text-foreground md:col-span-4 lg:col-span-2 shrink-0 transition-colors min-w-[--fd-sidebar-width]"
|
||||
>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<Logo />
|
||||
<p>BETTER-AUTH.</p>
|
||||
</div>
|
||||
{/* <PulicBetaBadge /> */}
|
||||
</div>
|
||||
</Link>
|
||||
<div className="md:col-span-9 lg:col-span-10 flex items-center justify-end ">
|
||||
<ul className="md:flex items-center divide-x w-max border-r hidden shrink-0">
|
||||
{
|
||||
hideNavbar ? null : navMenu.map((menu, i) => (
|
||||
<NavLink key={menu.name} href={menu.path}>
|
||||
{menu.name}
|
||||
</NavLink>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<ThemeToggle />
|
||||
<NavbarMobileBtn />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
return (
|
||||
<nav className="md:grid grid-cols-12 border-b sticky top-0 flex items-center justify-end bg-background backdrop-blur-md z-50">
|
||||
<Link
|
||||
href="/"
|
||||
className="md:border-r md:px-5 px-2.5 py-4 text-foreground md:col-span-4 lg:col-span-2 shrink-0 transition-colors min-w-[--fd-sidebar-width]"
|
||||
>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<Logo />
|
||||
<p>BETTER-AUTH.</p>
|
||||
</div>
|
||||
{/* <PulicBetaBadge /> */}
|
||||
</div>
|
||||
</Link>
|
||||
<div className="md:col-span-9 lg:col-span-10 flex items-center justify-end ">
|
||||
<ul className="md:flex items-center divide-x w-max border-r hidden shrink-0">
|
||||
{hideNavbar
|
||||
? null
|
||||
: navMenu.map((menu, i) => (
|
||||
<NavLink key={menu.name} href={menu.path}>
|
||||
{menu.name}
|
||||
</NavLink>
|
||||
))}
|
||||
</ul>
|
||||
<ThemeToggle />
|
||||
<NavbarMobileBtn />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export const navMenu = [
|
||||
{
|
||||
name: "helo_",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "docs",
|
||||
path: "/docs",
|
||||
},
|
||||
// {
|
||||
// name: "plugins",
|
||||
// path: "/plugins",
|
||||
// },
|
||||
// {
|
||||
// name: "pre-made ui",
|
||||
// path: "/ui",
|
||||
// },
|
||||
// {
|
||||
// name: "security",
|
||||
// path: "/security",
|
||||
// },
|
||||
{
|
||||
name: "changelogs",
|
||||
path: "/changelogs",
|
||||
},
|
||||
// {
|
||||
// name: "resources",
|
||||
// path: "/resources",
|
||||
// },
|
||||
{
|
||||
name: "helo_",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "docs",
|
||||
path: "/docs",
|
||||
},
|
||||
// {
|
||||
// name: "plugins",
|
||||
// path: "/plugins",
|
||||
// },
|
||||
// {
|
||||
// name: "pre-made ui",
|
||||
// path: "/ui",
|
||||
// },
|
||||
// {
|
||||
// name: "security",
|
||||
// path: "/security",
|
||||
// },
|
||||
{
|
||||
name: "changelogs",
|
||||
path: "/changelogs",
|
||||
},
|
||||
// {
|
||||
// name: "resources",
|
||||
// path: "/resources",
|
||||
// },
|
||||
];
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Key,
|
||||
LucideAArrowDown,
|
||||
LucideIcon,
|
||||
PlusCircle,
|
||||
ScanFace,
|
||||
Users2,
|
||||
UserSquare2,
|
||||
@@ -405,17 +406,23 @@ export const contents: Content[] = [
|
||||
icon: Key,
|
||||
href: "/docs/plugins/bearer",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Examples",
|
||||
list: [
|
||||
{
|
||||
title: "Next JS",
|
||||
href: "/docs/examples/next-js",
|
||||
icon: Icons.nextJS,
|
||||
title: "Guide",
|
||||
group: true,
|
||||
href: "/docs/plugins/guide",
|
||||
icon: LucideAArrowDown,
|
||||
},
|
||||
],
|
||||
Icon: AppWindow,
|
||||
},
|
||||
// {
|
||||
// title: "Examples",
|
||||
// list: [
|
||||
// {
|
||||
// title: "Next JS",
|
||||
// href: "/docs/examples/next-js",
|
||||
// icon: Icons.nextJS,
|
||||
// },
|
||||
// ],
|
||||
// Icon: AppWindow,
|
||||
// },
|
||||
];
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Next js example
|
||||
descirption: Better auth next js example
|
||||
---
|
||||
|
||||
<iframe src="https://codesandbox.io/p/github/better-auth/better-auth/draft/dry-wind?workspaceId=f8670ba9-e253-4b60-91a6-369d03522afe&embed=1"
|
||||
<iframe src="https://codesandbox.io/p/github/Bekacru/better-auth-demo/main?hidedevtools=1&codemirror=1&fontsize=14&theme=dark"
|
||||
style={
|
||||
{
|
||||
width: "100%",
|
||||
@@ -16,5 +16,4 @@ descirption: Better auth next js example
|
||||
title="Bekacru/better-auth-demo/draft/awesome-chihiro"
|
||||
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
|
||||
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||
module="/demo/nextjs"
|
||||
></iframe>
|
||||
></iframe>
|
||||
@@ -20,39 +20,47 @@ This plugin offers two main methods of 2FA:
|
||||
- Managing trusted devices
|
||||
|
||||
|
||||
## Initial Setup
|
||||
|
||||
To get started with Two-Factor Authentication, follow these steps:
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Add the plugin to your auth config:
|
||||
### Add the plugin to your auth config
|
||||
|
||||
Add the two-factor plugin to your auth configuration and specify your app name as the issuer.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { twoFactor } from "better-auth/plugins"
|
||||
import { twoFactor } from "better-auth/plugins" // [!code highlight]
|
||||
|
||||
export const auth = await betterAuth({
|
||||
// ... other config options
|
||||
plugins: [
|
||||
twoFactor({
|
||||
issuer: "my-app"
|
||||
})
|
||||
twoFactor({ // [!code highlight]
|
||||
issuer: "my-app" // [!code highlight]
|
||||
}) // [!code highlight]
|
||||
]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Migrate your database:
|
||||
Run the migration to add the required fields to the user table.
|
||||
|
||||
### Migrate your database:
|
||||
this will add the following fields to the **user** table:
|
||||
|
||||
```bash
|
||||
npx better-auth migrate
|
||||
```
|
||||
- `twoFactorEnabled`: A boolean field to indicate if 2FA is enabled for the user.
|
||||
- `twoFactorSecret`: The secret key used to generate TOTP codes.
|
||||
- `twoFactorBackupCodes`: Encrypted backup codes for account recovery.
|
||||
|
||||
```bash
|
||||
npx better-auth migrate
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Add the client plugin:
|
||||
### Add the client Plugin
|
||||
|
||||
Add the client plugin and Specify where to redirect the user after enabling 2FA:
|
||||
|
||||
```ts title="client.ts"
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
@@ -84,7 +92,6 @@ const enableTwoFactor = async() => {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Sign In with 2FA
|
||||
|
||||
When a user with 2FA enabled tries to sign in, you'll need to verify their 2FA code. If they have 2FA enabled, they'll be redirected to the `twoFactorPage` where they can enter their 2FA code.
|
||||
@@ -250,29 +257,10 @@ When `trustDevice` is set to `true`, the current device will be remembered for 6
|
||||
Trusted devices enhance user convenience but should be used carefully, as they slightly reduce security. Encourage users to only trust personal, secure devices.
|
||||
</Callout>
|
||||
|
||||
## Configuration
|
||||
## Options
|
||||
|
||||
### Server
|
||||
|
||||
To configure the two factor plugin, you need to add the following code to your better auth instance.
|
||||
|
||||
```ts title="auth.ts" twoslash
|
||||
import { betterAuth } from "better-auth"
|
||||
import { twoFactor } from "better-auth/plugins"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
plugins: [ // [!code highlight]
|
||||
twoFactor({ // [!code highlight]
|
||||
issuer: "my-app" // [!code highlight]
|
||||
}) // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
|
||||
**Issuer**: The issuer is the name of your application. It's used to generate totp codes. It'll be displayed in the authenticator apps.
|
||||
|
||||
**TOTP options**
|
||||
@@ -356,32 +344,4 @@ const client = createAuthClient({
|
||||
|
||||
`twoFactorPage`: The page to redirect the user to after they have enabled 2-Factor. This is the page where the user will be redirected to verify their 2-Factor code.
|
||||
|
||||
`redirect`: If set to `false`, the user will not be redirected to the `twoFactorPage` after they have enabled 2-Factor.
|
||||
|
||||
|
||||
## Database Schema
|
||||
|
||||
Two factores requires additional fields on the user table. If you use better auth's migration system, it will automatically create this table for you.
|
||||
|
||||
```ts
|
||||
const schema = {
|
||||
user: {
|
||||
fields: {
|
||||
twoFactorEnabled: {
|
||||
type: "boolean",
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
twoFactorSecret: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
twoFactorBackupCodes: {
|
||||
type: "string",
|
||||
required: false,
|
||||
returned: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
`redirect`: If set to `false`, the user will not be redirected to the `twoFactorPage` after they have enabled 2-Factor.
|
||||
@@ -1,4 +1,517 @@
|
||||
---
|
||||
title: Organization
|
||||
descirption: The organization plugin allows you to manage your organization's members and teams.
|
||||
---
|
||||
---
|
||||
|
||||
Organizations simplifies user access and permissions management. Assign roles and permissions to streamline project management, team coordination, and partnerships.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Add the plugin to your **auth** config
|
||||
```ts title="auth.ts" twoslash
|
||||
import { betterAuth } from "better-auth"
|
||||
import { organization } from "better-auth/plugins"
|
||||
|
||||
export const auth = await betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
plugins: [ // [!code highlight]
|
||||
organization() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Migarate database
|
||||
|
||||
Migrate your database to add the necessary tables and fields.
|
||||
|
||||
This will create the following tables:
|
||||
|
||||
- **organization**: Stores the organization's data. Includes the name, slug, logo, and other information.
|
||||
- **member**: Stores the members of the organization. Includes the user id, organization id, role, and other information.
|
||||
- **invitation**: Stores the invitations sent to users. Includes the email, role, organization, the status, and other information.
|
||||
|
||||
```bash
|
||||
npx better-auth migrate
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Add the client plugin
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { organizationClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
organizationClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Once you've installed the plugin, you can start using the organization plugin to manage your organization's members and teams. The client plugin will provide you methods under the `organization` namespace. And the server `api` will provide you with the necessary endpoints to manage your organization and gives you easier way to call the functions on your own backend.
|
||||
|
||||
## Organization
|
||||
|
||||
### 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="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { organizationClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
organizationClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
const data = await client.organization.create({
|
||||
name: "My Organization",
|
||||
slug: "my-org",
|
||||
logo: "https://example.com/logo.png"
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
#### Restrict who can create an organization
|
||||
|
||||
By default, any user can create an organization. To restrict this, set the `allowUserToCreateOrganization` option to a function that returns a boolean, or directly to `true` or `false`.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { organization } from "better-auth/plugins"
|
||||
|
||||
const auth = await betterAuth({
|
||||
//...
|
||||
plugins: [
|
||||
organization({
|
||||
allowUserToCreateOrganization: async (user) => { // [!code highlight]
|
||||
const subscription = await getSubscription(user.id) // [!code highlight]
|
||||
return subscription.plan === "pro" // [!code highlight]
|
||||
} // [!code highlight]
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### List User's Organizations
|
||||
|
||||
To list the organizations that a user is a member of, you can use `useListOrganizations` hook. It implments a reactive way to get the orSvelteganizations that the user is a member of.
|
||||
|
||||
<Tabs items={["React", "Vue", "Svelte"]} defaultValue="React">
|
||||
<Tab value="React">
|
||||
```tsx title="client.tsx"
|
||||
import { client } from "@/auth/client"
|
||||
|
||||
function App(){
|
||||
const { data: organizations } = client.useListOrganizations()
|
||||
return (
|
||||
<div>
|
||||
{organizations.map(org => <p>{org.name}</p>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab value="Svelte">
|
||||
```svelte title="page.svelte"
|
||||
<script lang="ts">
|
||||
import { client } from "$lib/client";
|
||||
const organizations = client.useListOrganizations();
|
||||
</script>
|
||||
|
||||
<h1>Organizations</h1>s
|
||||
|
||||
{#if $organizations.isPending}
|
||||
<p>Loading...</p>
|
||||
{:else if $organizations.data === null}
|
||||
<p>No organizations found.</p>
|
||||
{:else}
|
||||
<ul>
|
||||
{#each $organizations.data as organization}
|
||||
<li>{organization.name}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab value="Vue">
|
||||
```vue title="organization.vue"
|
||||
<script lang="ts">;
|
||||
export default {
|
||||
setup() {
|
||||
const organizations = client.useListOrganizations()
|
||||
return { organizations };
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Organizations</h1>
|
||||
<div v-if="organizations.isPending">Loading...</div>
|
||||
<div v-else-if="organizations.data === null">No organizations found.</div>
|
||||
<ul v-else>
|
||||
<li v-for="organization in organizations.data" :key="organization.id">
|
||||
{{ organization.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
### Active Organization
|
||||
|
||||
Active organization is the workspace the user is currently working on. By defualt when the user is signed in the active organization is set to `null`.
|
||||
|
||||
#### Set Active Organization
|
||||
|
||||
You can set the active organization by calling the `organization.setActive` function. It'll set the active organization for the user both on the client state and the session on the server.
|
||||
|
||||
```ts title="client.ts"
|
||||
client.organization.setActive("organization-id")
|
||||
```
|
||||
|
||||
#### Use Active Organization
|
||||
|
||||
To retrieve the active organization for the user, you can call the `useActiveOrganization` hook. It returns the active organization for the user. Whenever the active organization changes, the hook will re-evaluate and return the new active organization.
|
||||
|
||||
<Tabs items={['React', 'Vue', 'Svelte']}>
|
||||
<Tab value="React">
|
||||
```tsx title="client.tsx"
|
||||
import { client } from "@/auth/client"
|
||||
|
||||
function App(){
|
||||
const { data: activeOrganization } = client.useActiveOrganization()
|
||||
return (
|
||||
<div>
|
||||
{activeOrganization ? <p>{activeOrganization.name}</p> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="Svelte">
|
||||
```tsx title="client.tsx"
|
||||
<script lang="ts">
|
||||
import { client } from "$lib/client";
|
||||
const activeOrganization = client.useActiveOrganization();
|
||||
</script>
|
||||
|
||||
<h2>Active Organization</h2>
|
||||
|
||||
{#if $activeOrganization.isPending}
|
||||
<p>Loading...</p>
|
||||
{:else if $activeOrganization.data === null}
|
||||
<p>No active organization found.</p>
|
||||
{:else}
|
||||
<p>{$activeOrganization.data.name}</p>
|
||||
{/if}
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="Vue">
|
||||
```vue title="organization.vue"
|
||||
<script lang="ts">;
|
||||
export default {
|
||||
setup() {
|
||||
const activeOrganization = client.useActiveOrganization();
|
||||
return { activeOrganization };
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2>Active organization</h2>
|
||||
<div v-if="activeOrganization.isPending">Loading...</div>
|
||||
<div v-else-if="activeOrganization.data === null">No active organization.</div>
|
||||
<div v-else>
|
||||
{{ activeOrganization.data.name }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Invitations
|
||||
|
||||
To add a member to an organization, we first need to send an invitation to the user. The user will receive an email/sms with the invitation link. Once the user accepts the invitation, they will be added to the organization.
|
||||
|
||||
### Setup Invitation Email
|
||||
|
||||
For member invitation to work we first need to provider `sendInvitationEmail` to the `better-auth` instance. This function is responsible for sending the invitation email to the user.
|
||||
|
||||
You'll need to construct and send the invitation link to the user. The link should include the invitation ID, which will be used with the acceptInvitation function when the user clicks on it.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { organization } from "better-auth/plugins"
|
||||
import { sendOrganizationInvitation } from "./email"
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
organization({
|
||||
async sendInvitationEmail(data) {
|
||||
const inviteLink = `https://example.com/accept-invitation/${data.id}`
|
||||
sendOrganizationInvitation({
|
||||
email: data.email,
|
||||
invitedByUsername: data.inviter.user.name,
|
||||
invitedByEmail: data.inviter.user.email,
|
||||
teamName: data.organization.name,
|
||||
inviteLink
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Send Invitation
|
||||
|
||||
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 client.organization.inviteMember({
|
||||
email: "test@email.com",
|
||||
role: "admin",
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### Accept Invitation
|
||||
|
||||
When a user receives an invitation email, they can click on the invitation link to accept the invitation. The invitation link should include the invitation ID, which will be used to accept the invitation.
|
||||
|
||||
Make sure to call the `acceptInvitation` function after the user is logged in.
|
||||
|
||||
```ts title="client.ts"
|
||||
await client.organization.acceptInvitation({
|
||||
invitationId: "invitation-id"
|
||||
})
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```ts title="client.ts"
|
||||
//cancel invitation
|
||||
await client.organization.cancelInvitation({
|
||||
invitationId: "invitation-id"
|
||||
})
|
||||
|
||||
//reject invitation (needs to be called when the user who received the invitation is logged in)
|
||||
await client.organization.rejectInvitation({
|
||||
invitationId: "invitation-id"
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
```ts title="client.ts"
|
||||
client.organization.getInvitation({
|
||||
query: {
|
||||
id: params.id
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Remove Memebr
|
||||
|
||||
To remove you can use `organization.removeMember`
|
||||
|
||||
```ts title="client.ts"
|
||||
//remove member
|
||||
await client.organization.removeMember({
|
||||
memberId: "member-id"
|
||||
})
|
||||
```
|
||||
|
||||
### Update Member Role
|
||||
|
||||
To updadate 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="client.ts"
|
||||
await client.organization.updateMemberRole({
|
||||
memberId: "member-id",
|
||||
role: "admin"
|
||||
})
|
||||
```
|
||||
|
||||
## Access Control
|
||||
|
||||
The organization plugin providers a very flexible access control system. You can control the access of the user based on the role they have in the organization. You can define your own set of permissions based on the role of the user.
|
||||
|
||||
### Roles
|
||||
|
||||
currently only three roles are supported:
|
||||
|
||||
`owner`: The user who created the organization by default. The owner has full control over the organization and can perform any action.
|
||||
|
||||
`admin`: Users with the admin role have full control over the organization except for deleting the organization or changing the owner.
|
||||
|
||||
`member`: Users with the member role have limited control over the organization. They can create projects, invite users, and manage projects they have created.
|
||||
|
||||
<Callout type="warn">
|
||||
Currently, you can't create custom roles
|
||||
</Callout>
|
||||
|
||||
### Permissons
|
||||
|
||||
By defualt there are 3 resources and they have 2 to 3 actions.
|
||||
|
||||
**organization**:
|
||||
|
||||
`update` `delete`
|
||||
|
||||
**member**:
|
||||
|
||||
`create` `update` `delete`
|
||||
|
||||
**invitation**:
|
||||
|
||||
`create` `cancel`
|
||||
|
||||
The owner have full control over all the resources and actions. The admin have full control over all the resources except for deleting the organization or changing the owner. The member have no control over any of those action other than reading the data.
|
||||
|
||||
### Custom Permissions
|
||||
|
||||
the plugin providers easy way to define your own set of permission for each role.
|
||||
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
#### Create Access Control
|
||||
|
||||
You first need to create access controller by calling `createAccessControl` function and passing the statement object. The statement object should have the resource name as the key and the array of actions as the value.
|
||||
```ts title="permissions.ts"
|
||||
import { createAccessControl } from "better-auth/plugins/access";
|
||||
|
||||
/**
|
||||
* make sure to use `as const` so typescript can infer the type correctly
|
||||
*/
|
||||
const statement = { // [!code highlight]
|
||||
project: ["create", "share", "update", "delete"], // [!code highlight]
|
||||
} as const; // [!code highlight]
|
||||
|
||||
const ac = createAccessControl(statement); // [!code highlight]
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
#### Create Roles
|
||||
|
||||
Once you have created the access controller you can create roles with the permissions you have defined.
|
||||
|
||||
```ts title="permissions.ts"
|
||||
import { createAccessControl } from "better-auth/plugins/access";
|
||||
|
||||
const statement = {
|
||||
project: ["create", "share", "update", "delete"],
|
||||
} as const;
|
||||
|
||||
const ac = createAccessControl(statement);
|
||||
|
||||
const member = ac.newRole({ // [!code highlight]
|
||||
project: ["create"], // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
|
||||
const admin = ac.newRole({ // [!code highlight]
|
||||
project: ["create", "update"], // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
|
||||
const owner = ac.newRole({ // [!code highlight]
|
||||
project: ["create", "update", "delete"], // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
#### Pass Roles to the Plugin
|
||||
|
||||
Once you have created the roles you can pass them to the organization plugin both on the client and the server.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { ac, owner, admin, member } from "@/auth/permissions"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { organization } from "better-auth/plugins"
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
organization({
|
||||
ac: ac,
|
||||
roles: {
|
||||
owner,
|
||||
admin,
|
||||
member
|
||||
}
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
You also need to pass the access controller to the client plugin. This is useful for type checking and auto-completion.
|
||||
|
||||
```ts title="auth-client"
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { organizationClient } from "better-auth/client/plugins"
|
||||
import { ac } from "@/auth/permissions"
|
||||
|
||||
export const client = createAuthClient({
|
||||
plugins: [
|
||||
organizationClient({
|
||||
ac: ac,
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
**allowUserToCreateOrganization**: `boolean` | `((user: User) => Promise<boolean> | boolean)` - A function that determines whether a user can create an organization. By default, it's `true`. You can set it to `false` to restrict users from creating organizations.
|
||||
|
||||
**organizationLimit**: `numbe` | `((user: User) => Promise<boolean> | boolean)` - The maximum number of organizations allowed for a user. By default, it's `5`. You can set it to any number you want or a function that returns a boolean.
|
||||
|
||||
**creatorRole**: `admin | owner` - The role of the user who creates the organization. By default, it's `owner`. You can set it to `admin`.
|
||||
|
||||
**membershipLimit**: `number` - The maximum number of members allowed in an organization. By default, it's `100`. You can set it to any number you want.
|
||||
|
||||
**sendInvitationEmail**: `async (data) => Promise<void>` - A function that sends an invitation email to the user.
|
||||
|
||||
**invitationExpiresIn** : `number` - How long the invitation link is valid for in seconds. By default, it's 48 hours (2 days).
|
||||
@@ -7,7 +7,7 @@ Passkeys are a secure, passwordless authentication method using cryptographic ke
|
||||
|
||||
The passkey plugin implementation is powered by [simple-web-authn](https://simplewebauthn.dev/) behind the scenes.
|
||||
|
||||
## Quick setup
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
@@ -32,11 +32,7 @@ The passkey plugin implementation is powered by [simple-web-authn](https://simpl
|
||||
url: "./db.sqlite",
|
||||
},
|
||||
plugins: [ // [!code highlight]
|
||||
passkey({ // [!code highlight]
|
||||
rpID: "localhost", // [!code highlight]
|
||||
rpName: "BetterAuth", // [!code highlight]
|
||||
origin: "http://localhost:3000", // [!code highlight]
|
||||
}), // [!code highlight]
|
||||
passkey(), // [!code highlight]
|
||||
], // [!code highlight]
|
||||
})
|
||||
```
|
||||
@@ -55,56 +51,54 @@ The passkey plugin implementation is powered by [simple-web-authn](https://simpl
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Add a passkey
|
||||
|
||||
To add a passkey make sure a user is authenticated and then call the `passkey.addPasskey` function provided by the client.
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { passkeyClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
passkeyClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
const data = await client.passkey.addPasskey()
|
||||
```
|
||||
This will prompt the user to register a passkey. And it'll add the passkey to the user's account.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Signin with a passkey
|
||||
|
||||
To signin with a passkey you can use the passkeySignIn 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)
|
||||
|
||||
`callbackURL`: The URL to redirect to after the user has signed in. (optional)
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { passkeyClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
passkeyClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
const data = await client.signIn.passkey()
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
## Passkey Configuration
|
||||
## Usage
|
||||
|
||||
### Add/Register a passkey
|
||||
|
||||
To add or register a passkey make sure a user is authenticated and then call the `passkey.addPasskey` function provided by the client.
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { passkeyClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
passkeyClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
const data = await client.passkey.addPasskey()
|
||||
```
|
||||
This will prompt the user to register a passkey. And it'll add the passkey to the user's account.
|
||||
|
||||
### Signin with a passkey
|
||||
|
||||
To signin with a passkey you can use the passkeySignIn 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)
|
||||
|
||||
`callbackURL`: The URL to redirect to after the user has signed in. (optional)
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { passkeyClient } from "better-auth/client/plugins"
|
||||
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
passkeyClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
const data = await client.signIn.passkey()
|
||||
```
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
**rpID**: A unique identifier for your website. 'localhost' is okay for local dev.
|
||||
|
||||
@@ -112,47 +106,3 @@ The passkey plugin implementation is powered by [simple-web-authn](https://simpl
|
||||
|
||||
**origin**: The URL at which registrations and authentications should occur. 'http://localhost' and 'http://localhost:PORT' are also valid. Do NOT include any trailing /.
|
||||
|
||||
|
||||
## Database Schema
|
||||
|
||||
Passkey requires a database table called `passkey` with the following fields. If you use better auth's migration system, it will automatically create this table for you.
|
||||
|
||||
```ts
|
||||
const schema = {
|
||||
passkey: {
|
||||
fields: {
|
||||
publicKey: {
|
||||
type: "string",
|
||||
},
|
||||
userId: {
|
||||
type: "string",
|
||||
references: {
|
||||
model: "user",
|
||||
field: "id",
|
||||
},
|
||||
},
|
||||
webauthnUserID: {
|
||||
type: "string",
|
||||
},
|
||||
counter: {
|
||||
type: "number",
|
||||
},
|
||||
deviceType: {
|
||||
type: "string",
|
||||
},
|
||||
backedUp: {
|
||||
type: "boolean",
|
||||
},
|
||||
transports: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: "date",
|
||||
defaultValue: new Date(),
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -5,7 +5,7 @@ description: Username plugin
|
||||
|
||||
The username plugin wraps the email and password authenticator and adds username support. This allows users to sign in and sign up with their username instead of their email.
|
||||
|
||||
## Qiuck setup
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
@@ -26,6 +26,19 @@ The username plugin wraps the email and password authenticator and adds username
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Migrate the database
|
||||
|
||||
Run the migration to add the required fields to the user table.
|
||||
|
||||
This will add the following fields to the **user** table:
|
||||
|
||||
- `username`: The username of the user.
|
||||
|
||||
```bash
|
||||
npx better-auth migrate
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Add the client plugin
|
||||
|
||||
@@ -40,80 +53,64 @@ The username plugin wraps the email and password authenticator and adds username
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Signup with username
|
||||
|
||||
To signup a user with username, you can use the `signUp.username` function provided by the client. The `signUp` function takes an object with the following properties:
|
||||
|
||||
- `username`: The username of the user.
|
||||
- `email`: The email address of the user.
|
||||
- `password`: The password of the user. It should be at least 8 characters long and max 32 by default.
|
||||
- `name`: The name of the user.
|
||||
- `image`: The image of the user. (optional)
|
||||
- `callbackURL`: The url to redirect to after the user has signed up. (optional)
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { usernameClient } from "better-auth/client/plugins"
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
usernameClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
|
||||
const data = await client.signUp.username({
|
||||
username: "test",
|
||||
email: "test@email.com",
|
||||
password: "password1234",
|
||||
name: "test",
|
||||
image: "https://example.com/image.png",
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Signin with username
|
||||
|
||||
To signin 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:
|
||||
|
||||
- `username`: The username of the user.
|
||||
- `password`: The password of the user.
|
||||
- `callbackURL`: The url to redirect to after the user has signed in. (optional)
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { usernameClient } from "better-auth/client/plugins"
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
usernameClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
|
||||
const data = await client.signIn.username({
|
||||
username: "test",
|
||||
password: "password1234",
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Configuration
|
||||
## Usage
|
||||
|
||||
### Signup with username
|
||||
|
||||
To signup a user with username, you can use the `signUp.username` function provided by the client. The `signUp` function takes an object with the following properties:
|
||||
|
||||
- `username`: The username of the user.
|
||||
- `email`: The email address of the user.
|
||||
- `password`: The password of the user. It should be at least 8 characters long and max 32 by default.
|
||||
- `name`: The name of the user.
|
||||
- `image`: The image of the user. (optional)
|
||||
- `callbackURL`: The url to redirect to after the user has signed up. (optional)
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { usernameClient } from "better-auth/client/plugins"
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
usernameClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
|
||||
const data = await client.signUp.username({
|
||||
username: "test",
|
||||
email: "test@email.com",
|
||||
password: "password1234",
|
||||
name: "test",
|
||||
image: "https://example.com/image.png",
|
||||
})
|
||||
```
|
||||
|
||||
### Signin with username
|
||||
|
||||
To signin 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:
|
||||
|
||||
- `username`: The username of the user.
|
||||
- `password`: The password of the user.
|
||||
- `callbackURL`: The url to redirect to after the user has signed in. (optional)
|
||||
|
||||
```ts title="client.ts" twoslash
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { usernameClient } from "better-auth/client/plugins"
|
||||
const client = createAuthClient({
|
||||
plugins: [ // [!code highlight]
|
||||
usernameClient() // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
// ---cut---
|
||||
|
||||
const data = await client.signIn.username({
|
||||
username: "test",
|
||||
password: "password1234",
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
The username plugin doesn't require any configuration. It just needs to be added to the server and client.
|
||||
|
||||
## Database Schema
|
||||
|
||||
The username plugin requires a `username` field in the user table. If you're using better auth migration tool it will automatically add the `username` field to the user table. If not you can add it manually.
|
||||
|
||||
```ts
|
||||
const shcmea = {
|
||||
user: {
|
||||
username: {
|
||||
type: "string",
|
||||
unique: true,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export default function middleware(req: NextRequest) {
|
||||
if (
|
||||
req.nextUrl.pathname.startsWith("/docs") && !req.nextUrl.searchParams.has("allow_docs") &&
|
||||
process.env.NODE_ENV === "production"
|
||||
) {
|
||||
return NextResponse.redirect(new URL("/", req.url));
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: "/docs/:path*",
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins";
|
||||
import { remarkInstall } from "fumadocs-docgen";
|
||||
import { transformerTwoslash } from "fumadocs-twoslash";
|
||||
import { JsxEmit, ModuleResolutionKind } from "typescript";
|
||||
|
||||
const withMDX = createMDX({
|
||||
mdxOptions: {
|
||||
rehypeCodeOptions: {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codesandbox/sandpack-react": "^2.19.8",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
@@ -65,12 +66,14 @@
|
||||
"oslo": "^1.2.1",
|
||||
"prism-react-renderer": "^2.4.0",
|
||||
"react": "^18.3.1",
|
||||
"react-codesandboxer": "^3.1.5",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"react-resizable-panels": "^2.1.2",
|
||||
"recharts": "^2.12.7",
|
||||
"rehype-mermaid": "^2.1.0",
|
||||
"remark-codesandbox": "^0.10.1",
|
||||
"sonner": "^1.5.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
@@ -82,6 +85,7 @@
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "22.3.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-codesandboxer": "^3.1.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.41",
|
||||
|
||||
@@ -22,7 +22,7 @@ type InferResolvedHooks<O extends ClientOptions> = O["plugins"] extends Array<
|
||||
? never
|
||||
: key extends string
|
||||
? `use${Capitalize<key>}`
|
||||
: never]: Atoms[key];
|
||||
: never]:()=>Atoms[key];
|
||||
}
|
||||
: {}
|
||||
: {}
|
||||
@@ -41,13 +41,13 @@ export function createAuthClient<Option extends ClientOptions>(
|
||||
} = getClientConfig(options);
|
||||
let resolvedHooks: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(pluginsAtoms)) {
|
||||
resolvedHooks[`use${capitalizeFirstLetter(key)}`] = value;
|
||||
resolvedHooks[`use${capitalizeFirstLetter(key)}`] = () => value;
|
||||
}
|
||||
const { $session, _sessionSignal, $Infer } = getSessionAtom<Option>($fetch);
|
||||
const routes = {
|
||||
...pluginsActions,
|
||||
...resolvedHooks,
|
||||
useSession: $session,
|
||||
useSession: ()=>$session,
|
||||
};
|
||||
const proxy = createDynamicPathProxy(
|
||||
routes,
|
||||
|
||||
@@ -56,9 +56,11 @@ export interface OrganizationOptions {
|
||||
| boolean
|
||||
| ((user: User) => Promise<boolean> | boolean);
|
||||
/**
|
||||
* The maximum number of organizations a user can create.
|
||||
*
|
||||
* You can also pass a function that returns a boolean
|
||||
*/
|
||||
organizationLimit?: number;
|
||||
organizationLimit?: number | ((user: User) => Promise<boolean> | boolean);
|
||||
/**
|
||||
* The role that is assigned to the creator of the organization.
|
||||
*
|
||||
|
||||
@@ -284,7 +284,7 @@ export const cancelInvitation = createAuthEndpoint(
|
||||
);
|
||||
|
||||
export const getInvitation = createAuthEndpoint(
|
||||
"/organization/get-active-invitation",
|
||||
"/organization/get-invitation",
|
||||
{
|
||||
method: "GET",
|
||||
use: [orgMiddleware],
|
||||
|
||||
@@ -31,6 +31,7 @@ export const createOrganization = createAuthEndpoint(
|
||||
: options?.allowUserToCreateOrganization === undefined
|
||||
? true
|
||||
: options.allowUserToCreateOrganization;
|
||||
|
||||
if (!canCreateOrg) {
|
||||
return ctx.json(null, {
|
||||
status: 403,
|
||||
@@ -40,6 +41,24 @@ export const createOrganization = createAuthEndpoint(
|
||||
});
|
||||
}
|
||||
const adapter = getOrgAdapter(ctx.context.adapter, options);
|
||||
|
||||
const userOrganizations = await adapter.listOrganizations(user.id);
|
||||
const hasReachedOrgLimit =
|
||||
typeof options.organizationLimit === "number"
|
||||
? userOrganizations.length >= options.organizationLimit
|
||||
: typeof options.organizationLimit === "function"
|
||||
? await options.organizationLimit(user)
|
||||
: false;
|
||||
|
||||
if (hasReachedOrgLimit) {
|
||||
return ctx.json(null, {
|
||||
status: 403,
|
||||
body: {
|
||||
message: "You have reached the maximum number of organizations",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const existingOrganization = await adapter.findOrganizationBySlug(
|
||||
ctx.body.slug,
|
||||
);
|
||||
|
||||
1761
pnpm-lock.yaml
generated
1761
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user