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