diff --git a/services/nginx/app/objects/customer_password_reset_keys_o.php b/services/nginx/app/objects/customer_password_reset_keys_o.php new file mode 100644 index 00000000..c2b413c9 --- /dev/null +++ b/services/nginx/app/objects/customer_password_reset_keys_o.php @@ -0,0 +1,143 @@ +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 + } +} \ No newline at end of file