localgreenchain/lib/auth/withAuth.ts
Claude 39b6081baa
Implement comprehensive authentication system (Agent 1)
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.
2025-11-23 03:52:09 +00:00

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)
}
}