- Published on
构建现代Web应用的用户系统:Neon PostgreSQL与NextAuth的完美结合
- Authors
- Name
- Xiaofeng
构建现代Web应用的用户系统:Neon PostgreSQL与NextAuth的完美结合
在SynthesizerFlow项目的发展过程中,我们决定添加用户系统,以支持项目保存、权限控制和分享功能。这篇文章将详细介绍我们如何使用Vercel托管的Neon PostgreSQL和NextAuth(Auth.js)创建一个健壮的用户系统。
为什么选择Neon PostgreSQL和NextAuth?
在众多数据库和认证方案中,我们选择了这个组合有几个关键原因:
Neon PostgreSQL:
- 完全托管的无服务器PostgreSQL,无需管理基础设施
- 与Vercel平台无缝集成,减少了配置复杂度
- 自动扩展能力,可以应对从小型项目到企业级应用的需求
- 具有分支功能,方便开发和测试环境的分离
NextAuth/Auth.js:
- 专为NextJS应用设计的认证解决方案
- 支持OAuth提供商(GitHub、Google等)快速实现社交登录
- 与Prisma数据库工具无缝集成
- TypeScript友好的API设计
这个组合不仅减少了我们自行实现用户系统的工作量,还提供了高度的安全性和可扩展性。
技术栈概览
我们的用户系统建立在以下技术上:
- Next.js 15+:React框架,用于构建Web应用
- Prisma ORM:用于数据库交互和模型定义
- NextAuth/Auth.js:处理用户认证和会话管理
- Neon PostgreSQL:云数据库,用于存储用户数据和项目
- Zod:用于输入验证和运行时类型安全
实现步骤
1. 数据库模型设计
首先,我们通过Prisma定义了用户系统所需的数据库模型:
// 用户系统模型
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
role UserRole @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
sessions Session[]
projects Project[]
}
enum UserRole {
USER
ADMIN
}
// NextAuth所需的关联模型
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
// ...其他OAuth相关字段
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
// 项目模型
model Project {
id String @id @default(cuid())
name String
description String?
content Json // 存储项目数据的JSON
isPublic Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
这个模型设计允许我们:
- 存储用户基本信息和角色
- 关联OAuth认证数据
- 管理用户会话
- 存储用户创建的项目
2. NextAuth配置与集成
NextAuth的配置是用户系统的核心。以下是我们如何设置它与Prisma和Neon PostgreSQL协同工作:
import { PrismaAdapter } from '@auth/prisma-adapter'
import { NextAuthOptions } from 'next-auth'
import NextAuth from 'next-auth/next'
import { prisma } from '@/lib/prisma'
import GitHubProvider from 'next-auth/providers/github'
import GoogleProvider from 'next-auth/providers/google'
import { UserRole } from '@/generated/prisma'
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
// 配置OAuth提供商
GoogleProvider({
clientId: process.env.GOOGLE_ID || '',
clientSecret: process.env.GOOGLE_SECRET || '',
}),
GitHubProvider({
clientId: process.env.GITHUB_ID || '',
clientSecret: process.env.GITHUB_SECRET || '',
}),
],
callbacks: {
async session({ session, token }) {
// 扩展会话,添加用户ID和角色
if (session.user && token.sub) {
session.user.id = token.sub
try {
const dbUser = await prisma.user.findUnique({
where: { id: token.sub },
select: { role: true },
})
if (dbUser) {
session.user.role = dbUser.role
}
} catch (error) {
console.error('获取用户角色失败:', error)
session.user.role = UserRole.USER
}
}
return session
},
},
}
这个配置使用PrismaAdapter将认证数据存储到我们的Neon PostgreSQL数据库,并通过session回调扩展了用户会话数据,添加了用户ID和角色信息。
3. 用户界面组件
我们创建了几个关键的UI组件来支持用户交互:
登录页面
export default function LoginPage() {
const { data: session } = useSession()
const router = useRouter()
const callbackUrl = useSearchParams().get('callbackUrl') || '/'
// 已登录用户重定向
useEffect(() => {
if (session) router.push(callbackUrl)
}, [session, router, callbackUrl])
return (
<div className="flex min-h-screen items-center justify-center">
<Card className="mx-auto w-full max-w-md">
<CardHeader>
<CardTitle className="text-center text-2xl font-bold">登录</CardTitle>
<CardDescription className="text-center">
登录后即可保存和分享你的音频合成项目
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col space-y-3">
<Button onClick={() => signIn('github', { callbackUrl })}>
<GitHubIcon className="mr-2" />
使用GitHub登录
</Button>
<Button onClick={() => signIn('google', { callbackUrl })}>
<GoogleIcon className="mr-2" />
使用Google登录
</Button>
</div>
</CardContent>
</Card>
</div>
)
}
用户菜单组件
export function NavUser() {
const { data: session } = useSession()
if (!session) {
return (
<div onClick={() => signIn()} className="cursor-pointer">
<UserIcon className="mr-2 h-4 w-4" />
<span>登录</span>
</div>
)
}
const user = session.user
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="cursor-pointer">
<Avatar>
<AvatarImage src={user.image || ''} />
<AvatarFallback>{user.name?.substring(0, 2).toUpperCase() || 'U'}</AvatarFallback>
</Avatar>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link href="/dashboard">我的项目</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => signOut()}>退出登录</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
4. API路由和项目管理
为了支持项目的创建和分享,我们创建了几个关键API端点:
// 获取项目列表
export async function GET(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
const userId = session?.user?.id
const { searchParams } = new URL(req.url)
const isPublic = searchParams.get('public') === 'true'
// 构建查询条件
let whereClause: any = {}
if (isPublic) {
whereClause.isPublic = true
} else if (userId) {
whereClause.userId = userId
} else {
return NextResponse.json({ error: '未授权' }, { status: 401 })
}
const projects = await prisma.project.findMany({
where: whereClause,
orderBy: { updatedAt: 'desc' },
select: {
id: true,
name: true,
description: true,
isPublic: true,
createdAt: true,
updatedAt: true,
userId: true,
user: {
select: { name: true, image: true },
},
},
})
return NextResponse.json(projects)
} catch (error) {
return NextResponse.json({ error: '获取项目失败' }, { status: 500 })
}
}
// 创建新项目
export async function POST(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
const userId = session?.user?.id
if (!userId) {
return NextResponse.json({ error: '未授权' }, { status: 401 })
}
const body = await req.json()
const validatedData = projectCreateSchema.parse(body)
const project = await prisma.project.create({
data: {
name: validatedData.name,
description: validatedData.description,
content: validatedData.content,
isPublic: validatedData.isPublic,
userId,
},
})
return NextResponse.json(project)
} catch (error) {
console.error('创建项目失败:', error)
return NextResponse.json({ error: '创建项目失败' }, { status: 500 })
}
}
5. 部署到Vercel与CI/CD考虑
在部署到Vercel时,我们需要确保Prisma能正确工作。为此,我们修改了package.json
中的构建命令:
{
"scripts": {
"build": "prisma generate && next build"
}
}
并添加了vercel.json
配置:
{
"buildCommand": "prisma generate && next build",
"installCommand": "npm install",
"framework": "nextjs"
}
这确保了在构建过程中会生成Prisma客户端,然后再进行Next.js应用的构建,解决了CI/CD环境中的依赖顺序问题。
解决的挑战
1. 嵌套按钮导致的水合错误
在实现用户菜单时,我们遇到了React组件嵌套导致的水合错误:
In HTML, <button> cannot be a descendant of <button>.
This will cause a hydration error.
这是因为我们的组件结构中,Button
组件内嵌套了另一个Button
组件。我们通过将内部的Button
替换为样式化的div
元素解决了这个问题:
// 替换前
<Button>
<NavUser /> // 内部也包含Button组件
</Button>
// 替换后
<div className="[按钮样式]">
<NavUser />
</div>
2. Prisma客户端导入路径问题
由于Prisma生成的客户端位于自定义路径src/generated/prisma
,我们遇到了导入路径不一致的问题。特别是在导入UserRole
枚举时:
模块"@prisma/client"没有导出的成员"UserRole"
我们通过统一所有导入路径解决了这个问题:
// 错误的导入
import { UserRole } from '@prisma/client'
// 修复后的导入
import { UserRole } from '@/generated/prisma'
3. CI/CD环境中的Prisma生成问题
在Vercel部署过程中,我们遇到了构建时找不到生成的Prisma客户端的问题。通过修改构建命令和添加Vercel配置,我们确保了在构建过程中先生成Prisma客户端:
"build": "prisma generate && next build"
经验教训
通过构建这个用户系统,我们学到了几个重要的经验:
认证与数据库的紧密集成:使用NextAuth的PrismaAdapter简化了认证数据的存储和管理,但需要确保模型定义正确。
类型安全至关重要:在处理用户数据和认证时,TypeScript和Zod的组合提供了强大的类型检查和运行时验证。
组件嵌套需谨慎:React组件嵌套可能导致意外的DOM结构,特别是使用像
button
这样的HTML元素时。CI/CD环境中的生成资源:在构建过程中生成依赖文件(如Prisma客户端)需要特别注意构建步骤的顺序。
下一步计划
完成用户系统的基础架构后,我们计划进一步扩展功能:
- 实现项目协作功能,允许多用户共同编辑项目
- 添加项目版本控制,记录修改历史
- 构建公共项目库,展示和分享优秀作品
- 实现更细粒度的权限控制系统
结论
通过Neon PostgreSQL和NextAuth的结合,我们成功地为SynthesizerFlow添加了一个强大而灵活的用户系统。这个系统不仅满足了基本的身份验证需求,还提供了项目持久化存储和分享功能,为应用的进一步发展奠定了坚实的基础。
这种模式也可以轻松适应其他NextJS项目,特别是那些需要用户认证和数据存储的应用。Vercel托管的Neon PostgreSQL提供了一个简单而强大的数据库解决方案,而NextAuth则简化了认证逻辑的实现。
你有使用Neon PostgreSQL或NextAuth的经验吗?欢迎在评论中分享你的见解和建议!