I'm in a rush to release so I am adding features that are needed to make it usable.

This commit is contained in:
Cody Cook 2025-02-17 22:03:33 -08:00
commit 4c2857b445
25 changed files with 2475 additions and 3475 deletions

View file

@ -61,8 +61,116 @@ Class User{
return $user;
}
public function login(mixed $username, mixed $password)
public function login($email, $password)
{
// Retrieve user record by email
$stmt = $this->db->prepare("SELECT * FROM users WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();
$user_data = $result->fetch_assoc();
$stmt->close();
// Check login_attempts table for lockout status
$stmt = $this->db->prepare("SELECT * FROM login_attempts WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$attempt_data = $stmt->get_result()->fetch_assoc();
$stmt->close();
$current_time = new \DateTime();
if ($attempt_data && !empty($attempt_data['lockout_until'])) {
$lockout_until = new \DateTime($attempt_data['lockout_until']);
if ($current_time < $lockout_until) {
return "Account locked until " . $lockout_until->format('Y-m-d H:i:s') . ". Please try again later.";
}
}
// If no user record found, still update login_attempts to mitigate enumeration issues
if (!$user_data) {
$this->updateFailedAttempt($email);
return "Invalid email or password.";
}
// Verify the password using password_verify
if (password_verify($password, $user_data['password'])) {
// Successful login clear login attempts and set session variables
$this->resetLoginAttempts($email);
$_SESSION['user'] = [
'id' => $user_data['id'],
'email' => $user_data['email'],
'username' => $user_data['username'],
'role' => $user_data['isAdmin'] ? 'admin' : 'user'
];
return true;
} else {
$attempts = $this->updateFailedAttempt($email);
return "Invalid email or password. Attempt $attempts of 3.";
}
}
/**
* Update (or create) a record in the login_attempts table for a failed attempt.
* If attempts reach 3, set a lockout that doubles each time.
* Returns the current number of attempts.
*/
private function updateFailedAttempt($email)
{
// Check for an existing record
$stmt = $this->db->prepare("SELECT * FROM login_attempts WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$record = $stmt->get_result()->fetch_assoc();
$stmt->close();
$current_time = new \DateTime();
if ($record) {
$attempts = $record['attempts'] + 1;
$lockouts = $record['lockouts'];
if ($attempts >= 3) {
// Increment lockouts and calculate the new lockout duration:
// Duration in minutes = 30 * 2^(lockouts)
$lockouts++;
$duration = 30 * pow(2, $lockouts - 1);
$lockout_until = clone $current_time;
$lockout_until->modify("+{$duration} minutes");
// Reset attempts to 0 on lockout
$attempts = 0;
$stmt = $this->db->prepare("UPDATE login_attempts SET attempts = ?, lockouts = ?, last_attempt = NOW(), lockout_until = ? WHERE email = ?");
$lockout_until_str = $lockout_until->format('Y-m-d H:i:s');
$stmt->bind_param("iiss", $attempts, $lockouts, $lockout_until_str, $email);
$stmt->execute();
$stmt->close();
} else {
$stmt = $this->db->prepare("UPDATE login_attempts SET attempts = ?, last_attempt = NOW() WHERE email = ?");
$stmt->bind_param("is", $attempts, $email);
$stmt->execute();
$stmt->close();
}
return $attempts;
} else {
// Create a new record for this email
$attempts = 1;
$lockouts = 0;
$stmt = $this->db->prepare("INSERT INTO login_attempts (email, attempts, lockouts, last_attempt) VALUES (?, ?, ?, NOW())");
$stmt->bind_param("sii", $email, $attempts, $lockouts);
$stmt->execute();
$stmt->close();
return $attempts;
}
}
/**
* Reset the login_attempts record for the given email.
*/
private function resetLoginAttempts($email)
{
$stmt = $this->db->prepare("DELETE FROM login_attempts WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$stmt->close();
}