Skip to content

Commit 2eb95ed

Browse files
authored
feat: add Better Auth add-on (#291)
* feat: add Better Auth add-on for React * feat: add Better Auth add-on for Solid * refactor: add newline at end of file * refactor: add newline at end of file
1 parent c4f2e1b commit 2eb95ed

File tree

22 files changed

+767
-0
lines changed

22 files changed

+767
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## Setting up Better Auth
2+
3+
1. Generate and set the `BETTER_AUTH_SECRET` environment variable in your `.env.local`:
4+
5+
```bash
6+
npx @better-auth/cli secret
7+
```
8+
9+
2. Visit the [Better Auth documentation](https://www.better-auth.com) to unlock the full potential of authentication in your app.
10+
11+
### Adding a Database (Optional)
12+
13+
Better Auth can work in stateless mode, but to persist user data, add a database:
14+
15+
```typescript
16+
// src/lib/auth.ts
17+
import { betterAuth } from "better-auth";
18+
import { Pool } from "pg";
19+
20+
export const auth = betterAuth({
21+
database: new Pool({
22+
connectionString: process.env.DATABASE_URL,
23+
}),
24+
// ... rest of config
25+
});
26+
```
27+
28+
Then run migrations:
29+
30+
```bash
31+
npx @better-auth/cli migrate
32+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Better Auth configuration
2+
BETTER_AUTH_URL=http://localhost:3000
3+
BETTER_AUTH_SECRET= # Generate a secret key: `npx @better-auth/cli secret`
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { authClient } from "@/lib/auth-client";
2+
import { Link } from "@tanstack/react-router";
3+
4+
export default function BetterAuthHeader() {
5+
const { data: session, isPending } = authClient.useSession();
6+
7+
if (isPending) {
8+
return (
9+
<div className="h-8 w-8 bg-neutral-100 dark:bg-neutral-800 animate-pulse" />
10+
);
11+
}
12+
13+
if (session?.user) {
14+
return (
15+
<div className="flex items-center gap-2">
16+
{session.user.image ? (
17+
<img src={session.user.image} alt="" className="h-8 w-8" />
18+
) : (
19+
<div className="h-8 w-8 bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center">
20+
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-400">
21+
{session.user.name?.charAt(0).toUpperCase() || "U"}
22+
</span>
23+
</div>
24+
)}
25+
<button
26+
onClick={() => authClient.signOut()}
27+
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"
28+
>
29+
Sign out
30+
</button>
31+
</div>
32+
);
33+
}
34+
35+
return (
36+
<Link
37+
to="/demo/better-auth"
38+
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"
39+
>
40+
Sign in
41+
</Link>
42+
);
43+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createAuthClient } from 'better-auth/react'
2+
3+
export const authClient = createAuthClient()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { betterAuth } from 'better-auth'
2+
3+
export const auth = betterAuth({
4+
emailAndPassword: {
5+
enabled: true,
6+
},
7+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
import { auth } from '@/lib/auth'
3+
4+
export const Route = createFileRoute('/api/auth/$')({
5+
server: {
6+
handlers: {
7+
GET: ({ request }) => auth.handler(request),
8+
POST: ({ request }) => auth.handler(request),
9+
},
10+
},
11+
})
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { createFileRoute } from "@tanstack/react-router";
2+
import { useState } from "react";
3+
import { authClient } from "@/lib/auth-client";
4+
5+
export const Route = createFileRoute("/demo/better-auth")({
6+
component: BetterAuthDemo,
7+
});
8+
9+
function BetterAuthDemo() {
10+
const { data: session, isPending } = authClient.useSession();
11+
const [isSignUp, setIsSignUp] = useState(false);
12+
const [email, setEmail] = useState("");
13+
const [password, setPassword] = useState("");
14+
const [name, setName] = useState("");
15+
const [error, setError] = useState("");
16+
const [loading, setLoading] = useState(false);
17+
18+
if (isPending) {
19+
return (
20+
<div className="flex items-center justify-center py-10">
21+
<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" />
22+
</div>
23+
);
24+
}
25+
26+
if (session?.user) {
27+
return (
28+
<div className="flex justify-center py-10 px-4">
29+
<div className="w-full max-w-md p-6 space-y-6">
30+
<div className="space-y-1.5">
31+
<h1 className="text-lg font-semibold leading-none tracking-tight">
32+
Welcome back
33+
</h1>
34+
<p className="text-sm text-neutral-500 dark:text-neutral-400">
35+
You're signed in as {session.user.email}
36+
</p>
37+
</div>
38+
39+
<div className="flex items-center gap-3">
40+
{session.user.image ? (
41+
<img src={session.user.image} alt="" className="h-10 w-10" />
42+
) : (
43+
<div className="h-10 w-10 bg-neutral-200 dark:bg-neutral-800 flex items-center justify-center">
44+
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
45+
{session.user.name?.charAt(0).toUpperCase() || "U"}
46+
</span>
47+
</div>
48+
)}
49+
<div className="flex-1 min-w-0">
50+
<p className="text-sm font-medium truncate">
51+
{session.user.name}
52+
</p>
53+
<p className="text-xs text-neutral-500 dark:text-neutral-400 truncate">
54+
{session.user.email}
55+
</p>
56+
</div>
57+
</div>
58+
59+
<button
60+
onClick={() => authClient.signOut()}
61+
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"
62+
>
63+
Sign out
64+
</button>
65+
66+
<p className="text-xs text-center text-neutral-400 dark:text-neutral-500">
67+
Built with{" "}
68+
<a
69+
href="https://better-auth.com"
70+
target="_blank"
71+
rel="noopener noreferrer"
72+
className="font-medium hover:text-neutral-600 dark:hover:text-neutral-300"
73+
>
74+
BETTER-AUTH
75+
</a>
76+
.
77+
</p>
78+
</div>
79+
</div>
80+
);
81+
}
82+
83+
const handleSubmit = async (e: React.FormEvent) => {
84+
e.preventDefault();
85+
setError("");
86+
setLoading(true);
87+
88+
try {
89+
if (isSignUp) {
90+
const result = await authClient.signUp.email({
91+
email,
92+
password,
93+
name,
94+
});
95+
if (result.error) {
96+
setError(result.error.message || "Sign up failed");
97+
}
98+
} else {
99+
const result = await authClient.signIn.email({
100+
email,
101+
password,
102+
});
103+
if (result.error) {
104+
setError(result.error.message || "Sign in failed");
105+
}
106+
}
107+
} catch (err) {
108+
setError("An unexpected error occurred");
109+
} finally {
110+
setLoading(false);
111+
}
112+
};
113+
114+
return (
115+
<div className="flex justify-center py-10 px-4">
116+
<div className="w-full max-w-md p-6">
117+
<h1 className="text-lg font-semibold leading-none tracking-tight">
118+
{isSignUp ? "Create an account" : "Sign in"}
119+
</h1>
120+
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-2 mb-6">
121+
{isSignUp
122+
? "Enter your information to create an account"
123+
: "Enter your email below to login to your account"}
124+
</p>
125+
126+
<form onSubmit={handleSubmit} className="grid gap-4">
127+
{isSignUp && (
128+
<div className="grid gap-2">
129+
<label
130+
htmlFor="name"
131+
className="text-sm font-medium leading-none"
132+
>
133+
Name
134+
</label>
135+
<input
136+
id="name"
137+
type="text"
138+
value={name}
139+
onChange={(e) => setName(e.target.value)}
140+
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"
141+
required
142+
/>
143+
</div>
144+
)}
145+
146+
<div className="grid gap-2">
147+
<label htmlFor="email" className="text-sm font-medium leading-none">
148+
Email
149+
</label>
150+
<input
151+
id="email"
152+
type="email"
153+
value={email}
154+
onChange={(e) => setEmail(e.target.value)}
155+
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"
156+
required
157+
/>
158+
</div>
159+
160+
<div className="grid gap-2">
161+
<label
162+
htmlFor="password"
163+
className="text-sm font-medium leading-none"
164+
>
165+
Password
166+
</label>
167+
<input
168+
id="password"
169+
type="password"
170+
value={password}
171+
onChange={(e) => setPassword(e.target.value)}
172+
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"
173+
required
174+
minLength={8}
175+
/>
176+
</div>
177+
178+
{error && (
179+
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3">
180+
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
181+
</div>
182+
)}
183+
184+
<button
185+
type="submit"
186+
disabled={loading}
187+
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"
188+
>
189+
{loading ? (
190+
<span className="flex items-center justify-center gap-2">
191+
<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" />
192+
<span>Please wait</span>
193+
</span>
194+
) : isSignUp ? (
195+
"Create account"
196+
) : (
197+
"Sign in"
198+
)}
199+
</button>
200+
</form>
201+
202+
<div className="mt-4 text-center">
203+
<button
204+
type="button"
205+
onClick={() => {
206+
setIsSignUp(!isSignUp);
207+
setError("");
208+
}}
209+
className="text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 transition-colors"
210+
>
211+
{isSignUp
212+
? "Already have an account? Sign in"
213+
: "Don't have an account? Sign up"}
214+
</button>
215+
</div>
216+
217+
<p className="mt-6 text-xs text-center text-neutral-400 dark:text-neutral-500">
218+
Built with{" "}
219+
<a
220+
href="https://better-auth.com"
221+
target="_blank"
222+
rel="noopener noreferrer"
223+
className="font-medium hover:text-neutral-600 dark:hover:text-neutral-300"
224+
>
225+
BETTER-AUTH
226+
</a>
227+
.
228+
</p>
229+
</div>
230+
</div>
231+
);
232+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "Better Auth",
3+
"description": "Add Better Auth authentication to your application.",
4+
"phase": "add-on",
5+
"type": "add-on",
6+
"priority": 26,
7+
"link": "https://www.better-auth.com",
8+
"modes": ["file-router"],
9+
"tailwind": true,
10+
"dependsOn": ["start"],
11+
"routes": [
12+
{
13+
"url": "/demo/better-auth",
14+
"name": "Better Auth",
15+
"path": "src/routes/demo/better-auth.tsx",
16+
"jsName": "BetterAuthDemo"
17+
}
18+
],
19+
"integrations": [
20+
{
21+
"type": "header-user",
22+
"jsName": "BetterAuthHeader",
23+
"path": "src/integrations/better-auth/header-user.tsx"
24+
}
25+
]
26+
}
Lines changed: 7 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"better-auth": "^1.4.12"
4+
}
5+
}

0 commit comments

Comments
 (0)