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