auth = $auth; } /** * Require authentication */ public function requireAuth(): callable { return function(Context $context, callable $next) { if ($this->auth->guest()) { // Check if request expects JSON if ($context->request->expectsJson()) { $context->json(['error' => 'Unauthenticated'], 401); return; } // Redirect to login page $context->redirect('/login'); return; } // Add user to context $context->set('user', $this->auth->user()); $next(); }; } /** * Require guest (not authenticated) */ public function requireGuest(): callable { return function(Context $context, callable $next) { if ($this->auth->check()) { // Check if request expects JSON if ($context->request->expectsJson()) { $context->json(['error' => 'Already authenticated'], 403); return; } // Redirect to home or dashboard $context->redirect('/'); return; } $next(); }; } /** * Optional authentication (sets user if authenticated) */ public function optional(): callable { return function(Context $context, callable $next) { if ($this->auth->check()) $context->set('user', $this->auth->user()); $next(); }; } /** * Check if user has specific role */ public function requireRole(string|array $roles): callable { $roles = is_array($roles) ? $roles : [$roles]; return function(Context $context, callable $next) use ($roles) { if ($this->auth->guest()) { if ($context->request->expectsJson()) { $context->json(['error' => 'Unauthenticated'], 401); return; } $context->redirect('/login'); return; } $user = $this->auth->user(); $userRole = $user->role; if (!in_array($userRole, $roles)) { if ($context->request->expectsJson()) { $context->json(['error' => 'Insufficient permissions'], 403); return; } $context->error(403, 'Forbidden'); return; } $context->set('user', $user); $next(); }; } /** * Rate limiting per user */ public function rateLimit(int $maxAttempts = 60, int $decayMinutes = 1): callable { return function(Context $context, callable $next) use ($maxAttempts, $decayMinutes) { if ($this->auth->guest()) { $identifier = $context->request->ip(); } else { $identifier = 'user:' . $this->auth->id(); } $key = 'rate_limit:' . $identifier . ':' . $context->request->path; $attempts = $context->session->get($key, 0); $resetTime = $context->session->get($key . ':reset', 0); // Reset counter if decay time has passed if (time() > $resetTime) { $attempts = 0; $context->session->set($key . ':reset', time() + ($decayMinutes * 60)); } if ($attempts >= $maxAttempts) { $retryAfter = $resetTime - time(); $context->response->header('X-RateLimit-Limit', (string)$maxAttempts); $context->response->header('X-RateLimit-Remaining', '0'); $context->response->header('Retry-After', (string)$retryAfter); if ($context->request->expectsJson()) { $context->json([ 'error' => 'Too many requests', 'retry_after' => $retryAfter ], 429); return; } $context->error(429, 'Too Many Requests'); return; } // Increment attempts $context->session->set($key, $attempts + 1); // Add rate limit headers $context->response->header('X-RateLimit-Limit', (string)$maxAttempts); $context->response->header('X-RateLimit-Remaining', (string)($maxAttempts - $attempts - 1)); $next(); }; } /** * CSRF protection */ public function verifyCsrf(): callable { return function(Context $context, callable $next) { // Skip CSRF for safe methods if (in_array($context->request->method, ['GET', 'HEAD', 'OPTIONS'])) { $next(); return; } $token = $context->request->input('_token') ?? $context->request->header('X-CSRF-TOKEN') ?? $context->request->header('X-XSRF-TOKEN'); if (!$context->session->validateCsrf($token)) { if ($context->request->expectsJson()) { $context->json(['error' => 'CSRF token mismatch'], 419); return; } $context->error(419, 'CSRF token mismatch'); return; } $next(); }; } /** * Remember user from cookie */ public function remember(): callable { return function(Context $context, callable $next) { // Auth class already handles remember cookies in constructor // This middleware can be used to refresh the remember token if needed if ($this->auth->check()) { $context->set('user', $this->auth->user()); } $next(); }; } }