1. NextAuth.js 集成
1.1 安装依赖
npm install next-auth bcryptjs
npm install @types/bcryptjs --save-dev
TypeScript
npm install next-auth bcryptjs
npm install @types/bcryptjs --save-dev
1.2 配置NextAuth
TypeScript
// src/app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { compare } from 'bcryptjs'
import { UserService } from '@/services/database/user.service'
const handler = NextAuth({
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: "邮箱", type: "email" },
password: { label: "密码", type: "password" }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error('请输入邮箱和密码')
}
const user = await UserService.findByEmail(credentials.email)
if (!user) {
throw new Error('用户不存在')
}
const isValid = await compare(credentials.password, user.password)
if (!isValid) {
throw new Error('密码错误')
}
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
}
}
})
],
session: {
strategy: 'jwt',
},
pages: {
signIn: '/auth/login',
signOut: '/auth/logout',
error: '/auth/error',
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role
token.id = user.id
}
return token
},
async session({ session, token }) {
if (session.user) {
session.user.role = token.role as string
session.user.id = token.id as string
}
return session
}
}
})
export { handler as GET, handler as POST }
2. 认证页面实现
2.1 登录页面
TypeScript
// src/app/auth/login/page.tsx
'use client'
import { useState } from 'react'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
export default function LoginPage() {
const router = useRouter()
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
setError('')
const formData = new FormData(e.currentTarget)
const email = formData.get('email') as string
const password = formData.get('password') as string
try {
const result = await signIn('credentials', {
redirect: false,
email,
password,
})
if (result?.error) {
setError(result.error)
} else {
router.push('/')
router.refresh()
}
} catch (error) {
setError('登录失败,请重试')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
登录账户
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="rounded-md bg-red-50 p-4">
<div className="text-sm text-red-700">{error}</div>
</div>
)}
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">
邮箱
</label>
<input
id="email"
name="email"
type="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="邮箱地址"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
密码
</label>
<input
id="password"
name="password"
type="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="密码"
/>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
{loading ? '登录中...' : '登录'}
</button>
</div>
<div className="text-sm text-center">
<Link
href="/auth/register"
className="font-medium text-primary-600 hover:text-primary-500"
>
还没有账户?立即注册
</Link>
</div>
</form>
</div>
</div>
)
}
2.2 注册页面
TypeScript
// src/app/auth/register/page.tsx
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
export default function RegisterPage() {
const router = useRouter()
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
setError('')
const formData = new FormData(e.currentTarget)
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
name: formData.get('name') as string,
}
try {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) {
const error = await res.json()
throw new Error(error.message || '注册失败')
}
router.push('/auth/login')
} catch (error) {
setError(error instanceof Error ? error.message : '注册失败,请重试')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
创建新账户
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="rounded-md bg-red-50 p-4">
<div className="text-sm text-red-700">{error}</div>
</div>
)}
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="name" className="sr-only">
姓名
</label>
<input
id="name"
name="name"
type="text"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="姓名"
/>
</div>
<div>
<label htmlFor="email" className="sr-only">
邮箱
</label>
<input
id="email"
name="email"
type="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="邮箱地址"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
密码
</label>
<input
id="password"
name="password"
type="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="密码"
/>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
{loading ? '注册中...' : '注册'}
</button>
</div>
<div className="text-sm text-center">
<Link
href="/auth/login"
className="font-medium text-primary-600 hover:text-primary-500"
>
已有账户?立即登录
</Link>
</div>
</form>
</div>
</div>
)
}
3. 权限控制实现
3.1 创建权限中间件
TypeScript
// src/middleware.ts
import { withAuth } from 'next-auth/middleware'
import { NextResponse } from 'next/server'
export default withAuth(
function middleware(req) {
const token = req.nextauth.token
const path = req.nextUrl.pathname
// 管理员路由保护
if (path.startsWith('/admin') && token?.role !== 'ADMIN') {
return NextResponse.redirect(new URL('/auth/login', req.url))
}
// 咨询师路由保护
if (path.startsWith('/counselor') && token?.role !== 'COUNSELOR') {
return NextResponse.redirect(new URL('/auth/login', req.url))
}
return NextResponse.next()
},
{
callbacks: {
authorized: ({ token }) => !!token
},
}
)
export const config = {
matcher: ['/admin/:path*', '/counselor/:path*', '/profile/:path*']
}
3.2 创建权限Hook
TypeScript
// src/hooks/useAuth.ts
'use client'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
export function useAuth(requiredRole?: string) {
const { data: session, status } = useSession()
const router = useRouter()
useEffect(() => {
if (status === 'loading') return
if (!session) {
router.push('/auth/login')
return
}
if (requiredRole && session.user.role !== requiredRole) {
router.push('/')
}
}, [session, status, requiredRole, router])
return { session, status }
}
3.3 创建受保护的组件包装器
TypeScript
// src/components/auth/ProtectedRoute.tsx
'use client'
import { useAuth } from '@/hooks/useAuth'
import { LoadingSpinner } from '@/components/ui/LoadingSpinner'
interface ProtectedRouteProps {
children: React.ReactNode
requiredRole?: string
}
export function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
const { status } = useAuth(requiredRole)
if (status === 'loading') {
return (
<div className="min-h-screen flex items-center justify-center">
<LoadingSpinner />
</div>
)
}
return <>{children}</>
}
4. 用户状态管理
4.1 创建用户状态Store
TypeScript
// src/store/useUserStore.ts
import { create } from 'zustand'
import { UserProfile } from '@/types'
interface UserState {
profile: UserProfile | null
setProfile: (profile: UserProfile | null) => void
updateProfile: (data: Partial<UserProfile>) => void
}
export const useUserStore = create<UserState>((set) => ({
profile: null,
setProfile: (profile) => set({ profile }),
updateProfile: (data) =>
set((state) => ({
profile: state.profile ? { ...state.profile, ...data } : null,
})),
}))
4.2 创建用户Provider
TypeScript
// src/components/providers/UserProvider.tsx
'use client'
import { useSession } from 'next-auth/react'
import { useEffect } from 'react'
import { useUserStore } from '@/store/useUserStore'
export function UserProvider({ children }: { children: React.ReactNode }) {
const { data: session } = useSession()
const setProfile = useUserStore((state) => state.setProfile)
useEffect(() => {
if (session?.user) {
// 获取用户详细信息
fetch(`/api/users/profile?userId=${session.user.id}`)
.then((res) => res.json())
.then((data) => {
setProfile(data)
})
.catch(console.error)
} else {
setProfile(null)
}
}, [session, setProfile])
return <>{children}</>
}
5.集成到应用
5.1更新根布局
TypeScript
// src/app/layout.tsx
import { Inter } from 'next/font/google'
import { SessionProvider } from 'next-auth/react'
import { UserProvider } from '@/components/providers/UserProvider'
import PageLayout from '@/components/layout/PageLayout'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body className={inter.className}>
<SessionProvider>
<UserProvider>
<PageLayout>{children}</PageLayout>
</UserProvider>
</SessionProvider>
</body>
</html>
)
}
5.2更新导航栏
TypeScript
// src/components/layout/Navbar.tsx
'use client'
import { useSession, signOut } from 'next-auth/react'
import Link from 'next/link'
import { useUserStore } from '@/store/useUserStore'
export default function Navbar() {
const { data: session } = useSession()
const profile = useUserStore((state) => state.profile)
return (
<nav className="bg-white shadow">
{/* ... 其他导航代码 ... */}
<div className="flex items-center">
{session ? (
<div className="relative ml-3">
<div className="flex items-center">
<span className="text-gray-700 mr-4">
{profile?.name || session.user.email}
</span>
<button
onClick={() => signOut()}
className="text-gray-600 hover:text-gray-900"
>
退出
</button>
</div>
</div>
) : (
<Link
href="/auth/login"
className="text-gray-600 hover:text-gray-900"
>
登录
</Link>
)}
</div>
</nav>
)
}
6. 下一步计划
- 集成OpenAI API
- 实现心理测评功能
- 开发情绪检测系统
- 构建在线咨询功能