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.
180 lines
5.7 KiB
TypeScript
180 lines
5.7 KiB
TypeScript
import { NextApiRequest, NextApiResponse, NextApiHandler } from 'next'
|
|
import { getServerSession } from 'next-auth/next'
|
|
import { authOptions } from '@/pages/api/auth/[...nextauth]'
|
|
import { UserRole, AuthUser } from './types'
|
|
import { hasPermission, hasRole } from './permissions'
|
|
|
|
export interface AuthenticatedRequest extends NextApiRequest {
|
|
user: AuthUser
|
|
}
|
|
|
|
type AuthenticatedHandler = (
|
|
req: AuthenticatedRequest,
|
|
res: NextApiResponse
|
|
) => Promise<void> | void
|
|
|
|
interface WithAuthOptions {
|
|
requiredRole?: UserRole
|
|
requiredPermission?: string
|
|
requiredPermissions?: string[]
|
|
requireAll?: boolean // If true, requires all permissions; if false, requires any
|
|
}
|
|
|
|
export function withAuth(
|
|
handler: AuthenticatedHandler,
|
|
options?: WithAuthOptions
|
|
): NextApiHandler {
|
|
return async (req: NextApiRequest, res: NextApiResponse) => {
|
|
try {
|
|
const session = await getServerSession(req, res, authOptions)
|
|
|
|
if (!session?.user) {
|
|
return res.status(401).json({
|
|
error: 'Unauthorized',
|
|
message: 'You must be signed in to access this resource',
|
|
})
|
|
}
|
|
|
|
const user: AuthUser = {
|
|
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,
|
|
}
|
|
|
|
// Check role requirement
|
|
if (options?.requiredRole) {
|
|
if (!hasRole(user.role, options.requiredRole)) {
|
|
return res.status(403).json({
|
|
error: 'Forbidden',
|
|
message: `This resource requires ${options.requiredRole} role or higher`,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Check single permission
|
|
if (options?.requiredPermission) {
|
|
if (!hasPermission(user.role, options.requiredPermission)) {
|
|
return res.status(403).json({
|
|
error: 'Forbidden',
|
|
message: `You do not have the required permission: ${options.requiredPermission}`,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Check multiple permissions
|
|
if (options?.requiredPermissions && options.requiredPermissions.length > 0) {
|
|
const checkFunction = options.requireAll
|
|
? (perms: string[]) => perms.every(p => hasPermission(user.role, p))
|
|
: (perms: string[]) => perms.some(p => hasPermission(user.role, p))
|
|
|
|
if (!checkFunction(options.requiredPermissions)) {
|
|
return res.status(403).json({
|
|
error: 'Forbidden',
|
|
message: options.requireAll
|
|
? `You need all of these permissions: ${options.requiredPermissions.join(', ')}`
|
|
: `You need at least one of these permissions: ${options.requiredPermissions.join(', ')}`,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Add user to request
|
|
const authReq = req as AuthenticatedRequest
|
|
authReq.user = user
|
|
|
|
return handler(authReq, res)
|
|
} catch (error) {
|
|
console.error('Auth middleware error:', error)
|
|
return res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred while authenticating your request',
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convenience wrapper for role-based protection
|
|
export function withRole(
|
|
handler: AuthenticatedHandler,
|
|
role: UserRole
|
|
): NextApiHandler {
|
|
return withAuth(handler, { requiredRole: role })
|
|
}
|
|
|
|
// Convenience wrapper for permission-based protection
|
|
export function withPermission(
|
|
handler: AuthenticatedHandler,
|
|
permission: string
|
|
): NextApiHandler {
|
|
return withAuth(handler, { requiredPermission: permission })
|
|
}
|
|
|
|
// Convenience wrapper for multiple permissions (any)
|
|
export function withAnyPermission(
|
|
handler: AuthenticatedHandler,
|
|
permissions: string[]
|
|
): NextApiHandler {
|
|
return withAuth(handler, { requiredPermissions: permissions, requireAll: false })
|
|
}
|
|
|
|
// Convenience wrapper for multiple permissions (all)
|
|
export function withAllPermissions(
|
|
handler: AuthenticatedHandler,
|
|
permissions: string[]
|
|
): NextApiHandler {
|
|
return withAuth(handler, { requiredPermissions: permissions, requireAll: true })
|
|
}
|
|
|
|
// Rate limiting helper (basic implementation)
|
|
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
|
|
|
|
export function checkRateLimit(
|
|
identifier: string,
|
|
limit: number = 100,
|
|
windowMs: number = 60000
|
|
): { allowed: boolean; remaining: number; resetAt: number } {
|
|
const now = Date.now()
|
|
const record = rateLimitMap.get(identifier)
|
|
|
|
if (!record || now > record.resetAt) {
|
|
rateLimitMap.set(identifier, { count: 1, resetAt: now + windowMs })
|
|
return { allowed: true, remaining: limit - 1, resetAt: now + windowMs }
|
|
}
|
|
|
|
if (record.count >= limit) {
|
|
return { allowed: false, remaining: 0, resetAt: record.resetAt }
|
|
}
|
|
|
|
record.count++
|
|
return { allowed: true, remaining: limit - record.count, resetAt: record.resetAt }
|
|
}
|
|
|
|
export function withRateLimit(
|
|
handler: NextApiHandler,
|
|
options: { limit?: number; windowMs?: number; keyGenerator?: (req: NextApiRequest) => string }
|
|
): NextApiHandler {
|
|
const limit = options.limit || 100
|
|
const windowMs = options.windowMs || 60000
|
|
const keyGenerator = options.keyGenerator || ((req) => req.socket.remoteAddress || 'unknown')
|
|
|
|
return async (req: NextApiRequest, res: NextApiResponse) => {
|
|
const key = keyGenerator(req)
|
|
const { allowed, remaining, resetAt } = checkRateLimit(key, limit, windowMs)
|
|
|
|
res.setHeader('X-RateLimit-Limit', limit)
|
|
res.setHeader('X-RateLimit-Remaining', remaining)
|
|
res.setHeader('X-RateLimit-Reset', Math.ceil(resetAt / 1000))
|
|
|
|
if (!allowed) {
|
|
return res.status(429).json({
|
|
error: 'Too Many Requests',
|
|
message: 'Rate limit exceeded. Please try again later.',
|
|
retryAfter: Math.ceil((resetAt - Date.now()) / 1000),
|
|
})
|
|
}
|
|
|
|
return handler(req, res)
|
|
}
|
|
}
|