Add complete user authentication with NextAuth.js supporting: - Email/password credentials authentication - OAuth providers (GitHub, Google) with optional configuration - JWT-based session management with 30-day expiry - Role-based access control (USER, GROWER, FARM_MANAGER, ADMIN) - Permission system with granular access control - Secure password hashing with bcrypt (12 rounds) - Rate limiting on auth endpoints - Password reset flow with secure tokens - Email verification system Files added: - lib/auth/: Core auth library (types, permissions, context, hooks, middleware) - pages/api/auth/: Auth API routes (NextAuth, register, forgot-password, verify-email) - pages/auth/: Auth pages (signin, signup, forgot-password, reset-password, verify-email) - components/auth/: Reusable auth components (LoginForm, RegisterForm, AuthGuard, etc.) Updated _app.tsx to include SessionProvider for auth state management.
128 lines
3.7 KiB
TypeScript
128 lines
3.7 KiB
TypeScript
import { useEffect } from 'react'
|
|
import { useRouter } from 'next/router'
|
|
import { useAuth } from '@/lib/auth/useAuth'
|
|
import { UserRole } from '@/lib/auth/types'
|
|
import { hasRole, hasPermission } from '@/lib/auth/permissions'
|
|
|
|
interface AuthGuardProps {
|
|
children: React.ReactNode
|
|
requiredRole?: UserRole
|
|
requiredPermission?: string
|
|
fallback?: React.ReactNode
|
|
redirectTo?: string
|
|
}
|
|
|
|
export function AuthGuard({
|
|
children,
|
|
requiredRole,
|
|
requiredPermission,
|
|
fallback,
|
|
redirectTo = '/auth/signin',
|
|
}: AuthGuardProps) {
|
|
const router = useRouter()
|
|
const { user, isAuthenticated, isLoading } = useAuth()
|
|
|
|
useEffect(() => {
|
|
if (!isLoading && !isAuthenticated) {
|
|
const returnUrl = encodeURIComponent(router.asPath)
|
|
router.push(`${redirectTo}?callbackUrl=${returnUrl}`)
|
|
}
|
|
}, [isLoading, isAuthenticated, router, redirectTo])
|
|
|
|
// Show loading state
|
|
if (isLoading) {
|
|
return (
|
|
fallback || (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600">Loading...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
)
|
|
}
|
|
|
|
// Not authenticated
|
|
if (!isAuthenticated) {
|
|
return (
|
|
fallback || (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="text-center">
|
|
<p className="text-gray-600">Redirecting to sign in...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
)
|
|
}
|
|
|
|
// Check role requirement
|
|
if (requiredRole && user) {
|
|
if (!hasRole(user.role, requiredRole)) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="text-center max-w-md mx-auto p-8">
|
|
<div className="text-red-500 text-6xl mb-4">403</div>
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">Access Denied</h1>
|
|
<p className="text-gray-600 mb-4">
|
|
You don't have permission to access this page. This page requires{' '}
|
|
<span className="font-medium">{requiredRole}</span> role or higher.
|
|
</p>
|
|
<button
|
|
onClick={() => router.back()}
|
|
className="text-green-600 hover:text-green-500 font-medium"
|
|
>
|
|
Go back
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
// Check permission requirement
|
|
if (requiredPermission && user) {
|
|
if (!hasPermission(user.role, requiredPermission)) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="text-center max-w-md mx-auto p-8">
|
|
<div className="text-red-500 text-6xl mb-4">403</div>
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">Access Denied</h1>
|
|
<p className="text-gray-600 mb-4">
|
|
You don't have the required permission to access this page.
|
|
</p>
|
|
<button
|
|
onClick={() => router.back()}
|
|
className="text-green-600 hover:text-green-500 font-medium"
|
|
>
|
|
Go back
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
return <>{children}</>
|
|
}
|
|
|
|
// Higher-order component version
|
|
export function withAuthGuard<P extends object>(
|
|
Component: React.ComponentType<P>,
|
|
options?: {
|
|
requiredRole?: UserRole
|
|
requiredPermission?: string
|
|
fallback?: React.ReactNode
|
|
redirectTo?: string
|
|
}
|
|
) {
|
|
return function AuthGuardedComponent(props: P) {
|
|
return (
|
|
<AuthGuard {...options}>
|
|
<Component {...props} />
|
|
</AuthGuard>
|
|
)
|
|
}
|
|
}
|
|
|
|
export default AuthGuard
|