Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions frameworks/react-cra/add-ons/better-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Setting up Better Auth

1. Generate and set the `BETTER_AUTH_SECRET` environment variable in your `.env.local`:

```bash
npx @better-auth/cli secret
```

2. Visit the [Better Auth documentation](https://www.better-auth.com) to unlock the full potential of authentication in your app.

### Adding a Database (Optional)

Better Auth can work in stateless mode, but to persist user data, add a database:

```typescript
// src/lib/auth.ts
import { betterAuth } from "better-auth";
import { Pool } from "pg";

export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL,
}),
// ... rest of config
});
```

Then run migrations:

```bash
npx @better-auth/cli migrate
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Better Auth configuration
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET= # Generate a secret key: `npx @better-auth/cli secret`
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { authClient } from "@/lib/auth-client";
import { Link } from "@tanstack/react-router";

export default function BetterAuthHeader() {
const { data: session, isPending } = authClient.useSession();

if (isPending) {
return (
<div className="h-8 w-8 bg-neutral-100 dark:bg-neutral-800 animate-pulse" />
);
}

if (session?.user) {
return (
<div className="flex items-center gap-2">
{session.user.image ? (
<img src={session.user.image} alt="" className="h-8 w-8" />
) : (
<div className="h-8 w-8 bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center">
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-400">
{session.user.name?.charAt(0).toUpperCase() || "U"}
</span>
</div>
)}
<button
onClick={() => authClient.signOut()}
className="flex-1 h-9 px-4 text-sm font-medium bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50 border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors"
>
Sign out
</button>
</div>
);
}

return (
<Link
to="/demo/better-auth"
className="h-9 px-4 text-sm font-medium bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50 border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors inline-flex items-center"
>
Sign in
</Link>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createAuthClient } from 'better-auth/react'

export const authClient = createAuthClient()
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { betterAuth } from 'better-auth'

export const auth = betterAuth({
emailAndPassword: {
enabled: true,
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createFileRoute } from '@tanstack/react-router'
import { auth } from '@/lib/auth'

export const Route = createFileRoute('/api/auth/$')({
server: {
handlers: {
GET: ({ request }) => auth.handler(request),
POST: ({ request }) => auth.handler(request),
},
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { authClient } from "@/lib/auth-client";

export const Route = createFileRoute("/demo/better-auth")({
component: BetterAuthDemo,
});

function BetterAuthDemo() {
const { data: session, isPending } = authClient.useSession();
const [isSignUp, setIsSignUp] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);

if (isPending) {
return (
<div className="flex items-center justify-center py-10">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-neutral-200 border-t-neutral-900 dark:border-neutral-800 dark:border-t-neutral-100" />
</div>
);
}

if (session?.user) {
return (
<div className="flex justify-center py-10 px-4">
<div className="w-full max-w-md p-6 space-y-6">
<div className="space-y-1.5">
<h1 className="text-lg font-semibold leading-none tracking-tight">
Welcome back
</h1>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
You're signed in as {session.user.email}
</p>
</div>

<div className="flex items-center gap-3">
{session.user.image ? (
<img src={session.user.image} alt="" className="h-10 w-10" />
) : (
<div className="h-10 w-10 bg-neutral-200 dark:bg-neutral-800 flex items-center justify-center">
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
{session.user.name?.charAt(0).toUpperCase() || "U"}
</span>
</div>
)}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">
{session.user.name}
</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400 truncate">
{session.user.email}
</p>
</div>
</div>

<button
onClick={() => authClient.signOut()}
className="w-full h-9 px-4 text-sm font-medium border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
>
Sign out
</button>

<p className="text-xs text-center text-neutral-400 dark:text-neutral-500">
Built with{" "}
<a
href="https://better-auth.com"
target="_blank"
rel="noopener noreferrer"
className="font-medium hover:text-neutral-600 dark:hover:text-neutral-300"
>
BETTER-AUTH
</a>
.
</p>
</div>
</div>
);
}

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);

try {
if (isSignUp) {
const result = await authClient.signUp.email({
email,
password,
name,
});
if (result.error) {
setError(result.error.message || "Sign up failed");
}
} else {
const result = await authClient.signIn.email({
email,
password,
});
if (result.error) {
setError(result.error.message || "Sign in failed");
}
}
} catch (err) {
setError("An unexpected error occurred");
} finally {
setLoading(false);
}
};

return (
<div className="flex justify-center py-10 px-4">
<div className="w-full max-w-md p-6">
<h1 className="text-lg font-semibold leading-none tracking-tight">
{isSignUp ? "Create an account" : "Sign in"}
</h1>
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-2 mb-6">
{isSignUp
? "Enter your information to create an account"
: "Enter your email below to login to your account"}
</p>

<form onSubmit={handleSubmit} className="grid gap-4">
{isSignUp && (
<div className="grid gap-2">
<label
htmlFor="name"
className="text-sm font-medium leading-none"
>
Name
</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="flex h-9 w-full border border-neutral-300 dark:border-neutral-700 bg-transparent px-3 text-sm focus:outline-none focus:border-neutral-900 dark:focus:border-neutral-100 disabled:cursor-not-allowed disabled:opacity-50"
required
/>
</div>
)}

<div className="grid gap-2">
<label htmlFor="email" className="text-sm font-medium leading-none">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="flex h-9 w-full border border-neutral-300 dark:border-neutral-700 bg-transparent px-3 text-sm focus:outline-none focus:border-neutral-900 dark:focus:border-neutral-100 disabled:cursor-not-allowed disabled:opacity-50"
required
/>
</div>

<div className="grid gap-2">
<label
htmlFor="password"
className="text-sm font-medium leading-none"
>
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="flex h-9 w-full border border-neutral-300 dark:border-neutral-700 bg-transparent px-3 text-sm focus:outline-none focus:border-neutral-900 dark:focus:border-neutral-100 disabled:cursor-not-allowed disabled:opacity-50"
required
minLength={8}
/>
</div>

{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3">
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}

<button
type="submit"
disabled={loading}
className="w-full h-9 px-4 text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-800 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-neutral-400 border-t-white dark:border-neutral-600 dark:border-t-neutral-900" />
<span>Please wait</span>
</span>
) : isSignUp ? (
"Create account"
) : (
"Sign in"
)}
</button>
</form>

<div className="mt-4 text-center">
<button
type="button"
onClick={() => {
setIsSignUp(!isSignUp);
setError("");
}}
className="text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 transition-colors"
>
{isSignUp
? "Already have an account? Sign in"
: "Don't have an account? Sign up"}
</button>
</div>

<p className="mt-6 text-xs text-center text-neutral-400 dark:text-neutral-500">
Built with{" "}
<a
href="https://better-auth.com"
target="_blank"
rel="noopener noreferrer"
className="font-medium hover:text-neutral-600 dark:hover:text-neutral-300"
>
BETTER-AUTH
</a>
.
</p>
</div>
</div>
);
}
26 changes: 26 additions & 0 deletions frameworks/react-cra/add-ons/better-auth/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "Better Auth",
"description": "Add Better Auth authentication to your application.",
"phase": "add-on",
"type": "add-on",
"priority": 26,
"link": "https://www.better-auth.com",
"modes": ["file-router"],
"tailwind": true,
"dependsOn": ["start"],
"routes": [
{
"url": "/demo/better-auth",
"name": "Better Auth",
"path": "src/routes/demo/better-auth.tsx",
"jsName": "BetterAuthDemo"
}
],
"integrations": [
{
"type": "header-user",
"jsName": "BetterAuthHeader",
"path": "src/integrations/better-auth/header-user.tsx"
}
]
}
7 changes: 7 additions & 0 deletions frameworks/react-cra/add-ons/better-auth/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions frameworks/react-cra/add-ons/better-auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"better-auth": "^1.4.12"
}
}
9 changes: 9 additions & 0 deletions frameworks/react-cra/add-ons/better-auth/small-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading