From b0b912274fae7ca8f1b1bad5274ac9d2ad463403 Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Fri, 19 Dec 2025 22:36:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=AE=A4=E8=AF=81):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E5=92=8C?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E4=BF=9D=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 AuthProvider 管理全局认证状态 - 实现 middleware 路由保护中间件 - 支持受保护路由自动重定向到登录页 - 支持已登录用户自动跳转首页 --- src/middleware.ts | 84 ++++++++++++ src/providers/AuthProvider.tsx | 238 +++++++++++++++++++++++++++++++++ src/providers/index.ts | 1 + 3 files changed, 323 insertions(+) create mode 100644 src/middleware.ts create mode 100644 src/providers/AuthProvider.tsx create mode 100644 src/providers/index.ts diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..332a5eb --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,84 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { jwtVerify } from 'jose'; + +// JWT 密钥 +const JWT_SECRET = new TextEncoder().encode( + process.env.JWT_SECRET || 'lioncode-jwt-secret-key-2024' +); + +// Cookie 名称 +const AUTH_COOKIE_NAME = 'lioncode_auth_token'; + +// 需要登录才能访问的路由 +const protectedRoutes = ['/', '/chat', '/settings']; + +// 公开路由(已登录用户访问会重定向到首页) +const publicRoutes = ['/login', '/register', '/reset-password']; + +export async function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + // 忽略 API 路由和静态资源 + if ( + pathname.startsWith('/api/') || + pathname.startsWith('/_next/') || + pathname.startsWith('/favicon') || + pathname.includes('.') + ) { + return NextResponse.next(); + } + + // 获取认证 Cookie + const token = request.cookies.get(AUTH_COOKIE_NAME)?.value; + + // 验证 Token + let isAuthenticated = false; + if (token) { + try { + await jwtVerify(token, JWT_SECRET); + isAuthenticated = true; + } catch { + // Token 无效或过期 + isAuthenticated = false; + } + } + + // 检查是否是受保护的路由 + const isProtectedRoute = protectedRoutes.some( + (route) => pathname === route || pathname.startsWith(`${route}/`) + ); + + // 检查是否是公开路由 + const isPublicRoute = publicRoutes.some( + (route) => pathname === route || pathname.startsWith(`${route}/`) + ); + + // 未登录用户访问受保护路由 -> 重定向到登录页 + if (isProtectedRoute && !isAuthenticated) { + const loginUrl = new URL('/login', request.url); + loginUrl.searchParams.set('redirect', pathname); + return NextResponse.redirect(loginUrl); + } + + // 已登录用户访问公开路由 -> 重定向到首页 + if (isPublicRoute && isAuthenticated) { + return NextResponse.redirect(new URL('/', request.url)); + } + + return NextResponse.next(); +} + +// 配置匹配的路由 +export const config = { + matcher: [ + /* + * 匹配所有路径除了: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + */ + '/((?!api|_next/static|_next/image|favicon.ico).*)', + ], +}; diff --git a/src/providers/AuthProvider.tsx b/src/providers/AuthProvider.tsx new file mode 100644 index 0000000..e258862 --- /dev/null +++ b/src/providers/AuthProvider.tsx @@ -0,0 +1,238 @@ +'use client'; + +import { + createContext, + useContext, + useState, + useEffect, + useCallback, + type ReactNode, +} from 'react'; +import { useRouter, usePathname } from 'next/navigation'; + +// 用户类型 +export interface AuthUser { + id: string; + email: string; + nickname: string; + plan: string; + avatar?: string | null; + status?: string; + emailVerified?: boolean; + lastLoginAt?: string | null; + createdAt?: string; +} + +// 认证上下文类型 +interface AuthContextType { + user: AuthUser | null; + loading: boolean; + login: (email: string, password?: string, code?: string, loginType?: 'password' | 'code') => Promise<{ success: boolean; error?: string }>; + register: (email: string, password: string, confirmPassword: string, nickname: string, code: string) => Promise<{ success: boolean; error?: string }>; + logout: () => Promise; + sendCode: (email: string, type: 'register' | 'login' | 'reset') => Promise<{ success: boolean; error?: string }>; + resetPassword: (email: string, code: string, newPassword: string, confirmPassword: string) => Promise<{ success: boolean; error?: string }>; + refreshUser: () => Promise; +} + +// 创建上下文 +const AuthContext = createContext(undefined); + +// Provider Props +interface AuthProviderProps { + children: ReactNode; +} + +// 公开路由(不需要获取用户信息) +const publicRoutes = ['/login', '/register', '/reset-password']; + +export function AuthProvider({ children }: AuthProviderProps) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const router = useRouter(); + const pathname = usePathname(); + + // 获取当前用户信息 + const fetchUser = useCallback(async () => { + // 公开路由不需要获取用户信息 + if (publicRoutes.some(route => pathname?.startsWith(route))) { + setLoading(false); + return; + } + + try { + const response = await fetch('/api/auth/me'); + const data = await response.json(); + + if (data.success && data.user) { + setUser(data.user); + } else { + setUser(null); + } + } catch (error) { + console.error('获取用户信息失败:', error); + setUser(null); + } finally { + setLoading(false); + } + }, [pathname]); + + // 初始化时获取用户信息 + useEffect(() => { + fetchUser(); + }, [fetchUser]); + + // 刷新用户信息 + const refreshUser = useCallback(async () => { + await fetchUser(); + }, [fetchUser]); + + // 登录 + const login = useCallback(async ( + email: string, + password?: string, + code?: string, + loginType: 'password' | 'code' = 'password' + ) => { + try { + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password, code, loginType }), + }); + + const data = await response.json(); + + if (data.success) { + setUser(data.user); + router.push('/'); + return { success: true }; + } + + return { success: false, error: data.error }; + } catch (error) { + console.error('登录失败:', error); + return { success: false, error: '登录失败,请稍后重试' }; + } + }, [router]); + + // 注册 + const register = useCallback(async ( + email: string, + password: string, + confirmPassword: string, + nickname: string, + code: string + ) => { + try { + const response = await fetch('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password, confirmPassword, nickname, code }), + }); + + const data = await response.json(); + + if (data.success) { + setUser(data.user); + router.push('/'); + return { success: true }; + } + + return { success: false, error: data.error }; + } catch (error) { + console.error('注册失败:', error); + return { success: false, error: '注册失败,请稍后重试' }; + } + }, [router]); + + // 登出 + const logout = useCallback(async () => { + try { + await fetch('/api/auth/logout', { method: 'POST' }); + setUser(null); + router.push('/login'); + } catch (error) { + console.error('登出失败:', error); + } + }, [router]); + + // 发送验证码 + const sendCode = useCallback(async ( + email: string, + type: 'register' | 'login' | 'reset' + ) => { + try { + const response = await fetch('/api/auth/send-code', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, type }), + }); + + const data = await response.json(); + + if (data.success) { + return { success: true }; + } + + return { success: false, error: data.error }; + } catch (error) { + console.error('发送验证码失败:', error); + return { success: false, error: '发送验证码失败,请稍后重试' }; + } + }, []); + + // 重置密码 + const resetPassword = useCallback(async ( + email: string, + code: string, + newPassword: string, + confirmPassword: string + ) => { + try { + const response = await fetch('/api/auth/reset-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, code, newPassword, confirmPassword }), + }); + + const data = await response.json(); + + if (data.success) { + router.push('/login'); + return { success: true }; + } + + return { success: false, error: data.error }; + } catch (error) { + console.error('重置密码失败:', error); + return { success: false, error: '重置密码失败,请稍后重试' }; + } + }, [router]); + + const value: AuthContextType = { + user, + loading, + login, + register, + logout, + sendCode, + resetPassword, + refreshUser, + }; + + return ( + + {children} + + ); +} + +// 自定义 Hook +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} diff --git a/src/providers/index.ts b/src/providers/index.ts new file mode 100644 index 0000000..662ea48 --- /dev/null +++ b/src/providers/index.ts @@ -0,0 +1 @@ +export { AuthProvider, useAuth, type AuthUser } from './AuthProvider';