BetterAuth
在 Tanstack Start 中配置 Better-auth 快速模板(使用 drizzle + better-sqlite3)
项目配置
.env
# DB
DATABASE_URL=./local.db
# Better Auth configuration
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=2e5612f38ab39aec8fc2c5dbf26edc4779134d729fd8c414a73e0fafc5009e95 # Generate a secret key: `pnpm dlx @better-auth/cli secret`
package.json
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:pull": "drizzle-kit pull",
"db:studio": "drizzle-kit studio"
数据库配置
drizzle.config.ts
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
out: './drizzle',
schema: './src/db/index.ts',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
})
src/db/index.ts
import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
export * from "./auth-schema"
import * as authSchema from "./auth-schema";
const schema = {
...authSchema,
}
const sqlite = new Database(process.env.DATABASE_URL);
export const db = drizzle(sqlite, { schema });
BetterAuth 配置
src/db/auth-schema.ts
import { relations, sql } from "drizzle-orm";
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
export const user = sqliteTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: integer("email_verified", { mode: "boolean" })
.default(false)
.notNull(),
image: text("image"),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
});
export const session = sqliteTable(
"session",
{
id: text("id").primaryKey(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
token: text("token").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => [index("session_userId_idx").on(table.userId)],
);
export const account = sqliteTable(
"account",
{
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: integer("access_token_expires_at", {
mode: "timestamp_ms",
}),
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
mode: "timestamp_ms",
}),
scope: text("scope"),
password: text("password"),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("account_userId_idx").on(table.userId)],
);
export const verification = sqliteTable(
"verification",
{
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("verification_identifier_idx").on(table.identifier)],
);
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
}));
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, {
fields: [session.userId],
references: [user.id],
}),
}));
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, {
fields: [account.userId],
references: [user.id],
}),
}));
src/lib/auth.ts
import { betterAuth } from 'better-auth'
import { tanstackStartCookies } from 'better-auth/tanstack-start'
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { db } from "@/db"
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "sqlite",
}),
emailAndPassword: {
enabled: true,
},
plugins: [tanstackStartCookies()],
})
src/lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient();
export const { useSession, signIn, signUp, signOut } = authClient;
src/routes/api/auth/$.ts
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),
},
},
})
UI
src/routes/__root.tsx
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
import appCss from "../styles.css?url";
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: "utf-8",
},
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
title: "TanStack Start Starter",
},
],
links: [
{
rel: "stylesheet",
href: appCss,
},
],
}),
notFoundComponent: () => (
<div className="flex flex-col items-center justify-center h-screen">
<h1>404</h1>
<p>页面不存在</p>
</div>
),
shellComponent: RootDocument,
});
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
);
}
src/routes/index.tsx
import { signOut, useSession } from "@/lib/auth-client";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
export const Route = createFileRoute("/")({ component: HomePage });
function HomePage() {
const { data: session, isPending } = useSession();
const navigate = useNavigate();
useEffect(() => {
if (!isPending && !session) {
navigate({ to: "/login" });
}
}, [session, isPending, navigate]);
if (isPending) return <div>加载中...</div>;
if (!session) return null;
return (
<div>
<p>邮箱: {session.user.email}</p>
<button onClick={() => signOut()} className="bg-blue-500 text-white rounded">
退出登录
</button>
</div>
);
}
src/routes/login.tsx
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { signIn } from "@/lib/auth-client";
export const Route = createFileRoute("/login")({
component: LoginPage,
});
function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const handleLogin = async () => {
const { error } = await signIn.email({ email, password });
if (error) {
alert(error.message);
} else {
// navigate({ to: "/" });
window.location.href = "/";
}
};
return (
<div className="flex flex-col gap-1">
<span>登录</span>
<input
type="email"
placeholder="邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button onClick={handleLogin} className="bg-blue-500 text-white rounded">
登录
</button>
<button
onClick={() => navigate({ to: "/register" })}
className="bg-gray-300 text-white rounded"
>
注册
</button>
</div>
);
}
src/routes/register.tsx
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { signIn } from "@/lib/auth-client";
export const Route = createFileRoute("/login")({
component: LoginPage,
});
function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const handleLogin = async () => {
const { error } = await signIn.email({ email, password });
if (error) {
alert(error.message);
} else {
// navigate({ to: "/" });
window.location.href = "/";
}
};
return (
<div className="flex flex-col gap-1">
<span>登录</span>
<input
type="email"
placeholder="邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button onClick={handleLogin} className="bg-blue-500 text-white rounded">
登录
</button>
<button
onClick={() => navigate({ to: "/register" })}
className="bg-gray-300 text-white rounded"
>
注册
</button>
</div>
);
}
src/routes/register.tsx
import { signIn, signUp } from "#/lib/auth-client";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createFileRoute("/register")({
component: RegisterPage,
});
function RegisterPage() {
const [name] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const handleRegister = async () => {
const { error } = await signUp.email({ name, email, password });
if (error) {
alert(error.message);
} else {
await signIn.email({ email, password });
navigate({ to: "/" });
}
};
return (
<div className="flex flex-col gap-1">
<span>注册</span>
<input
type="email"
placeholder="邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
onClick={handleRegister}
className="bg-blue-500 text-white rounded"
>
注册
</button>
</div>
);
}
src/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function getRouter() {
const router = createTanStackRouter({
routeTree,
scrollRestoration: true,
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
})
return router
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>
}
}
浙公网安备 33010602011771号