Add customer_password_reset_keys_o class with token generation, validation, and password reset functionality.

This commit is contained in:
Jeppe Bundgaard
2026-01-19 10:41:42 +01:00
parent 574e206698
commit eb6cc72bf5
@@ -0,0 +1,143 @@
<?php
namespace objects;
use classes\db;
use classes\object_property;
use Exception;
use Random\RandomException;
use traits\db_object_t;
class customer_password_reset_keys_o extends db
{
use db_object_t;
public object_property $customer_id;
public object_property $note;
public object_property $token;
public object_property $created_at;
public object_property $updated_at;
public object_property $deleted_at;
const TOKEN_LENGTH = 32;
const TOKEN_EXPIRY_SECONDS = 3600; // 1 hour
public function structure(): void
{
$this->setTable('customer_password_reset_keys');
}
/**
* Add a new customer reset key
* @param array $data The properties
* @returns void
* @throws Exception If the object was not created successfully
*/
public function add(array $data): void
{
$tmp_id = self::add_object($data);
$this->id = $tmp_id;
self::getObjectProperties();
self::objectChanged();
}
/**
* Generate a unique token
* @param int $length The length of the token
* @return string The generated token
* @throws RandomException
*/
public static function generateToken(int $length = self::TOKEN_LENGTH): string
{
return bin2hex(random_bytes($length / 2));
}
/**
* Find a valid reset key by token
* @param string $token The token to search for
* @return customer_password_reset_keys_o|null The found reset key or null if not found
* @throws Exception
*/
public function findValidByToken(string $token): ?customer_password_reset_keys_o
{
global $db;
// Sanitize input to conform to the expected format
$token = preg_replace('/[^a-f0-9]/', '', strtolower($token));
if (strlen($token) !== self::TOKEN_LENGTH) {
return null;
}
// Query the database for a valid token
$current_time = date('Y-m-d H:i:s');
$sql = "SELECT id FROM $this->table WHERE token = '" . $db->escape_string($token) . "' AND deleted_at IS NULL AND created_at >= DATE_SUB('$current_time', INTERVAL " . self::TOKEN_EXPIRY_SECONDS . " SECOND) LIMIT 1";
$result = $db->query($sql);
if ($result->num_rows === 0) {
return null;
}
$row = $result->fetch_assoc();
$this->id = (int)$row['id'];
$this->getObjectProperties();
return $this;
}
/**
* Check if the token is valid and not expired
* @return bool True if the token is valid, false otherwise
* @throws Exception
*/
public function isValidToken(): bool
{
self::requireSelected();
$created_at = strtotime($this->created_at->value());
$current_time = time();
return (
($current_time - $created_at) <= self::TOKEN_EXPIRY_SECONDS) &&
($this->deleted_at->value() === null
);
}
/**
* Require that the token is valid and not expired
* @throws Exception If the token is invalid or expired
*/
public function requireValidToken(): void
{
self::requireSelected();
if (!$this->isValidToken()) {
throw new Exception('Invalid or expired token');
}
}
/**
* Set the password of the customer associated with this reset key
* @param string $new_password The new password to set
* @return void
* @throws Exception If the token is invalid or expired
*/
public function setPassword(string $new_password): void
{
self::requireSelected();
self::requireValidToken();
$customer = new users_o();
$customer->getUserByCustomerNumber((int)$this->customer_id->value());
$customer->requireSelected();
$customer->setPassword($new_password);
$this->delete();
}
/**
* @return void
*/
public function getObjectProperties(): void
{
$this->customer_id = new object_property($this->table, $this->id, 'customer_id', 'int', false);
$this->note = new object_property($this->table, $this->id, 'note', 'string', false);
$this->token = new object_property($this->table, $this->id, 'token', 'string', false);
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'string', false);
$this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'string', false);
$this->deleted_at = new object_property($this->table, $this->id, 'deleted_at', 'string', false);
}
public function objectChanged(): void
{
//TODO: Add cache invalidation
}
}