diff --git a/src/Services/AuditLogger.php b/src/Services/AuditLogger.php new file mode 100644 index 00000000..fafa0e60 --- /dev/null +++ b/src/Services/AuditLogger.php @@ -0,0 +1,399 @@ +getConnection(); + + if (!$conn) { + error_log("AuditLogger: Database connection failed"); + return false; + } + + // Prepare and execute insert statement + $query = "INSERT INTO audit_logs (user_id, action, status, ip_address, details, created_at) + VALUES (?, ?, ?, ?, ?, NOW())"; + + $stmt = $conn->prepare($query); + if (!$stmt) { + error_log("AuditLogger: Failed to prepare statement: " . $conn->error); + return false; + } + + $stmt->bind_param( + "issss", + $userId, + $action, + $status, + $ipAddress, + $details + ); + + if (!$stmt->execute()) { + error_log("AuditLogger: Failed to execute statement: " . $stmt->error); + $stmt->close(); + return false; + } + + $stmt->close(); + return true; + } catch (\Exception $e) { + error_log("AuditLogger exception: " . $e->getMessage()); + return false; + } + } + + /** + * Log login attempt + * + * @param string $email Email address + * @param bool $success Whether login was successful + * @param string|null $failureReason Reason for failure (if applicable) + * @return bool + */ + public static function logLogin( + string $email, + bool $success, + ?string $failureReason = null + ): bool { + $action = $success ? self::ACTION_LOGIN_SUCCESS : self::ACTION_LOGIN_FAILURE; + $status = $success ? self::STATUS_SUCCESS : self::STATUS_FAILURE; + + $details = [ + 'email' => $email, + 'reason' => $failureReason + ]; + + return self::log($action, $status, null, json_encode($details)); + } + + /** + * Log logout + * + * @param int $userId User ID + * @return bool + */ + public static function logLogout(int $userId): bool + { + return self::log(self::ACTION_LOGOUT, self::STATUS_SUCCESS, $userId); + } + + /** + * Log password change + * + * @param int $userId User ID + * @param bool $success Whether password was changed successfully + * @return bool + */ + public static function logPasswordChange(int $userId, bool $success): bool + { + $status = $success ? self::STATUS_SUCCESS : self::STATUS_FAILURE; + return self::log(self::ACTION_PASSWORD_CHANGE, $status, $userId); + } + + /** + * Log booking creation + * + * @param int $userId User ID + * @param string $bookingType Type of booking (trip, camping, course, etc.) + * @param string|int $bookingId Booking ID + * @param float|null $amount Booking amount + * @return bool + */ + public static function logBookingCreate( + int $userId, + string $bookingType, + $bookingId, + ?float $amount = null + ): bool { + $details = [ + 'booking_type' => $bookingType, + 'booking_id' => $bookingId, + 'amount' => $amount + ]; + + return self::log( + self::ACTION_BOOKING_CREATE, + self::STATUS_SUCCESS, + $userId, + json_encode($details) + ); + } + + /** + * Log payment + * + * @param int $userId User ID + * @param string $status Payment status (success/failure) + * @param float $amount Payment amount + * @param string|null $reason Failure reason if applicable + * @param string|null $details Additional details + * @return bool + */ + public static function logPayment( + int $userId, + string $status, + float $amount, + ?string $reason = null, + ?string $details = null + ): bool { + $action = ($status === self::STATUS_SUCCESS) ? + self::ACTION_PAYMENT_SUCCESS : + self::ACTION_PAYMENT_FAILURE; + + $data = [ + 'amount' => $amount, + 'reason' => $reason, + 'details' => $details + ]; + + return self::log($action, $status, $userId, json_encode($data)); + } + + /** + * Log membership application + * + * @param int $userId User ID + * @param string $action Action type (application/approval/renewal) + * @param bool $success Whether action was successful + * @return bool + */ + public static function logMembership( + int $userId, + string $action, + bool $success + ): bool { + $status = $success ? self::STATUS_SUCCESS : self::STATUS_FAILURE; + $actionType = 'membership_' . $action; + + return self::log($actionType, $status, $userId); + } + + /** + * Log access denied event + * + * @param int|null $userId User ID + * @param string $resource Resource that was accessed + * @param string|null $reason Reason for denial + * @return bool + */ + public static function logAccessDenied( + ?int $userId = null, + string $resource = 'unknown', + ?string $reason = null + ): bool { + $details = [ + 'resource' => $resource, + 'reason' => $reason + ]; + + return self::log( + self::ACTION_ACCESS_DENIED, + self::STATUS_FAILURE, + $userId, + json_encode($details) + ); + } + + /** + * Get recent audit logs + * + * @param int $limit Number of records to retrieve + * @param int $userId Optional user ID to filter by + * @return array Array of audit log records + */ + public static function getRecentLogs(int $limit = 100, ?int $userId = null): array + { + try { + $db = DatabaseService::getInstance(); + $conn = $db->getConnection(); + + if (!$conn) { + return []; + } + + if ($userId !== null) { + $query = "SELECT * FROM audit_logs WHERE user_id = ? + ORDER BY created_at DESC LIMIT ?"; + $stmt = $conn->prepare($query); + $stmt->bind_param("ii", $userId, $limit); + } else { + $query = "SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT ?"; + $stmt = $conn->prepare($query); + $stmt->bind_param("i", $limit); + } + + $stmt->execute(); + $result = $stmt->get_result(); + $logs = []; + + while ($row = $result->fetch_assoc()) { + // Decode JSON details if present + if (!empty($row['details'])) { + $row['details'] = json_decode($row['details'], true) ?? $row['details']; + } + $logs[] = $row; + } + + $stmt->close(); + return $logs; + } catch (\Exception $e) { + error_log("AuditLogger::getRecentLogs exception: " . $e->getMessage()); + return []; + } + } + + /** + * Get logs for a specific action + * + * @param string $action Action type to filter by + * @param int $limit Number of records + * @return array + */ + public static function getLogsByAction(string $action, int $limit = 100): array + { + try { + $db = DatabaseService::getInstance(); + $conn = $db->getConnection(); + + if (!$conn) { + return []; + } + + $query = "SELECT * FROM audit_logs WHERE action = ? + ORDER BY created_at DESC LIMIT ?"; + $stmt = $conn->prepare($query); + $stmt->bind_param("si", $action, $limit); + + $stmt->execute(); + $result = $stmt->get_result(); + $logs = []; + + while ($row = $result->fetch_assoc()) { + if (!empty($row['details'])) { + $row['details'] = json_decode($row['details'], true) ?? $row['details']; + } + $logs[] = $row; + } + + $stmt->close(); + return $logs; + } catch (\Exception $e) { + error_log("AuditLogger::getLogsByAction exception: " . $e->getMessage()); + return []; + } + } + + /** + * Get client IP address + * Attempts to detect the real IP even behind proxy/load balancer + * + * @return string Client IP address or 'unknown' + */ + private static function getClientIp(): string + { + // Check for IP from shared internet + if (!empty($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + } + // Check for IP passed from proxy + elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + // Handle multiple IPs (take the first one) + $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + $ip = trim($ips[0]); + } + // Check the remote address + elseif (!empty($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + else { + $ip = 'unknown'; + } + + // Validate IP format + if (filter_var($ip, FILTER_VALIDATE_IP)) { + return $ip; + } + + return 'unknown'; + } +} diff --git a/validate_login.php b/validate_login.php index cb144376..54c2985a 100644 --- a/validate_login.php +++ b/validate_login.php @@ -8,6 +8,7 @@ require_once 'google-client/vendor/autoload.php'; // Add this line for Google Cl use Middleware\CsrfMiddleware; use Middleware\RateLimitMiddleware; use Services\AuthenticationService; +use Services\AuditLogger; // Check if connection is established if (!$conn) { @@ -70,6 +71,8 @@ if (isset($_GET['code'])) { AuthenticationService::regenerateSession(); // Reset rate limit on successful login RateLimitMiddleware::reset('login'); + // Log successful registration via Google + AuditLogger::logLogin($email, true); // echo json_encode(['status' => 'success', 'message' => 'Google login successful']); header("Location: index.php"); exit(); @@ -89,6 +92,8 @@ if (isset($_GET['code'])) { AuthenticationService::regenerateSession(); // Reset rate limit on successful login RateLimitMiddleware::reset('login'); + // Log successful Google login + AuditLogger::logLogin($email, true); // echo json_encode(['status' => 'success', 'message' => 'Google login successful']); header("Location: index.php"); exit(); @@ -122,12 +127,14 @@ if (isset($_POST['email']) && isset($_POST['password'])) { if (empty($email) || empty($password)) { echo json_encode(['status' => 'error', 'message' => 'Please enter both email and password.']); RateLimitMiddleware::incrementAttempt('login', 900); + AuditLogger::logLogin($email ?? 'unknown', false, 'Empty email or password'); exit(); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']); RateLimitMiddleware::incrementAttempt('login', 900); + AuditLogger::logLogin($email ?? 'unknown', false, 'Invalid email format'); exit(); } @@ -152,6 +159,7 @@ if (isset($_POST['email']) && isset($_POST['password'])) { if ($row['is_verified'] == 0) { echo json_encode(['status' => 'error', 'message' => 'Your account is not verified. Please check your email for the verification link.']); RateLimitMiddleware::incrementAttempt('login', 900); + AuditLogger::logLogin($email, false, 'Account not verified'); exit(); } @@ -164,15 +172,19 @@ if (isset($_POST['email']) && isset($_POST['password'])) { AuthenticationService::regenerateSession(); // Reset rate limit on successful login RateLimitMiddleware::reset('login'); + // Log successful email/password login + AuditLogger::logLogin($email, true); echo json_encode(['status' => 'success', 'message' => 'Successful Login']); } else { // Password is incorrect - increment rate limit RateLimitMiddleware::incrementAttempt('login', 900); + AuditLogger::logLogin($email, false, 'Invalid password'); echo json_encode(['status' => 'error', 'message' => 'Invalid password.']); } } else { // User does not exist - still increment rate limit to prevent email enumeration RateLimitMiddleware::incrementAttempt('login', 900); + AuditLogger::logLogin($email, false, 'User not found'); echo json_encode(['status' => 'error', 'message' => 'User with that email does not exist.']); }