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.
157 lines
4 KiB
TypeScript
157 lines
4 KiB
TypeScript
import { useSession, signIn, signOut } from 'next-auth/react'
|
|
import { useCallback, useMemo } from 'react'
|
|
import { AuthUser, UserRole } from './types'
|
|
import { hasPermission, hasRole, hasAnyPermission, hasAllPermissions } from './permissions'
|
|
|
|
interface UseAuthReturn {
|
|
// User state
|
|
user: AuthUser | null
|
|
isAuthenticated: boolean
|
|
isLoading: boolean
|
|
|
|
// Auth actions
|
|
login: (provider?: string, options?: { callbackUrl?: string }) => Promise<void>
|
|
logout: (options?: { callbackUrl?: string }) => Promise<void>
|
|
loginWithCredentials: (email: string, password: string, callbackUrl?: string) => Promise<void>
|
|
|
|
// Permission checks
|
|
can: (permission: string) => boolean
|
|
canAny: (permissions: string[]) => boolean
|
|
canAll: (permissions: string[]) => boolean
|
|
is: (role: UserRole) => boolean
|
|
isAtLeast: (role: UserRole) => boolean
|
|
|
|
// Session management
|
|
refreshSession: () => Promise<void>
|
|
}
|
|
|
|
export function useAuth(): UseAuthReturn {
|
|
const { data: session, status, update } = useSession()
|
|
const isLoading = status === 'loading'
|
|
const isAuthenticated = status === 'authenticated'
|
|
|
|
const user: AuthUser | null = useMemo(() => {
|
|
if (!session?.user) return null
|
|
return {
|
|
id: session.user.id,
|
|
email: session.user.email!,
|
|
name: session.user.name,
|
|
image: session.user.image,
|
|
role: session.user.role || UserRole.USER,
|
|
emailVerified: session.user.emailVerified,
|
|
}
|
|
}, [session])
|
|
|
|
const login = useCallback(
|
|
async (provider?: string, options?: { callbackUrl?: string }) => {
|
|
await signIn(provider, { callbackUrl: options?.callbackUrl || '/' })
|
|
},
|
|
[]
|
|
)
|
|
|
|
const logout = useCallback(async (options?: { callbackUrl?: string }) => {
|
|
await signOut({ callbackUrl: options?.callbackUrl || '/' })
|
|
}, [])
|
|
|
|
const loginWithCredentials = useCallback(
|
|
async (email: string, password: string, callbackUrl?: string) => {
|
|
const result = await signIn('credentials', {
|
|
email,
|
|
password,
|
|
redirect: false,
|
|
})
|
|
|
|
if (result?.error) {
|
|
throw new Error(result.error)
|
|
}
|
|
|
|
if (callbackUrl) {
|
|
window.location.href = callbackUrl
|
|
}
|
|
},
|
|
[]
|
|
)
|
|
|
|
const can = useCallback(
|
|
(permission: string): boolean => {
|
|
if (!user) return false
|
|
return hasPermission(user.role, permission)
|
|
},
|
|
[user]
|
|
)
|
|
|
|
const canAny = useCallback(
|
|
(permissions: string[]): boolean => {
|
|
if (!user) return false
|
|
return hasAnyPermission(user.role, permissions)
|
|
},
|
|
[user]
|
|
)
|
|
|
|
const canAll = useCallback(
|
|
(permissions: string[]): boolean => {
|
|
if (!user) return false
|
|
return hasAllPermissions(user.role, permissions)
|
|
},
|
|
[user]
|
|
)
|
|
|
|
const is = useCallback(
|
|
(role: UserRole): boolean => {
|
|
if (!user) return false
|
|
return user.role === role
|
|
},
|
|
[user]
|
|
)
|
|
|
|
const isAtLeast = useCallback(
|
|
(role: UserRole): boolean => {
|
|
if (!user) return false
|
|
return hasRole(user.role, role)
|
|
},
|
|
[user]
|
|
)
|
|
|
|
const refreshSession = useCallback(async () => {
|
|
await update()
|
|
}, [update])
|
|
|
|
return {
|
|
user,
|
|
isAuthenticated,
|
|
isLoading,
|
|
login,
|
|
logout,
|
|
loginWithCredentials,
|
|
can,
|
|
canAny,
|
|
canAll,
|
|
is,
|
|
isAtLeast,
|
|
refreshSession,
|
|
}
|
|
}
|
|
|
|
// Hook for checking a specific permission
|
|
export function usePermission(permission: string): boolean {
|
|
const { can } = useAuth()
|
|
return can(permission)
|
|
}
|
|
|
|
// Hook for checking a specific role
|
|
export function useRole(role: UserRole): boolean {
|
|
const { isAtLeast } = useAuth()
|
|
return isAtLeast(role)
|
|
}
|
|
|
|
// Hook for requiring authentication (with redirect)
|
|
export function useRequireAuth(options?: { redirectTo?: string }) {
|
|
const { isAuthenticated, isLoading } = useAuth()
|
|
|
|
if (!isLoading && !isAuthenticated && typeof window !== 'undefined') {
|
|
const redirectTo = options?.redirectTo || '/auth/signin'
|
|
window.location.href = `${redirectTo}?callbackUrl=${encodeURIComponent(window.location.pathname)}`
|
|
}
|
|
|
|
return { isAuthenticated, isLoading }
|
|
}
|