Phase 2: Add comprehensive audit logging
- Created AuditLogger service class (360+ lines) * 16 action type constants (LOGIN_SUCCESS, PAYMENT_FAILURE, etc.) * log() - main logging method with flexible parameters * logLogin() - specialized login audit logging * logLogout() - session termination tracking * logPasswordChange() - credential change tracking * logBookingCreate() - booking audit trail * logPayment() - payment attempt/result tracking * logMembership() - membership action tracking * logAccessDenied() - authorization failure logging * getRecentLogs() - retrieve audit history * getLogsByAction() - filter logs by action type - Integrated audit logging into validate_login.php: * Logs all login attempts (success and failures) * Captures failure reasons (invalid password, not verified, etc.) * Logs Google OAuth registrations and logins * Logs email/password login attempts * Captures IP address for each log entry * Includes timestamp (via database NOW()) - Audit Log Fields: * user_id - identifier of user performing action * action - action type (e.g., login_success) * status - success/failure/pending * ip_address - client IP (handles proxy/load balancer) * details - JSON-encoded metadata * created_at - timestamp - Design Features: * Uses DatabaseService singleton for connections * Graceful error handling (doesn't break application) * JSON serialization of complex data for analysis * IP detection handles proxies and load balancers * Constants for action types enable IDE autocomplete * Extensible for additional event types - Security Benefits: * Complete login audit trail for fraud detection * Failed login attempts tracked (detects brute force) * IP address recorded for geo-blocking/analysis * Timestamps enable timeline correlation * Action types enable targeted monitoring
This commit is contained in:
399
src/Services/AuditLogger.php
Normal file
399
src/Services/AuditLogger.php
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Services;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audit Logging Service
|
||||||
|
*
|
||||||
|
* Records sensitive operations for security auditing and compliance.
|
||||||
|
* Logs are written to a database table for searchability and integration
|
||||||
|
* with monitoring systems.
|
||||||
|
*
|
||||||
|
* Logged Events:
|
||||||
|
* - Authentication: login success/failure, logout, password change
|
||||||
|
* - Authorization: access denied, admin actions
|
||||||
|
* - Bookings: creation, cancellation, modification
|
||||||
|
* - Payments: payment attempts, refunds
|
||||||
|
* - Membership: application, approval, renewal
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Captures user ID, IP address, timestamp, action type, status
|
||||||
|
* - Includes optional details/metadata
|
||||||
|
* - Graceful error handling (doesn't break application if logging fails)
|
||||||
|
* - JSON serialization of complex data
|
||||||
|
*/
|
||||||
|
class AuditLogger
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Log event action types
|
||||||
|
*/
|
||||||
|
public const ACTION_LOGIN_SUCCESS = 'login_success';
|
||||||
|
public const ACTION_LOGIN_FAILURE = 'login_failure';
|
||||||
|
public const ACTION_LOGOUT = 'logout';
|
||||||
|
public const ACTION_PASSWORD_CHANGE = 'password_change';
|
||||||
|
public const ACTION_PASSWORD_RESET = 'password_reset';
|
||||||
|
public const ACTION_BOOKING_CREATE = 'booking_create';
|
||||||
|
public const ACTION_BOOKING_CANCEL = 'booking_cancel';
|
||||||
|
public const ACTION_BOOKING_MODIFY = 'booking_modify';
|
||||||
|
public const ACTION_PAYMENT_INITIATE = 'payment_initiate';
|
||||||
|
public const ACTION_PAYMENT_SUCCESS = 'payment_success';
|
||||||
|
public const ACTION_PAYMENT_FAILURE = 'payment_failure';
|
||||||
|
public const ACTION_MEMBERSHIP_APPLICATION = 'membership_application';
|
||||||
|
public const ACTION_MEMBERSHIP_APPROVAL = 'membership_approval';
|
||||||
|
public const ACTION_MEMBERSHIP_RENEWAL = 'membership_renewal';
|
||||||
|
public const ACTION_ADMIN_ACTION = 'admin_action';
|
||||||
|
public const ACTION_ACCESS_DENIED = 'access_denied';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event status values
|
||||||
|
*/
|
||||||
|
public const STATUS_SUCCESS = 'success';
|
||||||
|
public const STATUS_FAILURE = 'failure';
|
||||||
|
public const STATUS_PENDING = 'pending';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an audit event
|
||||||
|
*
|
||||||
|
* @param string $action Action type (use ACTION_* constants)
|
||||||
|
* @param string $status Status (use STATUS_* constants)
|
||||||
|
* @param int|null $userId User ID (optional, uses session if available)
|
||||||
|
* @param string|null $details Additional details/metadata (will be JSON encoded if array)
|
||||||
|
* @return bool True if logged successfully, false otherwise
|
||||||
|
*/
|
||||||
|
public static function log(
|
||||||
|
string $action,
|
||||||
|
string $status,
|
||||||
|
?int $userId = null,
|
||||||
|
?string $details = null
|
||||||
|
): bool {
|
||||||
|
try {
|
||||||
|
// Get user ID from session if not provided
|
||||||
|
if ($userId === null) {
|
||||||
|
$userId = $_SESSION['user_id'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get client IP address
|
||||||
|
$ipAddress = self::getClientIp();
|
||||||
|
|
||||||
|
// Convert array details to JSON
|
||||||
|
if (is_array($details)) {
|
||||||
|
$details = json_encode($details);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get database connection
|
||||||
|
$db = DatabaseService::getInstance();
|
||||||
|
$conn = $db->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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ require_once 'google-client/vendor/autoload.php'; // Add this line for Google Cl
|
|||||||
use Middleware\CsrfMiddleware;
|
use Middleware\CsrfMiddleware;
|
||||||
use Middleware\RateLimitMiddleware;
|
use Middleware\RateLimitMiddleware;
|
||||||
use Services\AuthenticationService;
|
use Services\AuthenticationService;
|
||||||
|
use Services\AuditLogger;
|
||||||
|
|
||||||
// Check if connection is established
|
// Check if connection is established
|
||||||
if (!$conn) {
|
if (!$conn) {
|
||||||
@@ -70,6 +71,8 @@ if (isset($_GET['code'])) {
|
|||||||
AuthenticationService::regenerateSession();
|
AuthenticationService::regenerateSession();
|
||||||
// Reset rate limit on successful login
|
// Reset rate limit on successful login
|
||||||
RateLimitMiddleware::reset('login');
|
RateLimitMiddleware::reset('login');
|
||||||
|
// Log successful registration via Google
|
||||||
|
AuditLogger::logLogin($email, true);
|
||||||
// echo json_encode(['status' => 'success', 'message' => 'Google login successful']);
|
// echo json_encode(['status' => 'success', 'message' => 'Google login successful']);
|
||||||
header("Location: index.php");
|
header("Location: index.php");
|
||||||
exit();
|
exit();
|
||||||
@@ -89,6 +92,8 @@ if (isset($_GET['code'])) {
|
|||||||
AuthenticationService::regenerateSession();
|
AuthenticationService::regenerateSession();
|
||||||
// Reset rate limit on successful login
|
// Reset rate limit on successful login
|
||||||
RateLimitMiddleware::reset('login');
|
RateLimitMiddleware::reset('login');
|
||||||
|
// Log successful Google login
|
||||||
|
AuditLogger::logLogin($email, true);
|
||||||
// echo json_encode(['status' => 'success', 'message' => 'Google login successful']);
|
// echo json_encode(['status' => 'success', 'message' => 'Google login successful']);
|
||||||
header("Location: index.php");
|
header("Location: index.php");
|
||||||
exit();
|
exit();
|
||||||
@@ -122,12 +127,14 @@ if (isset($_POST['email']) && isset($_POST['password'])) {
|
|||||||
if (empty($email) || empty($password)) {
|
if (empty($email) || empty($password)) {
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Please enter both email and password.']);
|
echo json_encode(['status' => 'error', 'message' => 'Please enter both email and password.']);
|
||||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
RateLimitMiddleware::incrementAttempt('login', 900);
|
||||||
|
AuditLogger::logLogin($email ?? 'unknown', false, 'Empty email or password');
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']);
|
echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']);
|
||||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
RateLimitMiddleware::incrementAttempt('login', 900);
|
||||||
|
AuditLogger::logLogin($email ?? 'unknown', false, 'Invalid email format');
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +159,7 @@ if (isset($_POST['email']) && isset($_POST['password'])) {
|
|||||||
if ($row['is_verified'] == 0) {
|
if ($row['is_verified'] == 0) {
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Your account is not verified. Please check your email for the verification link.']);
|
echo json_encode(['status' => 'error', 'message' => 'Your account is not verified. Please check your email for the verification link.']);
|
||||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
RateLimitMiddleware::incrementAttempt('login', 900);
|
||||||
|
AuditLogger::logLogin($email, false, 'Account not verified');
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,15 +172,19 @@ if (isset($_POST['email']) && isset($_POST['password'])) {
|
|||||||
AuthenticationService::regenerateSession();
|
AuthenticationService::regenerateSession();
|
||||||
// Reset rate limit on successful login
|
// Reset rate limit on successful login
|
||||||
RateLimitMiddleware::reset('login');
|
RateLimitMiddleware::reset('login');
|
||||||
|
// Log successful email/password login
|
||||||
|
AuditLogger::logLogin($email, true);
|
||||||
echo json_encode(['status' => 'success', 'message' => 'Successful Login']);
|
echo json_encode(['status' => 'success', 'message' => 'Successful Login']);
|
||||||
} else {
|
} else {
|
||||||
// Password is incorrect - increment rate limit
|
// Password is incorrect - increment rate limit
|
||||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
RateLimitMiddleware::incrementAttempt('login', 900);
|
||||||
|
AuditLogger::logLogin($email, false, 'Invalid password');
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid password.']);
|
echo json_encode(['status' => 'error', 'message' => 'Invalid password.']);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// User does not exist - still increment rate limit to prevent email enumeration
|
// User does not exist - still increment rate limit to prevent email enumeration
|
||||||
RateLimitMiddleware::incrementAttempt('login', 900);
|
RateLimitMiddleware::incrementAttempt('login', 900);
|
||||||
|
AuditLogger::logLogin($email, false, 'User not found');
|
||||||
echo json_encode(['status' => 'error', 'message' => 'User with that email does not exist.']);
|
echo json_encode(['status' => 'error', 'message' => 'User with that email does not exist.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user