如何在 Better Auth 和 Astro 中使用 Prisma ORM
介绍
Better Auth 是一个现代化的开源 Web 应用程序身份验证解决方案。它采用 TypeScript 构建,提供简单且可扩展的身份验证体验,并支持包括 Prisma 在内的多种数据库适配器。
在本指南中,您将 Better Auth 集成到一个全新的 Astro 应用程序中,并将用户持久化到 Prisma Postgres 数据库中。您可以在 GitHub 上找到本指南的完整示例。
先决条件
- Node.js 20+
- 熟悉 Astro 和 Prisma 的基本知识
1. 设置项目
创建一个新的 Astro 项目
npm create astro@latest betterauth-astro-prisma
- 您希望如何开始您的新项目?
使用最小(空)模板 - 安装依赖项?(推荐)
Yes - 初始化一个新的 Git 仓库?(可选)
是
导航到项目目录
cd betterauth-astro-prisma
这些选择将创建一个带有 TypeScript 的最小 Astro 项目,以确保类型安全。
2. 设置 Prisma
接下来,您将向项目中添加 Prisma 以管理您的数据库。
2.1. 安装 Prisma 和依赖项
安装必要的 Prisma 包
npm install prisma tsx @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg dotenv pg
如果你使用的是其他数据库提供程序(MySQL、SQL Server、SQLite),请安装相应的驱动程序适配器包,而不是 @prisma/adapter-pg。有关更多信息,请参阅 数据库驱动程序。
安装后,在项目中初始化 Prisma
npx prisma init --db --output ../prisma/generated
在设置 Prisma Postgres 数据库时,您需要回答几个问题。选择离您位置最近的区域,并为您的数据库选择一个易于记忆的名称,例如“我的 Better Auth Astro 项目”。
这将创建
- 一个包含 `schema.prisma` 文件的 `prisma` 目录
- 一个 Prisma Postgres 数据库
- 项目根目录下包含 `DATABASE_URL` 的 `.env` 文件
- 一个用于配置 Prisma 的
prisma.config.ts文件 - 用于生成 Prisma 客户端的 `output` 目录,路径为 `prisma/generated`
2.2. 配置 Prisma 以加载环境变量
为了访问 `.env` 文件中的变量,请更新 `prisma.config.ts` 以导入 `dotenv`
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
engine: "classic",
datasource: {
url: env("DATABASE_URL"),
},
});
2.3. 生成 Prisma 客户端
运行以下命令生成 Prisma 客户端
npx prisma generate
2.4. 设置全局 Prisma 客户端
在 `src` 目录中,创建一个 `lib` 文件夹,并在其中创建一个 `prisma.ts` 文件。该文件将用于创建和导出您的 Prisma 客户端实例。
mkdir -p src/lib
touch src/lib/prisma.ts
像这样设置 Prisma 客户端
import { PrismaClient } from "../../prisma/generated/client";
import { PrismaPg } from "@prisma/adapter-pg";
const adapter = new PrismaPg({
connectionString: import.meta.env.DATABASE_URL,
});
const prisma = new PrismaClient({
adapter,
});
export default prisma;
我们建议使用连接池(如 Prisma Accelerate)来有效地管理数据库连接。
如果您选择不使用一个,请**避免**在长生命周期环境中全局实例化 PrismaClient。相反,请为每个请求创建和处置客户端,以防止耗尽数据库连接。
3. 设置 Better Auth
现在是时候集成 Better Auth 进行身份验证了。
3.1. 安装并配置 Better Auth
首先,安装 Better Auth 核心包
npm install better-auth
接下来,生成 Better Auth 将用于签署身份验证令牌的安全密钥。这确保了您的令牌不会被篡改。
npx @better-auth/cli@latest secret
复制生成的密钥,并将其与您的应用程序 URL 一起添加到您的 `.env` 文件中。
# Better Auth
BETTER_AUTH_SECRET=your-generated-secret
BETTER_AUTH_URL=https://:4321
# Prisma
DATABASE_URL="your-database-url"
Astro 的默认开发服务器在端口 `4321` 上运行。如果您的应用程序在不同的端口上运行,请相应地更新 `BETTER_AUTH_URL`。
现在,为 Better Auth 创建一个配置文件。在 `src/lib` 目录中,创建一个 `auth.ts` 文件。
touch src/lib/auth.ts
在此文件中,您将配置 Better Auth 使用 Prisma 适配器,该适配器允许它将用户和会话数据持久化到您的数据库中。您还将启用电子邮件和密码身份验证。
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import prisma from "./prisma";
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "postgresql",
}),
emailAndPassword: {
enabled: true,
},
});
Better Auth 还支持其他登录方法,例如社交登录(Google、GitHub 等),您可以在其文档中探索。
3.2. 将 Better Auth 模型添加到您的模式
Better Auth 提供了一个 CLI 命令,可以自动将必要的身份验证模型(`User`、`Session`、`Account` 和 `Verification`)添加到您的 `schema.prisma` 文件中。
运行以下命令
npx @better-auth/cli generate
它将询问是否确认覆盖您现有的 Prisma schema。选择 `y`。
这将添加以下模型
model User {
id String @id
name String
email String
emailVerified Boolean
image String?
createdAt DateTime
updatedAt DateTime
sessions Session[]
accounts Account[]
@@unique([email])
@@map("user")
}
model Session {
id String @id
expiresAt DateTime
token String
createdAt DateTime
updatedAt DateTime
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([token])
@@map("session")
}
model Account {
id String @id
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime
updatedAt DateTime
@@map("account")
}
model Verification {
id String @id
identifier String
value String
expiresAt DateTime
createdAt DateTime?
updatedAt DateTime?
@@map("verification")
}
3.3. 迁移数据库
随着 schema 中新模型的添加,您需要更新数据库。运行迁移以创建相应的表
npx prisma migrate dev --name add-auth-models
npx prisma generate
4. 设置 API 路由
Better Auth 需要一个 API 端点来处理身份验证请求,如登录、注册和注销。您将在 Astro 中创建一个全功能 API 路由来处理所有发送到 `/api/auth/[...all]` 的请求。
在 `src/pages` 目录中,创建一个 `api/auth` 文件夹结构,并在其中创建一个 `[...all].ts` 文件。
mkdir -p src/pages/api/auth
touch 'src/pages/api/auth/[...all].ts'
将以下代码添加到新创建的 `[...all].ts` 文件中。此代码使用 Better Auth 处理程序来处理身份验证请求。
import { auth } from "../../../lib/auth";
import type { APIRoute } from "astro";
export const prerender = false; // Not needed in 'server' mode
export const ALL: APIRoute = async (ctx) => {
return auth.handler(ctx.request);
};
接下来,您需要一个客户端工具来与 Astro 页面中的这些端点进行交互。在 `src/lib` 目录中,创建一个 `auth-client.ts` 文件。
touch src/lib/auth-client.ts
添加以下代码,它将创建您将在 UI 中使用的客户端函数。
import { createAuthClient } from "better-auth/client";
export const authClient = createAuthClient();
export const { signIn, signUp, signOut, useSession } = authClient;
5. 配置 TypeScript 定义
在 `src` 目录中,创建一个 `env.d.ts` 文件,为环境变量和 Astro local 提供 TypeScript 定义。
touch src/env.d.ts
添加以下类型定义
/// <reference path="../.astro/types.d.ts" />
declare namespace App {
interface Locals {
user: import("better-auth").User | null;
session: import("better-auth").Session | null;
}
}
interface ImportMetaEnv {
readonly DATABASE_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
6. 设置认证中间件
在 `src` 目录中,创建一个 `middleware.ts` 文件,用于检查每个请求的身份验证状态。这将使用户和会话数据可用于所有页面。
touch src/middleware.ts
添加以下代码
import { auth } from "./lib/auth";
import { defineMiddleware } from "astro:middleware";
export const onRequest = defineMiddleware(async (context, next) => {
context.locals.user = null;
context.locals.session = null;
const isAuthed = await auth.api.getSession({
headers: context.request.headers,
});
if (isAuthed) {
context.locals.user = isAuthed.user;
context.locals.session = isAuthed.session;
}
return next();
});
7. 设置您的页面
现在,让我们构建身份验证的用户界面。在 `src/pages` 目录中,创建以下文件夹结构
sign-up/index.astrosign-in/index.astrodashboard/index.astro
mkdir -p src/pages/{sign-up,sign-in,dashboard}
touch src/pages/{sign-up,sign-in,dashboard}/index.astro
7.1. 注册页面
此页面允许新用户创建账户。首先在 `src/pages/sign-up/index.astro` 中创建基本的 HTML 结构。
---
export const prerender = false;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Sign Up</title>
</head>
<body>
<main>
<h1>Sign Up</h1>
</main>
</body>
</html>
添加一个包含姓名、电子邮件和密码输入字段的表单。此表单将收集用户的注册信息。
---
export const prerender = false;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Sign Up</title>
</head>
<body>
<main>
<h1>Sign Up</h1>
<form id="signup-form">
<input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required />
<input
required
type="password"
name="password"
placeholder="Password"
/>
<button type="submit">Sign up</button>
</form>
<p>Already have an account? <a href="/sign-in">Sign in here</a>.</p>
</main>
</body>
</html>
现在添加一个脚本来处理表单提交。导入 `authClient` 并向表单添加一个事件监听器,该监听器将阻止默认提交行为,提取表单数据,并调用 Better Auth 的注册方法。
---
export const prerender = false;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Sign Up</title>
</head>
<body>
<main>
<h1>Sign Up</h1>
<form id="signup-form">
<input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required />
<input
required
type="password"
name="password"
placeholder="Password"
/>
<button type="submit">Sign up</button>
</form>
<p>Already have an account? <a href="/sign-in">Sign in here</a>.</p>
</main>
<script>
import { authClient } from "../../lib/auth-client";
document
.getElementById("signup-form")
?.addEventListener("submit", async (event) => {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const tmp = await authClient.signUp.email({
name,
email,
password,
});
console.log(tmp);
if (Boolean(tmp.error) === false) window.location.href = "/dashboard";
});
</script>
</body>
</html>
最后,添加一个服务器端检查,以将已通过身份验证的用户从该页面重定向。如果用户已经登录,他们应该被重定向到仪表板。
---
export const prerender = false;
if (Astro.locals.user?.id) return Astro.redirect("/dashboard");
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Sign Up</title>
</head>
<body>
<main>
<h1>Sign Up</h1>
<form id="signup-form">
<input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required />
<input
required
type="password"
name="password"
placeholder="Password"
/>
<button type="submit">Sign up</button>
</form>
<p>Already have an account? <a href="/sign-in">Sign in here</a>.</p>
</main>
<script>
import { authClient } from "../../lib/auth-client";
document
.getElementById("signup-form")
?.addEventListener("submit", async (event) => {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const tmp = await authClient.signUp.email({
name,
email,
password,
});
console.log(tmp);
if (Boolean(tmp.error) === false) window.location.href = "/dashboard";
});
</script>
</body>
</html>
7.2. 登录页面
此页面允许现有用户进行身份验证。首先在 `src/pages/sign-in/index.astro` 中创建基本的 HTML 结构。
---
export const prerender = false;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Sign In</title>
</head>
<body>
<main>
<h1>Sign In</h1>
</main>
</body>
</html>
添加一个包含电子邮件和密码输入字段的表单。此表单将收集用户的凭据。
---
export const prerender = false;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Sign In</title>
</head>
<body>
<main>
<h1>Sign In</h1>
<form id="signin-form">
<input type="email" name="email" placeholder="Email" required />
<input
required
type="password"
name="password"
placeholder="Password"
/>
<button type="submit">Sign In</button>
</form>
<p>Don't have an account? <a href="/sign-up">Sign up here</a>.</p>
</main>
</body>
</html>
现在添加一个脚本来处理表单提交。导入 `authClient` 并添加一个事件监听器,该监听器将阻止默认提交,提取表单数据,并调用 Better Auth 登录方法。
---
export const prerender = false;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Sign In</title>
</head>
<body>
<main>
<h1>Sign In</h1>
<form id="signin-form">
<input type="email" name="email" placeholder="Email" required />
<input
required
type="password"
name="password"
placeholder="Password"
/>
<button type="submit">Sign In</button>
</form>
<p>Don't have an account? <a href="/sign-up">Sign up here</a>.</p>
</main>
<script>
import { authClient } from "../../lib/auth-client";
document
.getElementById("signin-form")
?.addEventListener("submit", async (event) => {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const tmp = await authClient.signIn.email({
email,
password,
});
if (Boolean(tmp.error) === false) window.location.href = "/dashboard";
});
</script>
</body>
</html>
最后,添加一个服务器端检查,以将已通过身份验证的用户从该页面重定向。如果用户已经登录,他们应该被重定向到仪表板。
---
export const prerender = false;
if (Astro.locals.user?.id) return Astro.redirect("/dashboard");
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Sign In</title>
</head>
<body>
<main>
<h1>Sign In</h1>
<form id="signin-form">
<input type="email" name="email" placeholder="Email" required />
<input
required
type="password"
name="password"
placeholder="Password"
/>
<button type="submit">Sign In</button>
</form>
<p>Don't have an account? <a href="/sign-up">Sign up here</a>.</p>
</main>
<script>
import { authClient } from "../../lib/auth-client";
document
.getElementById("signin-form")
?.addEventListener("submit", async (event) => {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const tmp = await authClient.signIn.email({
email,
password,
});
if (Boolean(tmp.error) === false) window.location.href = "/dashboard";
});
</script>
</body>
</html>
7.3. 仪表盘页面
这是用于已通过身份验证的用户的受保护页面。首先在 `src/pages/dashboard/index.astro` 中创建基本的 HTML 结构。
---
export const prerender = false;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Dashboard</title>
</head>
<body>
<main>
<h1>Dashboard</h1>
</main>
</body>
</html>
添加一个服务器端检查以保护此路由。如果用户未通过身份验证,请将其重定向到登录页面。
---
export const prerender = false;
if (!Astro.locals.user?.id) return Astro.redirect("/sign-in");
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Dashboard</title>
</head>
<body>
<main>
<h1>Dashboard</h1>
</main>
</body>
</html>
现在显示已通过身份验证的用户信息。`Astro.locals.user` 对象包含由中间件设置的用户数据。
---
export const prerender = false;
if (!Astro.locals.user?.id) return Astro.redirect("/sign-in");
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Dashboard</title>
</head>
<body>
<main>
<h1>Dashboard</h1>
<pre>{JSON.stringify(Astro.locals.user, null, 2)}</pre>
</main>
</body>
</html>
最后,添加一个退出按钮。导入 `authClient` 并添加一个调用退出方法的按钮,允许用户注销并被重定向到登录页面。
---
export const prerender = false;
if (!Astro.locals.user?.id) return Astro.redirect("/sign-in");
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Dashboard</title>
</head>
<body>
<main>
<h1>Dashboard</h1>
<pre>{JSON.stringify(Astro.locals.user, null, 2)}</pre>
<button id="signOutButton">Sign Out</button>
</main>
<script>
import { authClient } from "../../lib/auth-client";
document
.getElementById("signOutButton")
?.addEventListener("click", async () => {
await authClient.signOut();
window.location.href = "/sign-in";
});
</script>
</body>
</html>
7.4. 主页
最后,更新主页以提供简单的导航。将 `src/pages/index.astro` 的内容替换为以下内容
---
export const prerender = false;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Better Auth + Astro + Prisma</title>
</head>
<body>
<main>
<h1>Better Auth + Astro + Prisma</h1>
{
Astro.locals.user ? (
<div>
<p>Welcome back, {Astro.locals.user.name}!</p>
<a href="/dashboard">Go to Dashboard</a>
</div>
) : (
<div>
<a href="/sign-up">Sign Up</a>
<a href="/sign-in">Sign In</a>
</div>
)
}
</main>
</body>
</html>
8. 测试一下
你的应用程序现在已完全配置。
- 启动开发服务器以进行测试
npm run dev
-
在浏览器中导航到 `https://:4321`。您应该会看到带有“注册”和“登录”链接的主页。
-
点击注册,创建一个新账户,您应该会被重定向到仪表板。然后您可以注销并重新登录。
-
要直接在数据库中查看用户数据,您可以使用 Prisma Studio。
npx prisma studio
- 这将在浏览器中打开一个新标签页,你可以在其中查看
User、Session和Account表及其内容。
恭喜!您现在拥有一个使用 Better Auth、Prisma 和 Astro 构建的完全功能的身份验证系统。
后续步骤
- 添加对社交登录或魔术链接的支持
- 实现密码重置和电子邮件验证
- 添加用户个人资料和帐户管理页面
- 部署到 Vercel 或 Netlify 并保护您的环境变量
- 使用自定义应用程序模型扩展您的 Prisma 模式
进一步阅读
与 Prisma 保持联系
通过以下方式与我们保持联系,继续你的 Prisma 之旅: 我们的活跃社区。保持信息灵通,参与其中,并与其他开发人员协作。
- 在 X 上关注我们 获取公告、直播活动和实用技巧。
- 加入我们的 Discord 提问、与社区交流,并通过对话获得积极支持。
- 在 YouTube 上订阅 获取教程、演示和直播。
- 在 GitHub 上参与 加星收藏存储库、报告问题或为问题做出贡献。