76 lines
2.0 KiB
TypeScript
76 lines
2.0 KiB
TypeScript
'use client'
|
|
|
|
import { useAuth } from '@/contexts/AuthContext'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useEffect } from 'react'
|
|
|
|
interface ProtectedRouteProps {
|
|
children: React.ReactNode
|
|
redirectTo?: string
|
|
fallback?: React.ReactNode
|
|
}
|
|
|
|
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|
children,
|
|
redirectTo = '/login',
|
|
fallback
|
|
}) => {
|
|
const { isAuthenticated, isLoading, checkAuth } = useAuth()
|
|
const router = useRouter()
|
|
|
|
useEffect(() => {
|
|
const verifyAuth = async () => {
|
|
if (!isLoading) {
|
|
if (!isAuthenticated) {
|
|
router.push(redirectTo)
|
|
return
|
|
}
|
|
|
|
// 額外檢查 token 有效性
|
|
const isValid = await checkAuth()
|
|
if (!isValid) {
|
|
router.push(redirectTo)
|
|
}
|
|
}
|
|
}
|
|
|
|
verifyAuth()
|
|
}, [isAuthenticated, isLoading, router, redirectTo, checkAuth])
|
|
|
|
// Loading 狀態
|
|
if (isLoading) {
|
|
return (
|
|
fallback || (
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
|
|
<div className="bg-white rounded-2xl shadow-xl p-8">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
<p className="text-gray-600">驗證中...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
)
|
|
}
|
|
|
|
// 未認證
|
|
if (!isAuthenticated) {
|
|
return null // 這時會觸發重定向
|
|
}
|
|
|
|
// 已認證,渲染子組件
|
|
return <>{children}</>
|
|
}
|
|
|
|
// 便利的 HOC 版本
|
|
export function withAuth<T extends object>(Component: React.ComponentType<T>, redirectTo = '/login') {
|
|
const AuthenticatedComponent = (props: T) => (
|
|
<ProtectedRoute redirectTo={redirectTo}>
|
|
<Component {...props} />
|
|
</ProtectedRoute>
|
|
)
|
|
|
|
AuthenticatedComponent.displayName = `withAuth(${Component.displayName || Component.name})`
|
|
|
|
return AuthenticatedComponent
|
|
} |