177 lines
No EOL
6.3 KiB
PHP
177 lines
No EOL
6.3 KiB
PHP
<?php
|
||
|
||
namespace DJMixHosting;
|
||
|
||
use Random\RandomException;
|
||
|
||
Class User{
|
||
|
||
private $db;
|
||
private $id;
|
||
private $username;
|
||
private $email;
|
||
private $location;
|
||
private $bio;
|
||
private $created;
|
||
private $updated;
|
||
private $verified;
|
||
private $role;
|
||
private $img;
|
||
private $api_key;
|
||
|
||
public function __construct($db){
|
||
$this->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();
|
||
}
|
||
|
||
|
||
} |