db = $db; } /** * @throws RandomException */ public function newUser($username, $password, $email){ if ($this->check_existing_user($username, $email)){ throw new RandomException("User already exists"); } $this->username = $username; $this->email = $email; $password2 = password_hash($password, PASSWORD_DEFAULT); $this->password = $password2; $this->location = ""; $this->bio = ""; $this->created = date('Y-m-d H:i:s'); $this->updated = date('Y-m-d H:i:s'); $this->verified = 0; $this->role = "user"; $this->img = ""; $this->api_key = bin2hex(random_bytes(32)); $stmt = $this->db->prepare("INSERT INTO users (username, password, email, img) VALUES (?, ?, ?, ?)"); $stmt->bind_param("ssss", $this->username, $this->password, $this->email, $this->img); $stmt->execute(); $stmt->close(); } private function check_existing_user($username, $email){ $stmt = $this->db->prepare("SELECT * FROM users WHERE username = ? OR email = ?"); $stmt->bind_param("ss", $username, $email); $stmt->execute(); $result = $stmt->get_result(); $user = $result->fetch_assoc(); $stmt->close(); return $user; } 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(); } }