verify($pending2FA['user_id'], $code)) { // Complete login handleSuccessfulLogin($pending2FA['user_id'], $pending2FA['username'], $pending2FA['remember_me'], $config, $logObject, $user_IP); // Clean up 2FA session data Session::clear2FAPending(); exit(); } // If we get here (and we have code submitted), verification failed if (!empty($code)) { Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code'); } // Get any new feedback messages include '../app/helpers/feedback.php'; // Load the 2FA verification template include '../app/templates/credentials-2fa-verify.php'; exit(); } elseif ($action === 'forgot') { if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Handle password reset request try { // Validate CSRF token $security = SecurityHelper::getInstance(); if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) { throw new Exception('Invalid security token. Please try again.'); } // Apply rate limiting if (!$rateLimiter->isIpWhitelisted($user_IP)) { if ($rateLimiter->isIpBlacklisted($user_IP)) { throw new Exception(Feedback::get('LOGIN', 'IP_BLACKLISTED')['message']); } if ($rateLimiter->tooManyAttempts('password_reset', $user_IP)) { throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']); } $rateLimiter->attempt('password_reset', $user_IP); } // Validate email $email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL); if (!$email) { throw new Exception('Please enter a valid email address.'); } // Process reset request require_once '../app/classes/passwordReset.php'; $resetHandler = new PasswordReset($db); $result = $resetHandler->requestReset($email); // Always show same message whether email exists or not for security Feedback::flash('NOTICE', 'DEFAULT', $result['message']); header("Location: $app_root?page=login"); exit(); } catch (Exception $e) { Feedback::flash('ERROR', 'DEFAULT', $e->getMessage()); } } // Generate CSRF token $security = SecurityHelper::getInstance(); $security->generateCsrfToken(); // Load the forgot password form include '../app/helpers/feedback.php'; include '../app/templates/form-password-forgot.php'; exit(); } elseif ($action === 'reset' && isset($_GET['token'])) { // Handle password reset try { require_once '../app/classes/passwordReset.php'; $resetHandler = new PasswordReset($db); $token = $_GET['token']; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Validate CSRF token $security = SecurityHelper::getInstance(); if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) { throw new Exception('Invalid security token. Please try again.'); } // Apply rate limiting if (!$rateLimiter->isIpWhitelisted($user_IP)) { if ($rateLimiter->tooManyAttempts('password_reset', $user_IP)) { throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']); } $rateLimiter->attempt('password_reset', $user_IP); } // Validate password require_once '../app/classes/validator.php'; $validator = new Validator($_POST); $rules = [ 'new_password' => [ 'required' => true, 'min' => 8 ], 'confirm_password' => [ 'required' => true, 'matches' => 'new_password' ] ]; if (!$validator->validate($rules)) { throw new Exception($validator->getFirstError()); } // Reset password if ($resetHandler->resetPassword($token, $_POST['new_password'])) { Feedback::flash('NOTICE', 'DEFAULT', 'Your password has been reset successfully. You can now log in.'); header("Location: $app_root?page=login"); exit(); } throw new Exception('Invalid or expired reset link. Please request a new one.'); } // Verify token is valid $validation = $resetHandler->validateToken($token); if (!$validation['valid']) { throw new Exception('Invalid or expired reset link. Please request a new one.'); } // Show reset password form include '../app/helpers/feedback.php'; include '../app/templates/form-password-reset.php'; exit(); } catch (Exception $e) { Feedback::flash('ERROR', 'DEFAULT', $e->getMessage()); header("Location: $app_root?page=login&action=forgot"); exit(); } } if ( $_SERVER['REQUEST_METHOD'] == 'POST' && $action !== 'verify' ) { try { // Validate form data $security = SecurityHelper::getInstance(); $formData = $security->sanitizeArray($_POST, ['username', 'password', 'remember_me', 'csrf_token']); $validationRules = [ 'username' => [ 'type' => 'string', 'required' => true, 'min' => 3, 'max' => 20 ], 'password' => [ 'type' => 'string', 'required' => true, 'min' => 5 ] ]; $errors = $security->validateFormData($formData, $validationRules); if (!empty($errors)) { throw new Exception("Invalid input: " . implode(", ", $errors)); } $username = $formData['username']; $password = $formData['password']; // Skip all checks if IP is whitelisted if (!$rateLimiter->isIpWhitelisted($user_IP)) { // Check if IP is blacklisted if ($rateLimiter->isIpBlacklisted($user_IP)) { throw new Exception(Feedback::get('LOGIN', 'IP_BLACKLISTED')['message']); } // Check rate limiting before recording attempt if ($rateLimiter->tooManyAttempts($username, $user_IP)) { throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']); } // Record this attempt $rateLimiter->attempt($username, $user_IP); } // Attempt login $loginResult = $userObject->login($username, $password); if (is_array($loginResult)) { switch ($loginResult['status']) { case 'requires_2fa': // Store pending 2FA info Session::store2FAPending($loginResult['user_id'], $loginResult['username'], isset($formData['remember_me'])); // Redirect to 2FA verification header('Location: ?page=login&action=verify'); exit(); case 'success': // Complete login handleSuccessfulLogin($loginResult['user_id'], $loginResult['username'], isset($formData['remember_me']), $config, $logObject, $user_IP); exit(); default: throw new Exception($loginResult['message'] ?? 'Login failed'); } } throw new Exception(Feedback::get('LOGIN', 'LOGIN_FAILED')['message']); } catch (Exception $e) { // Log the failed attempt Feedback::flash('ERROR', 'DEFAULT', $e->getMessage()); if (isset($username)) { $user_id = $userObject->getUserId($username)[0]['id'] ?? 0; $logObject->insertLog($user_id, "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", 'user'); } } } } catch (Exception $e) { Feedback::flash('ERROR', 'DEFAULT'); } // Show configured login message if any if (!empty($config['login_message'])) { echo Feedback::render('NOTICE', 'DEFAULT', $config['login_message'], false); } // Get any new feedback messages include '../app/helpers/feedback.php'; // Load the template include '../app/templates/form-login.php'; /** * Handle successful login by setting up session and cookies */ function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $userIP) { // Create authenticated session Session::createAuthSession($userId, $username, $rememberMe, $config); // Log successful login $logObject->insertLog($userId, "Login: User \"$username\" logged in. IP: $userIP", 'user'); // Set success message and redirect Feedback::flash('LOGIN', 'LOGIN_SUCCESS'); header('Location: ' . htmlspecialchars($app_root)); }