feat(认证): 添加认证状态管理和路由保护
- 实现 AuthProvider 管理全局认证状态 - 实现 middleware 路由保护中间件 - 支持受保护路由自动重定向到登录页 - 支持已登录用户自动跳转首页
This commit is contained in:
parent
733c93a91c
commit
b0b912274f
84
src/middleware.ts
Normal file
84
src/middleware.ts
Normal file
@ -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).*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
238
src/providers/AuthProvider.tsx
Normal file
238
src/providers/AuthProvider.tsx
Normal file
@ -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<void>;
|
||||||
|
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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建上下文
|
||||||
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
// Provider Props
|
||||||
|
interface AuthProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公开路由(不需要获取用户信息)
|
||||||
|
const publicRoutes = ['/login', '/register', '/reset-password'];
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: AuthProviderProps) {
|
||||||
|
const [user, setUser] = useState<AuthUser | null>(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 (
|
||||||
|
<AuthContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义 Hook
|
||||||
|
export function useAuth() {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
1
src/providers/index.ts
Normal file
1
src/providers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { AuthProvider, useAuth, type AuthUser } from './AuthProvider';
|
||||||
Loading…
Reference in New Issue
Block a user