Files
better-auth/docs/components/builder/code-tabs/frameworks/nextjs.ts
2026-01-14 13:22:14 -08:00

597 lines
14 KiB
TypeScript

import { socialProviders } from "../../social-provider";
import type { SignInBoxOptions } from "../../store";
export function resolveNextJSFiles(options: SignInBoxOptions) {
const files = [
{
id: "1",
name: "auth.ts",
content: `import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next";${
options.magicLink
? `
import { magicLink } from "better-auth/plugins";`
: ""
}${
options.passkey
? `
import { passkey } from "@better-auth/passkey";`
: ""
}
export const auth = betterAuth({
${
options.email
? `emailAndPassword: {
enabled: true,
${
options.requestPasswordReset
? `async sendResetPassword(data, request) {
// Send an email to the user with a link to reset their password
},`
: ``
}
},`
: ""
}${
options.socialProviders.length
? `socialProviders: ${JSON.stringify(
options.socialProviders.reduce(
(acc, provider) => ({
...acc,
[provider]: {
clientId: `process.env.${provider.toUpperCase()}_CLIENT_ID!`,
clientSecret: `process.env.${provider.toUpperCase()}_CLIENT_SECRET!`,
},
}),
{},
),
).replace(/"/g, "")},`
: ""
}
plugins: [${
options.magicLink
? `
magicLink({
async sendMagicLink(data) {
// Send an email to the user with a magic link
},
}),`
: ""
}${
options.passkey
? `
passkey(),`
: ""
}
nextCookies(),
],
/** if no database is provided, the user data will be stored in memory.
* Make sure to provide a database to persist user data **/
});
`,
},
{
id: "2",
name: "auth-client.ts",
content: `import { createAuthClient } from "better-auth/react";${
options.magicLink
? `
import { magicLinkClient } from "better-auth/client/plugins";`
: ""
}${
options.passkey
? `
import { passkeyClient } from "@better-auth/passkey/client";`
: ""
}
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,${
options.magicLink || options.passkey
? `
plugins: [${options.magicLink ? `magicLinkClient()${options.passkey ? "," : ""}` : ""}${
options.passkey ? `passkeyClient()` : ""
}],`
: ""
}
});
export const { signIn, signOut, signUp, useSession } = authClient;
`,
},
{
id: "3",
name: "sign-in.tsx",
content: signInString(options),
},
];
if (options.signUp) {
files.push({
id: "4",
name: "sign-up.tsx",
content: signUpString(options),
});
}
return files;
}
const signInString = (options: SignInBoxOptions) => `"use client"
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { useState } from "react";
import { Loader2, Key } from "lucide-react";
import { signIn } from "@/lib/auth-client";
import Link from "next/link";
import { cn } from "@/lib/utils";
export default function SignIn() {${
options.email || options.magicLink
? `
const [email, setEmail] = useState("");`
: ""
}${
options.email
? `
const [password, setPassword] = useState("");`
: ""
}
const [loading, setLoading] = useState(false);${
options.rememberMe
? `
const [rememberMe, setRememberMe] = useState(false);`
: ""
}
return (
<Card className="max-w-md">
<CardHeader>
<CardTitle className="text-lg md:text-xl">Sign In</CardTitle>
<CardDescription className="text-xs md:text-sm">
Enter your email below to login to your account
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
${
options.email
? `<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
onChange={(e) => {
setEmail(e.target.value);
}}
value={email}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>${
options.requestPasswordReset
? `
<Link
href="#"
className="ml-auto inline-block text-sm underline"
>
Forgot your password?
</Link>`
: ""
}
</div>
<Input
id="password"
type="password"
placeholder="password"
autoComplete="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>${
options.rememberMe
? `
<div className="flex items-center gap-2">
<Checkbox
id="remember"
onClick={() => {
setRememberMe(!rememberMe);
}}
/>
<Label htmlFor="remember">Remember me</Label>
</div>`
: ""
}`
: ""
}${
options.magicLink
? `<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
onChange={(e) => {
setEmail(e.target.value);
}}
value={email}
/>
<Button
disabled={loading}
className="gap-2"
onClick={async () => {
await signIn.magicLink({
email,
fetchOptions: {
onRequest: () => {
setLoading(true);
},
onResponse: () => {
setLoading(false);
},
},
});
}}>
{loading ? (
<Loader2 size={16} className="animate-spin" />
):(
Sign-in with Magic Link
)}
</Button>
</div>`
: ""
}${
options.email
? `
<Button
type="submit"
className="w-full"
disabled={loading}
onClick={async () => {
await signIn.email({
email,
password,${
options.rememberMe
? `
rememberMe,`
: ""
}
fetchOptions: {
onRequest: () => {
setLoading(true);
},
onResponse: () => {
setLoading(false);
},
},
});
}}
>
{loading ? (
<Loader2 size={16} className="animate-spin" />
) : (
<p>Login</p>
)}
</Button>`
: ""
}${
options.passkey
? `
<Button
variant="secondary"
disabled={loading}
className="gap-2"
onClick={async () => {
await signIn.passkey({
fetchOptions: {
onRequest: () => {
setLoading(true);
},
onResponse: () => {
setLoading(false);
},
},
});
}}
>
<Key size={16} />
Sign-in with Passkey
</Button>`
: ""
}${
options.socialProviders?.length > 0
? `
<div className={cn(
"w-full gap-2 flex items-center",
${
options.socialProviders.length > 3
? '"justify-between flex-wrap"'
: '"justify-between flex-col"'
}
)}>
${options.socialProviders
.map((provider: string) => {
const icon =
socialProviders[provider as keyof typeof socialProviders]
?.stringIcon || "";
return `<Button
variant="outline"
className=${
options.socialProviders.length > 3
? '"flex-grow"'
: '"w-full gap-2"'
}
disabled={loading}
onClick={async () => {
await signIn.social({
provider: "${provider}",
callbackURL: "/dashboard",
fetchOptions: {
onRequest: () => {
setLoading(true);
},
onResponse: () => {
setLoading(false);
},
},
});
}}
>
${icon}
${
options.socialProviders.length <= 3
? `Sign in with ${
provider.charAt(0).toUpperCase() + provider.slice(1)
}`
: ""
}
</Button>`;
})
.join("\n\t\t\t\t\t\t")}
</div>`
: ""
}
</div>
</CardContent>
${
options.label
? `<CardFooter>
<div className="flex justify-center w-full border-t py-4">
<p className="text-center text-xs text-neutral-500">
built with{" "}
<Link
href="https://better-auth.com"
className="underline"
target="_blank"
>
<span className="dark:text-white/70 cursor-pointer">
better-auth.
</span>
</Link>
</p>
</div>
</CardFooter>`
: ""
}
</Card>
);
}`;
const signUpString = (options: SignInBoxOptions) => `"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useState } from "react";
import Image from "next/image";
import { Loader2, X } from "lucide-react";
import { signUp } from "@/lib/auth-client";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
export default function SignUp() {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState("");
const [image, setImage] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const router = useRouter();
const [loading, setLoading] = useState(false);
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setImage(file);
const reader = new FileReader();
reader.onloadend = () => {
setImagePreview(reader.result as string);
};
reader.readAsDataURL(file);
}
};
return (
<Card className="z-50 rounded-md rounded-t-none max-w-md">
<CardHeader>
<CardTitle className="text-lg md:text-xl">Sign Up</CardTitle>
<CardDescription className="text-xs md:text-sm">
Enter your information to create an account
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="first-name">First name</Label>
<Input
id="first-name"
placeholder="Max"
required
onChange={(e) => {
setFirstName(e.target.value);
}}
value={firstName}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="last-name">Last name</Label>
<Input
id="last-name"
placeholder="Robinson"
required
onChange={(e) => {
setLastName(e.target.value);
}}
value={lastName}
/>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
onChange={(e) => {
setEmail(e.target.value);
}}
value={email}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="new-password"
placeholder="Password"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password_confirmation">Confirm Password</Label>
<Input
id="password_confirmation"
type="password"
value={passwordConfirmation}
onChange={(e) => setPasswordConfirmation(e.target.value)}
autoComplete="new-password"
placeholder="Confirm Password"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="image">Profile Image (optional)</Label>
<div className="flex items-end gap-4">
{imagePreview && (
<div className="relative w-16 h-16 rounded-sm overflow-hidden">
<Image
src={imagePreview}
alt="Profile preview"
layout="fill"
objectFit="cover"
/>
</div>
)}
<div className="flex items-center gap-2 w-full">
<Input
id="image"
type="file"
accept="image/*"
onChange={handleImageChange}
className="w-full"
/>
{imagePreview && (
<X
className="cursor-pointer"
onClick={() => {
setImage(null);
setImagePreview(null);
}}
/>
)}
</div>
</div>
</div>
<Button
type="submit"
className="w-full"
disabled={loading}
onClick={async () => {
await signUp.email({
email,
password,
name: \`\${firstName} \${lastName}\`,
image: image ? await convertImageToBase64(image) : "",
callbackURL: "/dashboard",
fetchOptions: {
onResponse: () => {
setLoading(false);
},
onRequest: () => {
setLoading(true);
},
onError: (ctx) => {
toast.error(ctx.error.message);
},
onSuccess: () => {
router.push("/dashboard");
},
},
});
}}
>
{loading ? (
<Loader2 size={16} className="animate-spin" />
) : (
"Create your account"
)}
</Button>
</div>
</CardContent>${
options.label
? `
<CardFooter>
<div className="flex justify-center w-full border-t py-4">
<p className="text-center text-xs text-neutral-500">
Secured by <span className="text-orange-400">better-auth.</span>
</p>
</div>
</CardFooter>`
: ""
}
</Card>
);
}
async function convertImageToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}`;