feat: docs and new migration cli

This commit is contained in:
Bereket Engida
2024-09-02 09:41:45 +03:00
parent 3ac5916da7
commit 7234291718
40 changed files with 2692 additions and 815 deletions

View File

@@ -30,10 +30,8 @@ export default function ResetPassword({
setIsSubmitting(true);
setError("");
const res = await authClient.resetPassword({
body: {
token,
newPassword: password,
},
token,
newPassword: password,
});
}
return (

View File

@@ -1,9 +1,17 @@
import { createAuthClient } from "better-auth/react";
import { organization } from "better-auth/client";
import {
organizationClient,
twoFactorClient,
passkeyClient,
} from "better-auth/client";
export const authClient = createAuthClient({
baseURL: "http://localhost:3000/api/auth",
authPlugins: [organization],
authPlugins: [
organizationClient(),
twoFactorClient({ twoFactorPage: "/two-factor" }),
passkeyClient,
],
});
export const {
@@ -12,3 +20,5 @@ export const {
useInvitation,
useListOrganization,
} = authClient;
authClient.signInPasskey();

View File

@@ -34,7 +34,6 @@ export const auth = betterAuth({
}),
twoFactor({
issuer: "BetterAuth",
twoFactorURL: "/two-factor",
otpOptions: {
async sendOTP(user, otp) {
console.log({ user, otp });

View File

@@ -2,9 +2,7 @@
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 360 74% 95%;
--foreground: 360 5% 0%;

View File

@@ -4,15 +4,16 @@ import { RootProvider } from 'fumadocs-ui/provider';
import { Inter } from 'next/font/google';
import type { ReactNode } from 'react';
import { NavbarProvider } from '@/components/nav-mobile';
import { GeistMono } from "geist/font/mono";
import { GeistSans } from "geist/font/sans";
const inter = Inter({
subsets: ['latin'],
});
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en" className={inter.className} suppressHydrationWarning>
<body>
<html lang="en" suppressHydrationWarning>
<body className={`${GeistSans.variable} ${GeistMono.variable} font-sans`}>
<RootProvider>
<NavbarProvider>
<Navbar />

View File

@@ -1,19 +1,19 @@
import Link from 'next/link';
import { } from "better-auth/client";
import Section from '@/components/landing/section';
import Hero from '@/components/landing/hero';
import { Separator } from '@/components/ui/separator';
export default function HomePage() {
return (
<main className="flex h-screen flex-col justify-center text-center">
<h1 className="mb-4 text-2xl font-bold">Hello World</h1>
<p className="text-fd-muted-foreground">
You can open{' '}
<Link
href="/docs"
className="text-fd-foreground font-semibold underline"
>
/docs
</Link>{' '}
and see the documentation.
</p>
</main>
<Section
className="-z-1 mb-1"
crosses
crossesOffset="lg:translate-y-[5.25rem]"
customPaddings
id="hero"
>
<Hero />
<Separator className="w-full" />
</Section>
);
}

View File

@@ -0,0 +1,129 @@
"use client";
import { motion } from "framer-motion";
import { useEffect, useId, useRef, useState } from "react";
const Block = ({
x,
y,
...props
}: Omit<React.ComponentPropsWithoutRef<typeof motion.path>, "x" | "y"> & {
x: number;
y: number;
}) => {
return (
<motion.path
transform={`translate(${-32 * y + 96 * x} ${160 * y})`}
d="M45.119 4.5a11.5 11.5 0 0 0-11.277 9.245l-25.6 128C6.82 148.861 12.262 155.5 19.52 155.5h63.366a11.5 11.5 0 0 0 11.277-9.245l25.6-128c1.423-7.116-4.02-13.755-11.277-13.755H45.119Z"
{...props}
/>
);
};
export const GridPattern = ({ yOffset = 0, interactive = false, ...props }) => {
const id = useId();
const ref = useRef<React.ElementRef<"svg">>(null);
const currentBlock = useRef<[x: number, y: number]>();
const counter = useRef(0);
const [hoveredBlocks, setHoveredBlocks] = useState<
Array<[x: number, y: number, key: number]>
>([]);
const staticBlocks = [
[1, 1],
[2, 2],
[4, 3],
[6, 2],
[7, 4],
[5, 5],
];
useEffect(() => {
if (!interactive) {
return;
}
function onMouseMove(event: MouseEvent) {
if (!ref.current) {
return;
}
const rect = ref.current.getBoundingClientRect();
let x = event.clientX - rect.left;
let y = event.clientY - rect.top;
if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
return;
}
x = x - rect.width / 2 - 32;
y = y - yOffset;
x += Math.tan(32 / 160) * y;
x = Math.floor(x / 96);
y = Math.floor(y / 160);
if (currentBlock.current?.[0] === x && currentBlock.current?.[1] === y) {
return;
}
currentBlock.current = [x, y];
setHoveredBlocks((blocks) => {
const key = counter.current++;
const block = [x, y, key] as (typeof hoveredBlocks)[number];
return [...blocks, block].filter(
(block) => !(block[0] === x && block[1] === y && block[2] !== key),
);
});
}
window.addEventListener("mousemove", onMouseMove);
return () => {
window.removeEventListener("mousemove", onMouseMove);
};
}, [yOffset, interactive]);
return (
<motion.svg
ref={ref}
aria-hidden="true"
{...props}
exit={{ opacity: 0 }}
animate={{ opacity: 1 }}
initial={{ opacity: 0 }}
>
<rect width="100%" height="100%" fill={`url(#${id})`} strokeWidth="0" />
<svg x="50%" y={yOffset} strokeWidth="0" className="overflow-visible">
{staticBlocks.map((block) => (
<Block key={`${block}`} x={block[0]} y={block[1]} />
))}
{hoveredBlocks.map((block) => (
<Block
key={block[2]}
x={block[0]}
y={block[1]}
animate={{ opacity: [0, 1, 0] }}
transition={{ duration: 1, times: [0, 0, 1] }}
onAnimationComplete={() => {
setHoveredBlocks((blocks) =>
blocks.filter((b) => b[2] !== block[2]),
);
}}
/>
))}
</svg>
<defs>
<pattern
id={id}
width="96"
height="480"
x="50%"
patternUnits="userSpaceOnUse"
patternTransform={`translate(0 ${yOffset})`}
fill="none"
>
<path d="M128 0 98.572 147.138A16 16 0 0 1 82.883 160H13.117a16 16 0 0 0-15.69 12.862l-26.855 134.276A16 16 0 0 1-45.117 320H-116M64-160 34.572-12.862A16 16 0 0 1 18.883 0h-69.766a16 16 0 0 0-15.69 12.862l-26.855 134.276A16 16 0 0 1-109.117 160H-180M192 160l-29.428 147.138A15.999 15.999 0 0 1 146.883 320H77.117a16 16 0 0 0-15.69 12.862L34.573 467.138A16 16 0 0 1 18.883 480H-52M-136 480h58.883a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1-18.883 320h69.766a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1 109.117 160H192M-72 640h58.883a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1 45.117 480h69.766a15.999 15.999 0 0 0 15.689-12.862l26.856-134.276A15.999 15.999 0 0 1 173.117 320H256M-200 320h58.883a15.999 15.999 0 0 0 15.689-12.862l26.856-134.276A16 16 0 0 1-82.883 160h69.766a16 16 0 0 0 15.69-12.862L29.427 12.862A16 16 0 0 1 45.117 0H128" />
</pattern>
</defs>
</motion.svg>
);
};

View File

@@ -0,0 +1,434 @@
"use client";
import { GridPattern } from "./grid-pattern";
import { Button } from "@/components/ui/button";
import clsx from "clsx";
import { Github } from "lucide-react";
import { useTheme } from "next-themes";
import Link from "next/link";
import { Highlight, themes } from "prism-react-renderer";
import { Fragment, useId, useState } from "react";
import { LayoutGroup, motion } from "framer-motion"
function Glow() {
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>
);
}
const tabs = [
{
name: "server.ts", code: `export const auth = betterAuth({
database: {
provider: "postgresql",
url: process.env.DATABASE_URL,
},
emailAndPassword: {
enabled: true,
},
plugins: [
organization(),
twoFactor(),
]
})` },
{
name: "client.ts", code:
`const client = createAuthClient({
authPlugins: [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>
);
}
export default function Hero() {
const theme = useTheme();
const [activeTab, setActiveTab] = useState("server.ts");
const code = tabs.find((tab) => tab.name === activeTab)?.code ?? "";
return (
<section className="flex mt-1 min-h-screen items-center justify-center gap-20 p-5">
<div className="overflow-hidden bg-transparent dark:-mb-32 dark:mt-[-4.75rem] dark:pb-32 dark:pt-[4.75rem] px-10">
<div className="py-16 sm:px-2 lg:relative lg:px-20 lg:py-20">
<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">
<p className="inline dark:text-white opacity-90 text-5xl tracking-tight relative">
BETTER-AUTH.
</p>
</div>
<p className="mt-3 text-2xl font-mono tracking-tight dark:text-zinc-300 text-zinc-800">
Framework agnostic, comprehensive authentication library and ecosystem for typescript.
</p>
<div className="mt-8 flex gap-4 font-sans md:justify-center lg:justify-start">
<Link href="/docs">
<Button variant="default">Get started</Button>
</Link>
<Button variant="outline" className="flex items-center gap-2">
<Github size={16} />
View on GitHub
</Button>
</div>
</div>
</div>
<div className="relative lg:static xl:pl-10">
<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"
animate={{
y: [10, -10, 0],
transition: {
duration: 2,
stiffness: 10,
type: "just"
},
}}
className="relative rounded-sm bg-gradient-to-tr from-stone-50 to-stone-100 dark:from-stone-950/70 dark:to-stone-900/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-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="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
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>
</div>
</div>
</motion.div>
</LayoutGroup>
</div>
</div>
</div>
</div>
</div>
<GridPattern
className="absolute inset-x-0 -top-14 -z-10 h-full w-full dark:fill-secondary/20 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>
);
}

View File

@@ -0,0 +1,30 @@
const SectionSvg = ({ crossesOffset }: {
crossesOffset: string;
}) => {
return (
<>
<PlusSvg
className={`hidden absolute -top-[0.3125rem] left-[1.5625rem] ${crossesOffset && crossesOffset
} pointer-events-none lg:block xl:left-[3.6825rem]`}
/>
<PlusSvg
className={`hidden absolute -top-[0.3125rem] right-[1.4625rem] ${crossesOffset && crossesOffset
} pointer-events-none lg:block xl:right-[3.25rem]`}
/>
</>
);
};
export default SectionSvg;
export const PlusSvg = ({ className = "" }) => {
return (
<svg className={`${className} || ""`} width="11" height="11" fill="none">
<path
d="M7 1a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V8a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H8a1 1 0 0 1-1-1V1z"
fill="#ada8c4"
/>
</svg>
);
};

View File

@@ -0,0 +1,47 @@
import type React from "react";
import SectionSvg from "./section-svg";
const Section = ({
className,
id,
crosses,
crossesOffset,
customPaddings,
children,
}: {
className: string;
id: string;
crosses?: boolean;
crossesOffset: string;
customPaddings: boolean;
children: React.ReactNode;
}) => {
return (
<div
id={id}
className={`
relative
${customPaddings ||
`py-10 lg:py-16 xl:py-20 ${crosses ? "lg:py-32 xl:py-40" : ""}`
}
${className || ""}`}
>
{children}
<div className="hidden absolute top-0 left-5 w-[0.0625rem] h-[calc(100%_+_30px)] dark:bg-[#26242C] bg-stone-200 pointer-events-none md:block lg:left-7.5 xl:left-16" />
<div className="hidden absolute top-0 right-5 w-[0.0625rem] h-[calc(100%_+_30px)] dark:bg-[#26242C] bg-stone-200 pointer-events-none md:block lg:right-7.5 xl:right-14" />
{crosses && (
<>
<div
className={`hidden absolute top-0 left-7.5 right-7.5 h-0.25 bg-[#26242C] ${crossesOffset && crossesOffset
} pointer-events-none lg:block xl:left-16 right-16`}
/>
<SectionSvg crossesOffset={crossesOffset} />
</>
)}
</div>
);
};
export default Section;

View File

@@ -7,7 +7,7 @@ import {
} from "@/components/ui/accordion";
import { AsideLink } from "@/components/ui/aside-link";
import { FadeIn, FadeInStagger } from "@/components/ui/fade-in";
import { Key, LucideIcon, MailCheck, ScanFace, Search, Users2 } from "lucide-react";
import { Key, LucideAArrowDown, LucideIcon, MailCheck, ScanFace, Search, Users2, UserSquare2 } from "lucide-react";
import { ReactNode, Suspense, SVGProps, useState } from "react";
import {
SearchDialog,
@@ -25,14 +25,13 @@ export default function ArticleLayout() {
}
return (
<aside className="border-r border-lines md:block hidden overflow-y-auto min-w-[--fd-sidebar-width] h-full sticky top-[60px] min-h-[92dvh]">
<div className="relative">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
className="pl-8 bg-muted/50 focus-visible:ring-0 focus-visible:border-0 rounded-none dark:bg-stone-950 border-none border-b border-white"
placeholder="Search documentation..."
type="search"
onClick={() => setOpenSearch(true)}
/>
<div className="flex items-center gap-2 p-2 px-4 border-b bg-gradient-to-br from-stone-900 to-stone-950/80" onClick={() => {
setOpenSearch(true)
}}>
<Search className="h-4 w-4" />
<p className="text-sm bg-gradient-to-tr from-gray-200 to-stone-500 bg-clip-text text-transparent">
Search documentation...
</p>
</div>
<Accordion type="single" collapsible defaultValue={`item-${getDefaultValue()}`}>
{contents.map((item, i) => (
@@ -48,7 +47,7 @@ export default function ArticleLayout() {
<AccordionContent className=" space-y-1 p-0">
<FadeInStagger faster>
{item.list.map((listItem, j) => (
<FadeIn key={listItem.title}>
<FadeIn key={listItem.title} >
<Suspense fallback={<>Loading...</>}>
{
listItem.group ?
@@ -330,64 +329,60 @@ const contents: Content[] = [
],
},
{
title: "Adapters",
title: "Integrations",
Icon: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.4em"
height="1.4em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M20 18c0 2.21-3.582 4-8 4s-8-1.79-8-4v-4.026c.502.617 1.215 1.129 2.008 1.525C7.58 16.285 9.7 16.75 12 16.75s4.42-.465 5.992-1.25c.793-.397 1.506-.91 2.008-1.526z"
/>
<path
fill="currentColor"
d="M12 10.75c2.3 0 4.42-.465 5.992-1.25c.793-.397 1.506-.91 2.008-1.526V12c0 .5-1.786 1.591-2.679 2.158c-1.323.661-3.203 1.092-5.321 1.092s-3.998-.43-5.321-1.092C5.5 13.568 4 12.5 4 12V7.974c.502.617 1.215 1.129 2.008 1.525C7.58 10.285 9.7 10.75 12 10.75"
/>
<path
fill="currentColor"
d="M17.321 8.158C15.998 8.819 14.118 9.25 12 9.25s-3.998-.43-5.321-1.092c-.515-.202-1.673-.843-2.477-1.879a.81.81 0 0 1-.162-.621c.023-.148.055-.301.096-.396C4.828 3.406 8.086 2 12 2c3.914 0 7.172 1.406 7.864 3.262c.041.095.073.248.096.396a.81.81 0 0 1-.162.621c-.804 1.036-1.962 1.677-2.477 1.879"
/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="1.3em" height="1.3em" viewBox="0 0 48 48"><path fill="currentColor" stroke="currentColor" stroke-linejoin="round" stroke-width="4" d="M18 6H8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2Zm0 22H8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V30a2 2 0 0 0-2-2ZM40 6H30a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2Zm0 22H30a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V30a2 2 0 0 0-2-2Z"></path></svg>
),
list: [
{
title: "Drizzle",
href: "/docs/adapters/drizzle",
icon: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M5.353 11.823a1.036 1.036 0 0 0-.395-1.422a1.063 1.063 0 0 0-1.437.399L.138 16.702a1.035 1.035 0 0 0 .395 1.422a1.063 1.063 0 0 0 1.437-.398zm11.216 0a1.036 1.036 0 0 0-.394-1.422a1.064 1.064 0 0 0-1.438.399l-3.382 5.902a1.036 1.036 0 0 0 .394 1.422c.506.283 1.15.104 1.438-.398zm7.293-4.525a1.036 1.036 0 0 0-.395-1.422a1.06 1.06 0 0 0-1.437.399l-3.383 5.902a1.036 1.036 0 0 0 .395 1.422a1.063 1.063 0 0 0 1.437-.399zm-11.219 0a1.035 1.035 0 0 0-.394-1.422a1.064 1.064 0 0 0-1.438.398l-3.382 5.903a1.036 1.036 0 0 0 .394 1.422c.506.282 1.15.104 1.438-.399z"
/>
</svg>
),
group: true,
title: "Frameworks",
href: "/docs/integrations",
icon: LucideAArrowDown
},
{
title: "Prisma",
href: "/docs/adapters/prisma",
title: "Hono",
icon: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M21.807 18.285L13.553.757a1.32 1.32 0 0 0-1.129-.755a1.31 1.31 0 0 0-1.206.626l-8.952 14.5a1.36 1.36 0 0 0 .016 1.455l4.376 6.778a1.41 1.41 0 0 0 1.58.581l12.703-3.757c.389-.115.707-.39.873-.755s.164-.783-.007-1.145m-1.848.752L9.18 22.224a.452.452 0 0 1-.575-.52l3.85-18.438c.072-.345.549-.4.699-.08l7.129 15.138a.515.515 0 0 1-.325.713"
/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 256 330"><path fill="#9E9494" d="M134.129.029q1.315-.17 2.319.662a1256 1256 0 0 1 69.573 93.427q24.141 36.346 41.082 76.862q27.055 72.162-28.16 125.564q-48.313 40.83-111.318 31.805q-75.312-15.355-102.373-87.133Q-1.796 217.85.614 193.51q4.014-41.896 19.878-80.838q6.61-15.888 17.228-29.154a382 382 0 0 1 16.565 21.203q3.66 3.825 7.62 7.289Q92.138 52.013 134.13.029" opacity=".993"></path><path fill="#3C2020" d="M129.49 53.7q36.47 42.3 65.93 90.114a187.3 187.3 0 0 1 15.24 33.13q12.507 49.206-26.836 81.169q-38.05 26.774-83.488 15.902q-48.999-15.205-56.653-65.929q-1.857-15.993 3.314-31.142a225.4 225.4 0 0 1 17.89-35.78l19.878-29.155a5510 5510 0 0 0 44.726-58.31"></path></svg>
),
href: "/docs/integrations/hono",
},
],
{
title: "Next",
icon: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 24 24"><path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m4-14h-1.35v4H16zM9.346 9.71l6.059 7.828l1.054-.809L9.683 8H8v7.997h1.346z"></path></svg>
),
href: "/docs/integrations/next",
},
{
title: "Nuxt",
icon: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 512 512"><path fill="currentColor" d="M200.662 81.35L0 430.65h130.774l139.945-239.803zm134.256 40.313l-39.023 69.167l138.703 239.82H512zm-51.596 91.052L155.924 430.651h253.485z"></path></svg>
),
href: "/docs/integrations/nuxt",
},
{
title: "Svelte Kit",
icon: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 426 512"><path fill="currentColor" d="M403.508 229.23C491.235 87.7 315.378-58.105 190.392 23.555L71.528 99.337c-57.559 37.487-82.55 109.513-47.45 183.53c-87.761 133.132 83.005 289.03 213.116 205.762l118.864-75.782c64.673-42.583 79.512-116.018 47.45-183.616m-297.592-80.886l118.69-75.739c77.973-46.679 167.756 34.942 135.388 110.992c-19.225-15.274-40.65-24.665-56.923-28.894c6.186-24.57-22.335-42.796-42.174-30.106l-118.95 75.48c-29.411 20.328 1.946 62.138 31.014 44.596l45.33-28.895c101.725-57.403 198 80.425 103.38 147.975l-118.692 75.739C131.455 485.225 34.11 411.96 67.592 328.5c17.786 13.463 36.677 23.363 56.923 28.894c-4.47 28.222 24.006 41.943 42.476 30.365L285.64 312.02c29.28-21.955-2.149-61.692-30.97-44.595l-45.504 28.894c-100.56 58.77-199.076-80.42-103.25-147.975"></path></svg>
),
href: "/docs/integrations/svelte-kit",
},
{
title: "Solid Start",
icon: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 128 128"><path fill="currentColor" d="M61.832 4.744c-3.205.058-6.37.395-9.45 1.07l-2.402.803c-4.806 1.603-8.813 4.005-11.216 7.21l-1.602 2.404l-12.017 20.828l.166.031c-4.785 5.823-5.007 14.07-.166 21.6c1.804 2.345 4.073 4.431 6.634 6.234l-15.445 4.982L.311 97.946s42.46 32.044 75.306 24.033l2.403-.801c5.322-1.565 9.292-4.48 11.683-8.068l.334.056l16.022-28.84c3.204-5.608 2.404-12.016-1.602-18.425a36 36 0 0 0-7.059-6.643l15.872-5.375l14.42-24.033S92.817 4.19 61.831 4.744z"></path></svg>
),
href: "/docs/integrations/solid-start",
},
{
title: "React",
icon: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 15 15"><path fill="currentColor" fillRule="evenodd" d="M5.315 1.837c-.4-.116-.695-.085-.91.032c-.216.116-.404.347-.526.745c-.122.401-.163.936-.104 1.582q.015.157.037.321a14 14 0 0 1 1.676-.311a13 13 0 0 1 1.275-1.54l-.066-.053c-.508-.402-.98-.66-1.382-.776m2.185.14q-.09-.076-.182-.148C6.746 1.377 6.16 1.04 5.594.876C5.024.711 4.441.711 3.928.99s-.833.767-1.005 1.334c-.172.564-.21 1.238-.144 1.965q.023.255.065.523q-.256.09-.49.192c-.671.287-1.246.642-1.66 1.062C.278 6.487 0 7 0 7.584S.278 8.68.694 9.103c.414.42.989.774 1.66 1.062q.235.1.49.192a9 9 0 0 0-.065.523c-.066.726-.028 1.4.144 1.965c.172.567.492 1.056 1.005 1.333c.513.278 1.097.279 1.666.114c.566-.165 1.152-.5 1.724-.953l.182-.149q.09.076.182.149c.572.452 1.158.788 1.724.953c.569.165 1.153.164 1.666-.114c.513-.277.833-.766 1.005-1.333c.172-.564.21-1.239.144-1.965a9 9 0 0 0-.065-.523q.255-.09.49-.192c.671-.288 1.246-.643 1.66-1.062c.416-.422.694-.936.694-1.52c0-.582-.278-1.096-.694-1.518c-.414-.42-.989-.775-1.66-1.062a9 9 0 0 0-.49-.192q.04-.268.065-.523c.066-.727.028-1.4-.144-1.965c-.172-.567-.492-1.056-1.005-1.334S9.975.711 9.406.876c-.566.164-1.152.5-1.724.953zm0 1.365q-.338.346-.672.755a17 17 0 0 1 1.344 0a11 11 0 0 0-.672-.755m2.012.864c-.41-.574-.84-1.092-1.275-1.54l.065-.053c.51-.402.98-.66 1.383-.776c.399-.116.695-.085.91.032c.216.116.404.347.525.745c.122.401.164.936.105 1.582q-.015.158-.037.32a14 14 0 0 0-1.676-.31m-.563.944a15.6 15.6 0 0 0-2.898 0A15.6 15.6 0 0 0 4.72 7.584a15.7 15.7 0 0 0 1.33 2.433a15.6 15.6 0 0 0 2.9 0a15.6 15.6 0 0 0 1.33-2.433A15.7 15.7 0 0 0 8.95 5.15m1.824 1.138a17 17 0 0 0-.527-.956q.39.075.752.168q-.094.385-.225.788m0 2.591a17 17 0 0 1-.527.957q.39-.075.752-.169a12 12 0 0 0-.225-.788m1.18.487a14 14 0 0 0-.588-1.782c.246-.61.443-1.209.588-1.782q.154.058.3.12c.596.256 1.047.547 1.341.845c.292.296.406.572.406.817s-.114.52-.406.816c-.294.299-.745.59-1.341.846a8 8 0 0 1-.3.12m-.765 1.285a14 14 0 0 1-1.676.311c-.41.574-.84 1.091-1.275 1.54l.066.052c.508.403.98.66 1.382.777c.399.116.695.085.91-.032s.404-.348.525-.746c.123-.4.164-.936.105-1.582a7 7 0 0 0-.037-.32M7.5 11.826q.338-.346.672-.755a17 17 0 0 1-1.344 0q.334.408.672.755m-2.746-1.99a17 17 0 0 1-.527-.957q-.13.404-.225.788q.361.094.752.169m-.942.815a14 14 0 0 0 1.676.311c.41.574.839 1.091 1.275 1.54l-.066.052c-.508.403-.98.66-1.382.777c-.4.116-.695.085-.911-.032s-.403-.348-.525-.746c-.122-.4-.163-.936-.104-1.582a8 8 0 0 1 .037-.32m-.765-1.285c.145-.574.341-1.172.588-1.782a14 14 0 0 1-.588-1.782q-.155.058-.3.12c-.596.256-1.047.547-1.341.845c-.292.296-.406.572-.406.817s.114.52.406.816c.294.299.745.59 1.341.846q.146.061.3.12m.955-3.865q.094.384.225.787a17 17 0 0 1 .527-.956q-.39.075-.752.169M6 7.584a1.5 1.5 0 1 1 3 0a1.5 1.5 0 0 1-3 0m1.5-.5a.5.5 0 1 0 0 1a.5.5 0 0 0 0-1" clipRule="evenodd"></path></svg>
),
href: "/docs/integrations/react",
},
]
},
{
title: "Plugins",
@@ -409,26 +404,124 @@ const contents: Content[] = [
),
list: [
{
title: "2 Factor",
title: "Introduction",
icon: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="1.1em" height="1.1em" viewBox="0 0 14 14"><path fill="currentColor" fillRule="evenodd" d="M12.402 8.976H7.259a2.278 2.278 0 0 0-.193-4.547h-1.68A3.095 3.095 0 0 0 4.609 0h7.793a1.35 1.35 0 0 1 1.348 1.35v6.279c0 .744-.604 1.348-1.348 1.348ZM2.898 4.431a1.848 1.848 0 1 0 0-3.695a1.848 1.848 0 0 0 0 3.695m5.195 2.276c0-.568-.46-1.028-1.027-1.028H2.899a2.65 2.65 0 0 0-2.65 2.65v1.205c0 .532.432.963.964.963h.172l.282 2.61A1 1 0 0 0 2.66 14h.502a1 1 0 0 0 .99-.862l.753-5.404h2.16c.567 0 1.027-.46 1.027-1.027Z" clipRule="evenodd"></path></svg>
),
href: "/docs/plugins/introduction",
},
{
title: "Authentication",
group: true,
href: "/docs/plugins/1st-party-plugins",
icon: LucideAArrowDown
},
{
title: "Passkey",
href: "/docs/plugins/passkey",
icon: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 24 24"><path fill="currentColor" d="M18 16.663a3.5 3.5 0 0 1-2-3.163a3.5 3.5 0 1 1 4.5 3.355V17l1.146 1.146a.5.5 0 0 1 0 .708L20.5 20l1.161 1.161a.5.5 0 0 1 .015.692l-1.823 1.984a.5.5 0 0 1-.722.015l-.985-.984a.5.5 0 0 1-.146-.354zM20.5 13a1 1 0 1 0-2 0a1 1 0 0 0 2 0M17 17.242v3.69c-1.36.714-3.031 1.07-5 1.07c-3.42 0-5.944-1.073-7.486-3.237a2.75 2.75 0 0 1-.51-1.596v-.92a2.25 2.25 0 0 1 2.249-2.25h8.775A4.5 4.5 0 0 0 17 17.243M12 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10"></path></svg>
),
},
{
title: "Two Factor",
icon: ScanFace,
href: "/docs/plugins/2fa",
},
{
title: "Username",
icon: UserSquare2,
href: "/docs/plugins/username",
},
{
title: "Authorization",
group: true,
href: "/docs/plugins/1st-party-plugins",
icon: LucideAArrowDown
},
{
title: "Organization",
icon: Users2,
href: "/docs/plugins/organization",
},
{
title: "Utility",
group: true,
href: "/docs/plugins/1st-party-plugins",
icon: LucideAArrowDown
},
{
title: "Bearer",
icon: Key,
href: "/docs/plugins/email-verifier",
},
{
title: "Email Verifier",
title: "Email Checker",
icon: MailCheck,
href: "/docs/plugins/email-verifier",
},
{
title: "Organization",
icon: Users2,
href: "/docs/plugins/organization",
},
],
},
// {
// title: "Database",
// Icon: () => (
// <svg
// xmlns="http://www.w3.org/2000/svg"
// width="1.4em"
// height="1.4em"
// viewBox="0 0 24 24"
// >
// <path
// fill="currentColor"
// d="M20 18c0 2.21-3.582 4-8 4s-8-1.79-8-4v-4.026c.502.617 1.215 1.129 2.008 1.525C7.58 16.285 9.7 16.75 12 16.75s4.42-.465 5.992-1.25c.793-.397 1.506-.91 2.008-1.526z"
// />
// <path
// fill="currentColor"
// d="M12 10.75c2.3 0 4.42-.465 5.992-1.25c.793-.397 1.506-.91 2.008-1.526V12c0 .5-1.786 1.591-2.679 2.158c-1.323.661-3.203 1.092-5.321 1.092s-3.998-.43-5.321-1.092C5.5 13.568 4 12.5 4 12V7.974c.502.617 1.215 1.129 2.008 1.525C7.58 10.285 9.7 10.75 12 10.75"
// />
// <path
// fill="currentColor"
// d="M17.321 8.158C15.998 8.819 14.118 9.25 12 9.25s-3.998-.43-5.321-1.092c-.515-.202-1.673-.843-2.477-1.879a.81.81 0 0 1-.162-.621c.023-.148.055-.301.096-.396C4.828 3.406 8.086 2 12 2c3.914 0 7.172 1.406 7.864 3.262c.041.095.073.248.096.396a.81.81 0 0 1-.162.621c-.804 1.036-1.962 1.677-2.477 1.879"
// />
// </svg>
// ),
// list: [
// {
// title: "Drizzle",
// href: "/docs/adapters/drizzle",
// icon: () => (
// <svg
// xmlns="http://www.w3.org/2000/svg"
// width="1.2em"
// height="1.2em"
// viewBox="0 0 24 24"
// >
// <path
// fill="currentColor"
// d="M5.353 11.823a1.036 1.036 0 0 0-.395-1.422a1.063 1.063 0 0 0-1.437.399L.138 16.702a1.035 1.035 0 0 0 .395 1.422a1.063 1.063 0 0 0 1.437-.398zm11.216 0a1.036 1.036 0 0 0-.394-1.422a1.064 1.064 0 0 0-1.438.399l-3.382 5.902a1.036 1.036 0 0 0 .394 1.422c.506.283 1.15.104 1.438-.398zm7.293-4.525a1.036 1.036 0 0 0-.395-1.422a1.06 1.06 0 0 0-1.437.399l-3.383 5.902a1.036 1.036 0 0 0 .395 1.422a1.063 1.063 0 0 0 1.437-.399zm-11.219 0a1.035 1.035 0 0 0-.394-1.422a1.064 1.064 0 0 0-1.438.398l-3.382 5.903a1.036 1.036 0 0 0 .394 1.422c.506.282 1.15.104 1.438-.399z"
// />
// </svg>
// ),
// },
// {
// title: "Prisma",
// href: "/docs/adapters/prisma",
// icon: () => (
// <svg
// xmlns="http://www.w3.org/2000/svg"
// width="1.2em"
// height="1.2em"
// viewBox="0 0 24 24"
// >
// <path
// fill="currentColor"
// d="M21.807 18.285L13.553.757a1.32 1.32 0 0 0-1.129-.755a1.31 1.31 0 0 0-1.206.626l-8.952 14.5a1.36 1.36 0 0 0 .016 1.455l4.376 6.778a1.41 1.41 0 0 0 1.58.581l12.703-3.757c.389-.115.707-.39.873-.755s.164-.783-.007-1.145m-1.848.752L9.18 22.224a.452.452 0 0 1-.575-.52l3.85-18.438c.072-.345.549-.4.699-.08l7.129 15.138a.515.515 0 0 1-.325.713"
// />
// </svg>
// ),
// },
// ],
// },
];

View File

@@ -24,11 +24,11 @@ export const FadeIn = (
variants={{
hidden: {
opacity: 0,
y: shouldReduceMotion ? 0 : props.fromTopToBottom ? -24 : 24,
y: shouldReduceMotion ? 0 : props.fromTopToBottom ? -24 : 2,
},
visible: { opacity: 1, y: 0 },
}}
transition={{ duration: 0.5 }}
transition={{ duration: 0.3 }}
{...(isInStaggerGroup
? {}
: {
@@ -53,7 +53,7 @@ export const FadeInStagger = ({
initial="hidden"
whileInView="visible"
viewport={viewport}
transition={{ staggerChildren: faster ? 0.12 : 0.2 }}
transition={{ staggerChildren: faster ? 0.08 : 0.2 }}
{...props}
/>
</FadeInStaggerContext.Provider>

View File

@@ -42,7 +42,7 @@ const signup = async () => {
password: "password1234",
name: "test",
image: "https://example.com/image.png",
callbackUrl: "/"
callbackURL: "/"
})
}

View File

@@ -68,7 +68,9 @@ description: Installation
```
</Step>
<Step>
### Migrate Database
### Database
**Using Database Migrations**
better auth comes with a cli tool to migrate your database. You can run the following command to migrate your database.
```bash
npx better-auth migrate

View File

@@ -0,0 +1,367 @@
---
title: Two Factor
description: Two Factor
---
Two factor authentication is a security feature that adds an extra layer of security to your account. It requires users to provide a second form of verification, in addition to their password, before they can log in to your account.
The two facor plugin providers `OTP` and `TOTP` authentication methods. The `OTP` method is a one-time password that is sent to the user's email or phone number, while the `TOTP` method is a time-based one-time password that is generated by the user's device.
## Quick setup
<Steps>
<Step>
### Add the plugin to your auth config
To add the two factor plugin to your auth config, you need to import the plugin and pass it to the `plugins` option of the auth instance.
```ts title="auth.ts" twoslash
import { betterAuth } from "better-auth"
import { twoFactor } from "better-auth/plugins"
export const { api, handler } = await betterAuth({
database: {
provider: "sqlite",
url: "./db.sqlite",
},
plugins: [ // [!code highlight]
twoFactor({ // [!code highlight]
issuer: "my-app" // [!code highlight]
}) // [!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
To use the two factor plugin in the client, you need to add it on your authPlugins list.
```ts title="client.ts" twoslash
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
const client = createAuthClient({
authPlugins: [ // [!code highlight]
twoFactorClient({ // [!code highlight]
twoFactorPage: "/two-factor" // [!code highlight]
}) // [!code highlight]
] // [!code highlight]
})
```
</Step>
<Step>
### Enable two factor authentication for a user
To enable two factor authentication for a user, you can use the `twoFactor.enable` function provided by the client. Make sure the user is signed in before calling this function.
```ts title="client.ts" twoslash
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
const client = createAuthClient({
authPlugins: [ // [!code highlight]
twoFactorClient({ // [!code highlight]
twoFactorPage: "/two-factor" // [!code highlight]
}) // [!code highlight]
]
})
// ---cut---
const enableTwoFactor = async() => {
const data = await client.twoFactor.enable() // this will enable two factor authentication for the signed in user
}
```
<Callout type="info">
You can also disable two factor authentication for a user by calling the `twoFactor.disable` function.
</Callout>
</Step>
<Step>
### Get the TOTP URI
once you've enabled two factor authentication, you can get the TOTP URI by calling the `twoFactor.getTotpUri` function. This function returns an object with the `totpURI` property, which is the URI that the user needs to scan with their TOTP app.
```ts title="client.ts" twoslash
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
const client = createAuthClient({
authPlugins: [ // [!code highlight]
twoFactorClient({ // [!code highlight]
twoFactorPage: "/two-factor" // [!code highlight]
}) // [!code highlight]
]
})
// ---cut---
const { data, error } = await client.twoFactor.getTotpUri()
```
<Callout type="info">
You can either generate a QR code with this URI for the user to scan or simply display the URI on the screen.
</Callout>
</Step>
<Step>
### Generate backup codes
To generate backup codes, you can use the `generateBackupCodes` function provided by the client.
```ts title="client.ts" twoslash
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
const client = createAuthClient({
authPlugins: [ // [!code highlight]
twoFactorClient({ // [!code highlight]
twoFactorPage: "/two-factor" // [!code highlight]
}) // [!code highlight]
]
})
// ---cut---
const { data, error } = await client.twoFactor.generateBackupCodes()
```
<Callout type="info">
Make sure to show the backup codes to the user. So they can store them in a safe place.
</Callout>
</Step>
<Step>
### Sign In with 2-Factor
Once the user has enabled 2-Factor, when they try to sign in, a session won't be created before they passed the 2-Factor verification. If you didn't disable redirects, the user will be redirected to the two factor page you porvided on the client plugin. If you disbaled redirects, you need to handler callbacks.
**Callbacks**
Since better auth client wraps over [bette-fetch](https://better-fetch.vercel.app), you can use the `onSuccess` callback to handle the callback from the provider.
```ts title="client.ts" twoslash
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
const client = createAuthClient({
authPlugins: [ // [!code highlight]
twoFactorClient({ // [!code highlight]
twoFactorPage: "/two-factor" // [!code highlight]
}) // [!code highlight]
] // [!code highlight]
})
// ---cut---
const signin = async () => {
const data = await client.signIn.email({
email: "test@example.com",
password: "password1234",
options: {
async onSuccess(context){
if(context.data.twoFactorRedirect){
// handler the callback
}
}
}
})
}
```
**Send 2-Factor OTP**
If you want to use OTP over SMS or Email, you can use the `twoFactor.sendOtp` function provided by the client.
```ts title="client.ts" twoslash
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
const client = createAuthClient({
authPlugins: [ // [!code highlight]
twoFactorClient({ // [!code highlight]
twoFactorPage: "/two-factor" // [!code highlight]
}) // [!code highlight]
] // [!code highlight]
})
// ---cut---
/**
* This will call the sendOTP function you added
* on the server.
*/
const data = await client.twoFactor.sendOtp()
```
**Verify 2-Factor**
once you presented the user with an input field to enter the OTP, you can use the `twoFactor.verifyOtp` or `twoFactor.verifyTOTP` function provided by the client.
```ts title="client.ts" twoslash
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
const client = createAuthClient({
authPlugins: [ // [!code highlight]
twoFactorClient({ // [!code highlight]
twoFactorPage: "/two-factor" // [!code highlight]
}) // [!code highlight]
] // [!code highlight]
})
// ---cut---
await client.twoFactor.verifyOtp({
code: "123456",
})
//or
await client.twoFactor.verifyTotp({
code: "123456",
callbackURL: "/"
})
```
**Using Backup Code to verify 2-Factor**
If you want to use backup codes to verify 2-Factor, you can use the `twoFactor.verifyBackupCode` function provided by the client.
```ts title="client.ts" twoslash
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
const client = createAuthClient({
authPlugins: [ // [!code highlight]
twoFactorClient({ // [!code highlight]
twoFactorPage: "/two-factor" // [!code highlight]
}) // [!code highlight]
] // [!code hi ghlight]
})
// ---cut---
await client.twoFactor.verifyBackupCode({
code: "123456",
})
```
</Step>
<Step>
🎉 the user has successfully signed in with 2-Factor!
</Step>
</Steps>
## 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 { api, handler } = 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 authPlugins list.
```ts title="client.ts" twoslash
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
const client = createAuthClient({
authPlugins: [
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,
},
},
},
}
```

View File

@@ -0,0 +1,42 @@
---
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 { api, handler } = await betterAuth({
database: {
provider: "sqlite",
url: "./db.sqlite",
},
plugins: [ // [!code highlight]
organization() // [!code highlight]
] // [!code highlight]
})
```
### 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({
authPlugins: [ // [!code highlight]
organizationClient() // [!code highlight]
] // [!code highlight]
})
```
</Step>
</Steps>

View File

@@ -0,0 +1,158 @@
---
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 users 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 { api, handler } = 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({
authPlugins: [ // [!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({
authPlugins: [ // [!code highlight]
passkeyClient // [!code highlight]
] // [!code highlight]
})
// ---cut---
const data = await client.registerPasskey()
```
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({
authPlugins: [ // [!code highlight]
passkeyClient // [!code highlight]
] // [!code highlight]
})
// ---cut---
const data = await client.signInPasskey()
```
</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,
},
},
},
}
```

View File

@@ -0,0 +1,119 @@
---
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({
authPlugins: [ // [!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({
authPlugins: [ // [!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({
authPlugins: [ // [!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
}
}
}
```

15
docs/lib/auth.ts Normal file
View File

@@ -0,0 +1,15 @@
import { betterAuth } from "better-auth";
import { organization, twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
baseURL: "http://localhost:3000",
database: {
provider: "sqlite",
url: "./db.sqlite",
},
plugins: [
twoFactor({
issuer: "My App",
}),
organization(),
],
});

View File

@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

View File

@@ -2,8 +2,7 @@ import createMDX from 'fumadocs-mdx/config';
import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins";
import { remarkInstall } from "fumadocs-docgen";
import { transformerTwoslash } from "fumadocs-twoslash"
import { getParsedCommandLineOfConfigFile, ModuleResolutionKind } from 'typescript';
import { ModuleResolutionKind } from 'typescript';
const withMDX = createMDX({
mdxOptions: {
rehypeCodeOptions: {

View File

@@ -43,21 +43,25 @@
"cmdk": "1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.2.0",
"framer-motion": "^11.3.30",
"fumadocs-core": "13.4.1",
"fumadocs-docgen": "^1.1.0",
"fumadocs-mdx": "9.0.4",
"fumadocs-twoslash": "^1.1.2",
"fumadocs-ui": "13.4.1",
"geist": "^1.3.1",
"input-otp": "^1.2.4",
"lucide-react": "^0.435.0",
"next": "^14.2.5",
"next-themes": "^0.3.0",
"prism-react-renderer": "^2.4.0",
"react": "^18.3.1",
"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",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.0",
"tailwindcss-animate": "^1.0.7",

View File

@@ -1,4 +1,5 @@
import { createPreset } from 'fumadocs-ui/tailwind-plugin';
import defaultTheme from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
@@ -14,6 +15,11 @@ export default {
plugins: [require("tailwindcss-animate")],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-geist-sans)"],
mono: ["var(--font-geist-mono)"],
display: [...defaultTheme.fontFamily.sans],
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',

View File

@@ -1,6 +1,15 @@
import type { FieldAttribute } from "../db";
import type { BetterAuthOptions } from "../types";
export type BetterAuthDbSchema = Record<
string,
{
tableName: string;
fields: Record<string, FieldAttribute>;
disableMigrations?: boolean;
}
>;
export const getAuthTables = (options: BetterAuthOptions) => {
const pluginSchema = options.plugins?.reduce((acc, plugin) => {
const schema = plugin.schema;
@@ -113,12 +122,5 @@ export const getAuthTables = (options: BetterAuthOptions) => {
},
},
},
} satisfies Record<
string,
{
tableName: string;
fields: Record<string, FieldAttribute>;
disableMigrations?: boolean;
}
>;
} satisfies BetterAuthDbSchema;
};

View File

@@ -5,8 +5,6 @@ import {
createMiddleware,
createMiddlewareCreator,
} from "better-call";
import { migrateAll } from "../db/migrations";
import { getMigrations } from "../db/migrations/get-migrations";
import type { AuthContext } from "../init";
import type { BetterAuthOptions } from "../types/options";
@@ -23,26 +21,8 @@ export const createAuthMiddleware = createMiddlewareCreator({
use: [optionsMiddleware],
});
export const autoMigrateMiddleware = createAuthMiddleware(async (ctx) => {
if (!ctx.context?.options?.database) {
return;
}
if (
"autoMigrate" in ctx.context?.options?.database &&
ctx.context.options.database.autoMigrate
) {
const { noMigration } = await getMigrations(ctx.context.options, false);
if (noMigration) {
return;
}
await migrateAll(ctx.context.options, {
cli: false,
});
}
});
export const createAuthEndpoint = createEndpointCreator({
use: [optionsMiddleware, autoMigrateMiddleware],
use: [optionsMiddleware],
});
export type AuthEndpoint = Endpoint<

View File

@@ -1,14 +1,16 @@
import { existsSync } from "node:fs";
import * as path from "node:path";
import { Command } from "commander";
import { getConfig } from "../get-config";
import { z } from "zod";
import { migrateAll } from "../../db/migrations";
import { existsSync } from "fs";
import path from "path";
import { logger } from "../../utils/logger";
import { getConfig, possiblePaths } from "../get-config";
import { createKyselyAdapter } from "../../adapters/kysely";
import ora from "ora";
import chalk from "chalk";
import prompts from "prompts";
import { getMigrations } from "../utils/get-migration";
export const migrate = new Command()
.name("migrate")
.description("Migrate the database")
export const migrate = new Command("migrate")
.option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
@@ -25,34 +27,60 @@ export const migrate = new Command()
config: z.string().optional(),
})
.parse(opts);
try {
const cwd = path.resolve(options.cwd);
if (!existsSync(cwd)) {
logger.error(`The directory "${cwd}" does not exist.`);
process.exit(1);
}
const config = await getConfig({ cwd, configPath: options.config });
if (config) {
await migrateAll(config, {
cli: true,
});
} else {
logger.error("No configuration file found.");
logger.info(
"Better Auth will look for a configuration file in the following directories:",
);
for (const possiblePath of possiblePaths) {
logger.log(`📁 ${possiblePath}`);
}
logger.log(
"if you want to use a different configuration file, you can use the --config flag.",
);
}
} catch (e) {
logger.error(e);
throw e;
const cwd = path.resolve(options.cwd);
if (!existsSync(cwd)) {
logger.error(`The directory "${cwd}" does not exist.`);
process.exit(1);
}
const config = await getConfig({
cwd,
configPath: options.config,
});
if (!config) {
logger.error("No configuration file found.");
return;
}
const db = createKyselyAdapter(config);
if (!db) {
logger.error("Invalid database configuration.");
process.exit(1);
}
const spinner = ora("preparing migration...").start();
const { toBeAdded, toBeCreated, runMigrations } =
await getMigrations(config);
if (!toBeAdded.length && !toBeCreated.length) {
spinner.stop();
logger.success("🚀 No migrations needed.");
process.exit(0);
}
spinner.stop();
logger.info(`🔑 The migration will affect the following:`);
for (const table of [...toBeAdded, ...toBeCreated]) {
logger.info(
"->",
chalk.magenta(Object.keys(table.fields).join(", ")),
chalk.white("fields on"),
chalk.yellow(`${table.table}`),
chalk.white("table."),
);
}
const { migrate } = await prompts({
type: "confirm",
name: "migrate",
message: "Are you sure you want to run these migrations?",
initial: false,
});
if (!migrate) {
logger.info("Migration cancelled.");
process.exit(0);
}
spinner?.start("migrating...");
await runMigrations();
spinner.stop();
logger.success("🚀 migration was completed successfully!");
process.exit(0);
});

View File

@@ -1,7 +1,6 @@
import { Command } from "commander";
import "dotenv/config";
import { migrate } from "./commands/migrate";
async function main() {
const program = new Command().name("better-auth");
program.addCommand(migrate);

View File

@@ -0,0 +1,177 @@
import type {
AlterTableColumnAlteringBuilder,
CreateTableBuilder,
} from "kysely";
import type { FieldAttribute, FieldType } from "../../db";
import { logger } from "../../utils/logger";
import type { BetterAuthOptions } from "../../types";
import { getSchema } from "./get-schema";
import { createKyselyAdapter, getDatabaseType } from "../../adapters/kysely";
const postgresMap = {
string: ["character varying", "text"],
number: [
"integer",
"bigint",
"smallint",
"numeric",
"real",
"double precision",
],
boolean: ["boolean"],
date: ["timestamp", "date"],
};
const mysqlMap = {
string: ["varchar", "text"],
number: [
"integer",
"int",
"bigint",
"smallint",
"decimal",
"float",
"double",
],
boolean: ["boolean"],
date: ["date", "datetime"],
};
const sqliteMap = {
string: ["TEXT"],
number: ["INTEGER", "REAL"],
boolean: ["INTEGER", "BOOLEAN"], // 0 or 1
date: ["DATE", "INTEGER"],
};
const map = {
postgres: postgresMap,
mysql: mysqlMap,
sqlite: sqliteMap,
};
export function matchType(
columnDataType: string,
fieldType: FieldType,
dbType: "postgres" | "sqlite" | "mysql",
) {
const types = map[dbType];
const type = types[fieldType].map((t) => t.toLowerCase());
const matches = type.includes(columnDataType.toLowerCase());
return matches;
}
export async function getMigrations(config: BetterAuthOptions) {
const betterAuthSchema = getSchema(config);
const dbType = getDatabaseType(config);
const db = createKyselyAdapter(config);
if (!db) {
logger.error("Invalid database configuration.");
process.exit(1);
}
const tableMetadata = await db.introspection.getTables();
const toBeCreated: {
table: string;
fields: Record<string, FieldAttribute>;
}[] = [];
const toBeAdded: {
table: string;
fields: Record<string, FieldAttribute>;
}[] = [];
for (const [key, value] of Object.entries(betterAuthSchema)) {
const table = tableMetadata.find((t) => t.name === key);
if (!table) {
const tIndex = toBeCreated.findIndex((t) => t.table === key);
if (tIndex === -1) {
toBeCreated.push({
table: key,
fields: value.fields,
});
} else {
toBeCreated[tIndex].fields = {
...toBeCreated[tIndex].fields,
...value.fields,
};
}
continue;
}
let toBeAddedFields: Record<string, FieldAttribute> = {};
for (const [fieldName, field] of Object.entries(value.fields)) {
const column = table.columns.find((c) => c.name === fieldName);
if (!column) {
toBeAddedFields[fieldName] = field;
continue;
}
if (matchType(column.dataType, field.type, dbType)) {
continue;
} else {
logger.warn(
`Field ${fieldName} in table ${key} has a different type in the database. Expected ${field.type} but got ${column.dataType}.`,
);
}
}
if (Object.keys(toBeAddedFields).length > 0) {
toBeAdded.push({
table: key,
fields: toBeAddedFields,
});
}
}
const typeMap = {
string: "text",
boolean: "boolean",
number: "integer",
date: "date",
} as const;
const migrations: (
| AlterTableColumnAlteringBuilder
| CreateTableBuilder<string, string>
)[] = [];
if (toBeAdded.length) {
for (const table of toBeAdded) {
logger.info(`Adding fields to table ${table.table}`);
for (const [fieldName, field] of Object.entries(table.fields)) {
logger.info(`Adding field ${fieldName} with type ${field.type}`);
const type = typeMap[field.type];
const exec = db.schema
.alterTable(table.table)
.addColumn(fieldName, type, (col) => {
col = field.required !== false ? col.notNull() : col;
if (field.references) {
col = col.references(
`${field.references.model}.${field.references.field}`,
);
}
return col;
});
migrations.push(exec);
}
}
}
if (toBeCreated.length) {
for (const table of toBeCreated) {
let dbT = db.schema.createTable(table.table);
for (const [fieldName, field] of Object.entries(table.fields)) {
const type = typeMap[field.type];
dbT = dbT.addColumn(fieldName, type, (col) => {
col = field.required !== false ? col.notNull() : col;
if (field.references) {
col = col.references(
`${field.references.model}.${field.references.field}`,
);
}
return col;
});
}
migrations.push(dbT);
}
}
async function runMigrations() {
return await Promise.all(migrations.map((m) => m.execute()));
}
return { toBeCreated, toBeAdded, runMigrations };
}

View File

@@ -0,0 +1,46 @@
import {
getAuthTables,
type BetterAuthDbSchema,
} from "../../adapters/get-tables";
import type { FieldAttribute } from "../../db";
import type { BetterAuthOptions } from "../../types";
export function getPluginTable(config: BetterAuthOptions) {
const pluginsMigrations =
config.plugins?.flatMap((plugin) =>
Object.keys(plugin.schema || {})
.map((key) => {
const schema = plugin.schema || {};
const table = schema[key]!;
if (table?.disableMigration) {
return;
}
return {
tableName: key,
fields: table?.fields as Record<string, FieldAttribute>,
};
})
.filter((value) => value !== undefined),
) || [];
return pluginsMigrations;
}
export function getSchema(config: BetterAuthOptions) {
const baseSchema = getAuthTables(config);
const pluginSchema = getPluginTable(config);
const schema = [
...pluginSchema,
baseSchema.user,
baseSchema.session,
baseSchema.account,
].reduce((acc, curr) => {
//@ts-expect-error
acc[curr.tableName] = {
fields: {
...acc[curr.tableName]?.fields,
...curr.fields,
},
};
return acc;
}, {} as BetterAuthDbSchema);
return schema;
}

View File

@@ -1 +0,0 @@
export const MIGRATION_TABLE_NAME = "better_auth_migrations";

View File

@@ -1,416 +0,0 @@
import {
type AlterTableBuilder,
type AlterTableColumnAlteringBuilder,
type CreateTableBuilder,
type Kysely,
type Migration,
sql,
} from "kysely";
import { migrationTableName } from ".";
import { getAuthTables } from "../../adapters/get-tables";
import { createKyselyAdapter, getDatabaseType } from "../../adapters/kysely";
import type { MigrationTable } from "../../adapters/schema";
import { BetterAuthError } from "../../error/better-auth-error";
import type { BetterAuthOptions } from "../../types";
import type { FieldAttribute } from "../field";
import { toColumns } from "./to-columns";
export const BaseModels = ["session", "account", "user"];
async function findAllMigrations(db: Kysely<any>) {
try {
const res = await db.selectFrom(migrationTableName).selectAll().execute();
return res as MigrationTable[];
} catch (e) {
return [];
}
}
export const getMigrations = async (
option: BetterAuthOptions,
cli: boolean,
interrupt?: () => void,
) => {
const dbType = getDatabaseType(option);
if (!dbType) {
throw new BetterAuthError("Database type not found.");
}
const db = createKyselyAdapter(option);
if (!db) {
throw new BetterAuthError(
"Invalid Database Configuration. Make sure your database configuration is a kysely dialect, a mysql or postgres pool or a configuration.",
);
}
const migrations = await findAllMigrations(db);
const pluginsMigrations =
option.plugins?.map((plugin) => ({
migrations: Object.keys(plugin.schema || {})
.map((key) => {
const schema = plugin.schema || {};
const table = schema[key]!;
if (table?.disableMigration) {
return;
}
return {
tableName: key,
fields: table?.fields as Record<string, FieldAttribute>,
};
})
.filter((value) => value !== undefined),
default: plugin.migrations || {},
prefix: plugin.id,
})) || [];
const providerMigrations =
option.socialProvider?.map((provider) => ({
prefix: provider.id,
migrations: Object.keys(provider.schema || {})
.map((key) => {
const schema = provider.schema || {};
const table = schema[key]!;
if (table?.disableMigration) {
return;
}
return {
tableName: key,
fields: table?.fields as Record<string, FieldAttribute>,
};
})
.filter((value) => value !== undefined),
default: provider.migrations || {},
})) || [];
const baseSchema = getAuthTables(option);
const migrationsToRun: {
prefix: string;
migrations: {
tableName: string;
fields: Record<string, FieldAttribute>;
}[];
default: Record<string, Migration>;
}[] = [
{
prefix: "base",
migrations: [
{
tableName: baseSchema.user.tableName,
fields: baseSchema.user.fields,
},
{
tableName: baseSchema.session.tableName,
fields: baseSchema.session.fields,
},
{
tableName: baseSchema.account.tableName,
fields: baseSchema.account.fields,
},
],
default: {},
},
...pluginsMigrations,
...providerMigrations,
];
let returnedMigration: Record<string, Migration> = {};
const affected: {
[table in string]: string[];
} = {};
for (const toRun of migrationsToRun) {
if (!toRun.migrations) {
continue;
}
let pluginMigrations = {
...toRun.default,
};
for (const migration of toRun.migrations) {
const fields = migration.fields;
const modelName = migration.tableName;
const prefix = `${toRun.prefix}_${modelName}_`;
const modelMigrations = migrations.filter((migration) =>
migration.name.includes(prefix),
);
const isBaseAlter =
toRun.prefix !== "base" && BaseModels.includes(modelName);
const kyselyMigration = await getMigration({
migrations: modelMigrations,
fields,
dbType,
modelName,
prefix,
cli,
interrupt,
alter: isBaseAlter,
});
pluginMigrations = {
...pluginMigrations,
...kyselyMigration.migrations,
};
if (affected[modelName]) {
affected[modelName].push(...Object.keys(kyselyMigration.affected));
} else {
affected[modelName] = Object.keys(kyselyMigration.affected);
}
}
returnedMigration = {
...returnedMigration,
...pluginMigrations,
};
}
const noMigration = !Object.keys(affected).filter((key) => {
const fields = affected[key];
return fields?.length;
}).length;
return {
migrations: returnedMigration,
affected,
noMigration,
};
};
function toMigrationKey(fields: Record<string, FieldAttribute>) {
return Object.keys(fields).map(
(it) =>
`${it}!${fields[it]?.type[0]}!${
fields[it]?.required === false ? "f" : "t"
}`,
);
}
export function fromMigrationKey(key: string) {
const typeMap = {
s: "string",
n: "number",
b: "boolean",
d: "date",
} as const;
const [prefix, table, _migrationKey] = key.split("_");
const migrationKey = _migrationKey?.split("-");
if (!migrationKey) {
throw new BetterAuthError("Corrupted Migration");
}
const fields: Record<string, FieldAttribute> = {};
for (const k of migrationKey) {
const [key, type, required] = k.split("!") as [
string,
"n" | "s" | "b" | "d",
"t" | "f",
];
if (!key || !type || !required) {
throw new BetterAuthError("Corrupted Migration");
}
fields[key] = {
type: typeMap[type],
required: required !== "f",
};
}
if (!table || !prefix || !fields) {
throw new BetterAuthError("Corrupted Migration");
}
return { fields, prefix, table };
}
/**
* Migrations are identified through a name.
* The name is a combination of the timestamp and the fields that are on the model with the prefix,
* modelName and their properties.
*/
export const getMigration = async ({
migrations,
fields,
dbType,
modelName,
prefix,
cli,
defaultFields: _defaultFields,
interrupt,
alter,
}: {
migrations: MigrationTable[];
fields: Record<string, FieldAttribute>;
dbType: "sqlite" | "mysql" | "postgres";
modelName: string;
prefix: string;
cli: boolean;
defaultFields?: {
id?: boolean;
createdAt?: boolean;
updatedAt?: boolean;
};
interrupt?: () => void;
alter?: boolean;
}) => {
const defaultFields = {
id: true,
createdAt: true,
updatedAt: true,
..._defaultFields,
};
/**
* We're using the fields as the migration key so that we can
* have multiple migrations when the user changes their
* schema.
*/
const migrationKey = toMigrationKey(fields);
const oldMigrationKey = migrations[migrations.length - 1]?.name
.split(prefix)[1]
?.split("|")[0]
?.split("-");
let toAdd: Record<string, FieldAttribute> | null = null;
let toUpdate: Record<string, FieldAttribute> | null = null;
let toRemove: Record<string, FieldAttribute> | null = null;
const typeMap = {
s: "string",
n: "number",
b: "boolean",
d: "date",
} as const;
for (const k of migrationKey) {
const [key, type, required] = k.split("!") as [
string,
"n" | "s" | "b" | "d",
"t" | "f",
];
if (!key || !type || !required) {
throw new BetterAuthError("Corrupted Migration");
}
const existing = oldMigrationKey?.find((it) => it.split("!")[0] === key);
const field = fields[key];
if (!existing) {
if (!toAdd) {
toAdd = {};
}
toAdd[key] = {
type: typeMap[type],
required: required !== "f",
...field,
};
continue;
}
const [_, oldType, oldRequired] = existing.split("!");
if (oldType !== type || oldRequired !== required) {
if (!toUpdate) {
toUpdate = {};
}
toUpdate[key] = {
type: typeMap[type],
required: required !== "f",
...field,
};
}
}
for (const k of oldMigrationKey || []) {
const [key] = k.split("!");
if (!key) {
throw new BetterAuthError("Corrupted Migration");
}
const exists = migrationKey?.find((it) => it.split("!")[0] === key);
if (!exists) {
if (!toRemove) {
toRemove = {};
}
toRemove[key] = {
type: "string",
required: true,
};
}
}
async function up(db: Kysely<any>): Promise<void> {
let schema:
| CreateTableBuilder<any, any>
| AlterTableBuilder
| AlterTableColumnAlteringBuilder;
schema = db.schema.createTable(modelName);
if (oldMigrationKey || alter) {
schema = db.schema.alterTable(modelName);
}
if (!oldMigrationKey && !alter) {
if (defaultFields.id) {
schema = schema.addColumn("id", "text", (col) =>
col.primaryKey().notNull(),
);
}
}
schema = toAdd
? toColumns(toAdd, { dbType, builder: schema, to: "add" })
: schema;
schema = toUpdate
? toColumns(toUpdate, { dbType, builder: schema, to: "update" })
: schema;
schema = toRemove
? toColumns(toRemove, { dbType, builder: schema, to: "remove" })
: schema;
if ("execute" in schema) {
const compiled = schema.compile();
if (
compiled.sql.includes("alter") &&
compiled.sql.split(",").length > 1 &&
dbType === "sqlite"
) {
/**
* Sqlite doesn't support multiple columns update in one alter statement.
* This will separate each statement and build a transaction instead.
*
* @see https://stackoverflow.com/questions/6172815/sqlite-alter-table-add-multiple-columns-in-a-single-statement
*/
const alterSt = compiled.sql.split(",")[0]?.split(" add column")[0];
const compiledQ = compiled.sql
.split(",")
.map((q) =>
q.startsWith("alter table") ? q : `${alterSt}${q.replace(",", "")}`,
);
db.transaction().execute(async (tx) => {
for (const q of compiledQ) {
//@ts-expect-error q shouldn't be passed as template string
const s = sql(q).compile(tx);
await tx.executeQuery(s);
}
});
return;
}
await schema.execute();
}
}
async function down(db: Kysely<any>): Promise<void> {}
const latestMigration =
toAdd || toUpdate || toRemove
? {
[`${Date.now()}${prefix}${migrationKey.join("-")}|`]: {
up,
down,
},
}
: {};
const kMigrations = migrations.reduce((acc, it) => {
return {
...acc,
[it.name]: {
up: async (db: Kysely<any>) => {},
down: async (db: Kysely<any>) => {},
},
};
}, latestMigration);
return {
migrations: kMigrations,
affected: {
...toAdd,
...toRemove,
...toUpdate,
},
};
};

View File

@@ -1,114 +0,0 @@
import chalk from "chalk";
import { Kysely, Migrator } from "kysely";
import ora from "ora";
import prompts from "prompts";
import { getDialect } from "../../adapters/kysely";
import { BetterAuthError } from "../../error/better-auth-error";
import type { BetterAuthOptions } from "../../types";
import { logger } from "../../utils/logger";
import { getMigrations } from "./get-migrations";
export const migrationTableName = "better_auth_migrations";
export const migrationLockTableName = "better_auth_migrations_lock";
export const migrateAll = async (
options: BetterAuthOptions,
{ cli }: { cli: boolean },
) => {
const spinner = cli ? ora("preparing migration...").start() : null;
const { migrations, noMigration, affected } = await getMigrations(
options,
cli,
() => spinner?.stop(),
);
spinner?.stop();
if (!noMigration && cli) {
logger.info(chalk.bgBlack("\n🔑 The migration will affect the following:"));
for (const key of Object.keys(affected)) {
const i = affected[key];
if (!i?.length) {
continue;
}
logger.info(
"->",
chalk.magenta(i.join(", ")),
chalk.white("fields on"),
chalk.yellow(`${key}`),
chalk.white("table."),
);
}
const { migrate } = await prompts({
type: "confirm",
name: "migrate",
message: "Are you sure you want to run migrations?",
initial: false,
});
if (!migrate) {
logger.info("Migration cancelled.");
process.exit(0);
}
}
spinner?.start("migrating...");
const dialect = getDialect(options);
if (!dialect) {
if (cli) {
logger.error("Invalid database configuration.");
process.exit(1);
} else {
throw new BetterAuthError("Invalid database configuration.");
}
}
const db = new Kysely({
dialect,
});
const migrator = new Migrator({
db,
provider: {
async getMigrations() {
return migrations;
},
},
migrationTableName,
migrationLockTableName,
});
try {
const { error, results } = await migrator.migrateToLatest();
spinner?.stop();
results?.forEach((it, index) => {
if (it.status === "Error") {
logger.error(
`failed to execute ${
it.migrationName.split("_")[1]?.split("_")[0]
} migration`,
);
}
});
if (!results?.length && !error) {
logger.success("🚀 No migrations to run.");
} else {
logger.success("🚀 migration was completed successfully!");
}
if (error) {
if (cli) {
logger.error("failed to migrate");
logger.error(error);
process.exit(1);
} else {
throw new BetterAuthError("failed to migrate");
}
}
return {
results,
error,
};
} catch (e) {
spinner?.stop();
logger.error(e);
throw e;
}
};

View File

@@ -1,83 +0,0 @@
import type {
AlterTableBuilder,
AlterTableColumnAlteringBuilder,
CreateTableBuilder,
} from "kysely";
import type { FieldAttribute } from "../../db";
import { BetterAuthError } from "../../error/better-auth-error";
export function toColumns(
fields: Record<string, FieldAttribute>,
{
dbType,
builder,
to,
}: {
dbType: string;
to: "update" | "add" | "remove";
builder:
| AlterTableBuilder
| AlterTableColumnAlteringBuilder
| CreateTableBuilder<any, any>;
},
) {
for (const key in fields) {
if (key === "id") {
continue;
}
const val = fields[key as keyof typeof fields];
if (!val) {
continue;
}
const typeMap = {
string: "text",
boolean: "boolean",
number: "integer",
date: "date",
} as const;
const type = typeMap[val.type];
if (!type) {
throw new BetterAuthError("Invalid type in your user schema config.");
}
if (dbType === "sqlite" && type === "boolean") {
if (to === "add") {
builder = builder.addColumn(key, "integer", (col) =>
val.required !== false ? col.notNull() : col,
);
}
if (to === "update") {
builder = (builder as AlterTableBuilder)
.alterColumn(key, (col) => col.setDataType("integer"))
.alterColumn(key, (col) =>
val.required !== false ? col.setNotNull() : col.dropNotNull(),
);
}
if (to === "remove") {
builder = (builder as AlterTableColumnAlteringBuilder).dropColumn(key);
}
} else {
if (to === "add") {
builder = builder.addColumn(key, type, (col) => {
col = val.required !== false ? col.notNull() : col;
if (val.references) {
col = col
.references(`${val.references.model}.${val.references.field}`)
.onDelete(val.references.onDelete || "cascade");
}
return col;
});
}
if (to === "update") {
builder = (builder as AlterTableBuilder)
.alterColumn(key, (col) => col.setDataType(type))
.alterColumn(key, (col) =>
val.required !== false ? col.setNotNull() : col.dropNotNull(),
);
}
if (to === "remove") {
builder = (builder as AlterTableColumnAlteringBuilder).dropColumn(key);
}
}
}
return builder as CreateTableBuilder<any, any>;
}

View File

@@ -8,8 +8,8 @@ import type { Prettify } from "../../types/helper";
import type { organization as org } from "../../plugins";
import { createClientPlugin } from "../../client/create-client-plugin";
export const organization = createClientPlugin<ReturnType<typeof org>>()(
($fetch) => {
export const organizationClient = () =>
createClientPlugin<ReturnType<typeof org>>()(($fetch) => {
const activeOrgId = atom<string | null>(null);
const $listOrg = atom<boolean>(false);
const $activeOrgSignal = atom<boolean>(false);
@@ -143,5 +143,4 @@ export const organization = createClientPlugin<ReturnType<typeof org>>()(
$activeOrgSignal,
},
};
},
);
});

View File

@@ -53,7 +53,7 @@ export const getPasskeyActions = ($fetch: BetterFetch) => {
}
};
const register = async () => {
const registerPasskey = async () => {
const options = await $fetch<PublicKeyCredentialCreationOptionsJSON>(
"/passkey/generate-register-options",
{
@@ -93,7 +93,7 @@ export const getPasskeyActions = ($fetch: BetterFetch) => {
};
return {
signInPasskey,
register,
registerPasskey,
};
};

View File

@@ -10,19 +10,6 @@ export interface TwoFactorOptions {
totpOptions?: Omit<TOTPOptions, "issuer">;
otpOptions?: OTPOptions;
backupCodeOptions?: BackupCodeOptions;
requireOn?: {
signIn: () => boolean;
};
/**
* The url to redirect to after the user has
* signed in to validate the two factor. If not
* provided, the callbackURL will be used. If
* callbackURL is not provided, the user will be
* redirected to the root path.
*
* @default "/"
*/
twoFactorURL?: string;
}
export interface UserWithTwoFactor extends User {

View File

@@ -107,7 +107,7 @@ export const username = () => {
email: z.string().email(),
password: z.string(),
image: z.string().optional(),
callbackUrl: z.string().optional(),
callbackURL: z.string().optional(),
}),
},
async (ctx) => {
@@ -125,14 +125,18 @@ export const username = () => {
},
});
}
const updatedUser =
await ctx.context.internalAdapter.updateUserByEmail(
res.user.email,
{
username: ctx.body.username,
await ctx.context.internalAdapter.updateUserByEmail(res.user.email, {
username: ctx.body.username,
});
if (ctx.body.callbackURL) {
return ctx.json(res, {
body: {
url: ctx.body.callbackURL,
redirect: true,
...res,
},
);
console.log(updatedUser);
});
}
return ctx.json(res);
},
),

View File

@@ -83,12 +83,6 @@ export interface BetterAuthOptions {
| {
provider: "postgres" | "sqlite" | "mysql";
url: string;
/**
* Automatically migrate the database
*
* @default false
*/
autoMigrate?: boolean;
}
| Adapter;
/**

823
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,4 +4,5 @@
[x] remember me functionality
[x] add all oauth providers
[x] providers should only be oauth
[ ] add tests
[ ] add tests
[ ] add callback url on otp and backup code verification