From a92781cceb517ee1a394a53803574ee48a956864 Mon Sep 17 00:00:00 2001 From: Jepp9350 <2jepp9350@gmail.com> Date: Wed, 23 Apr 2025 10:52:13 +0200 Subject: [PATCH] Add user email and phone support with security routes Introduced support for user email and phone management, including phone country code and email validation. Added API endpoints for email change and password validation to enhance account security features. Updated Stripe processor logic for better external ID handling. --- .../objects/collected_order_invoices_o.php | 3 +- services/nginx/app/objects/users_o.php | 61 ++++++++++++++ .../nginx/app/routes/userSecurityRoute.php | 84 +++++++++++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 services/nginx/app/routes/userSecurityRoute.php diff --git a/services/nginx/app/objects/collected_order_invoices_o.php b/services/nginx/app/objects/collected_order_invoices_o.php index 3c9074b3..eb2824e7 100644 --- a/services/nginx/app/objects/collected_order_invoices_o.php +++ b/services/nginx/app/objects/collected_order_invoices_o.php @@ -115,7 +115,8 @@ class collected_order_invoices_o extends db $tmp['economic_invoice_booked_id'] = self::isBooked() ? self::getInvoiceBookedId() : null; } // If the processor is Stripe, get the stripe details - if ((int)$this->processor->value() === STRIPE_PROCESSOR || str_starts_with($this->external_id->value(), 'pi_')) { + $isExternalIdStripe = !empty($this->external_id->value()) && str_starts_with($this->external_id->value(), 'pi_'); + if ((int)$this->processor->value() === STRIPE_PROCESSOR || $isExternalIdStripe) { // Get the stripe details $stripe = new stripe(); $stripe_details = $stripe->payment_intents->get( diff --git a/services/nginx/app/objects/users_o.php b/services/nginx/app/objects/users_o.php index c5c6feb4..3c497183 100644 --- a/services/nginx/app/objects/users_o.php +++ b/services/nginx/app/objects/users_o.php @@ -32,12 +32,16 @@ class users_o extends db public user_price_overrides_o $price_overrides; public language_pack_en_us $language_pack; protected object_property $password; + protected object_property $phone_country_code; + protected object_property $phone; + protected object_property $email; public function structure(): void { $this->setTable('users'); } + public function edit(int $id, string $customer_number, string|null $role, string|null $password, string|null $display_name): void { global $db; @@ -79,6 +83,9 @@ class users_o extends db $this->display_name = new object_property($this->table, $this->id, 'display_name', 'string', false); $this->password = new object_property($this->table, $this->id, 'password', 'string', true); $this->group_id = new object_property($this->table, $this->id, 'group_id', 'int', true); + $this->phone_country_code = new object_property($this->table, $this->id, 'phone_country_code', 'int', false); + $this->phone = new object_property($this->table, $this->id, 'phone', 'int', false); + $this->email = new object_property($this->table, $this->id, 'email', '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->keys = (new user_key_value_pairs_o())->setUser($this->id); @@ -228,12 +235,22 @@ class users_o extends db public function asArray(): array { + $phone_country_code = $this->phone_country_code->value(); + $phone_country_code = $phone_country_code === null ? null : (int)$phone_country_code; + $phone = $this->phone->value(); + $phone = $phone === null ? null : (int)$phone; + // Create the array with the object properties $array = [ 'id' => (int)$this->id, 'customer_number' => (int)$this->customer_number->value(), 'customer_name' => $this->getCustomerName($this->customer_number->value()), 'display_name' => $this->display_name->value(), 'group_id' => (int)$this->group_id->value(), + 'phone' => [ + 'country_code' => $phone_country_code, + 'number' => $phone, + ], + 'email' => $this->email->value(), 'created_at' => $this->created_at->value(), 'updated_at' => $this->updated_at->value(), ]; @@ -984,4 +1001,48 @@ class users_o extends db // Return the customer numbers return array_unique($customer_numbers); } + + /** + * Check if the password matches the hashed password + * @param string $password_to_check_against The password (plain text) to check against the hashed password + * @return bool True if the password matches, false otherwise + * @throws Exception If the user is not selected + * @throws Exception If the user does not have a password + */ + public function passwordMatches(string $password_to_check_against): bool + { + // Require the user to be selected + self::requireSelected(); + // Check if the user has a password + if (empty($this->password->value())) { + throw new Exception('The user does not have a password, unable to check if the password matches'); + } + // Check if the password matches + return password_verify( + (string)$password_to_check_against, + (string)$this->password->value() + ); + } + + /** + * Set the email for the user + * @param string $email The email address to set + * @throws Exception If the user is not selected + * @throws Exception If the email address is invalid + * @throws Exception If the email address is already in use + */ + public function setEmail(string $email): void + { + self::requireSelected(); + // Check if the email is valid + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + throw new Exception('Invalid email address'); + } + // Check if the email is already in use + if (self::countRowsWhere(['email' => $email]) > 0) { + throw new Exception('Email address already in use'); + } + // Set the email + $this->email->set($email); + } } \ No newline at end of file diff --git a/services/nginx/app/routes/userSecurityRoute.php b/services/nginx/app/routes/userSecurityRoute.php new file mode 100644 index 00000000..00c80387 --- /dev/null +++ b/services/nginx/app/routes/userSecurityRoute.php @@ -0,0 +1,84 @@ +post('/account/security/change-email', function () { + // Require the user to be logged in + global $response; + self::requirePermission('user_security_change_email'); + $user = (new authentication())->get_user(); + if (!$user) { + (new logs_o())->add('user_security', 'global', 0, 0, 'USER_SECURITY_CHANGE_EMAIL', 'User not logged in'); + $response->error('Invalid session', 400); + } + // Require the email, and password parameters + self::requireParameters(['email', 'password']); + // Check if the email is valid + $email = (string)self::getParameter('email'); + self::requireMinLength('email', 5); + self::requireMaxLength('email', 255); + self::requireType($email, self::type_string()); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + (new logs_o())->add('user_security', 'global', 0, $user->id, 'USER_SECURITY_CHANGE_EMAIL', 'Invalid email'); + $response->error('Invalid email', 400); + } + // Validate the password + $password = (string)self::getParameter('password'); + self::requireMinLength('password', 5); + self::requireMaxLength('password', 255); + self::requireType($password, self::type_string()); + if (!$user->passwordMatches($password)) { + (new logs_o())->add('user_security', 'global', 0, $user->id, 'USER_SECURITY_CHANGE_EMAIL', 'Invalid password'); + $response->error('Invalid password', 400); + } else { + // Change the email + $user->setEmail($email); + (new logs_o())->add('user_security', 'global', 0, $user->id, 'USER_SECURITY_CHANGE_EMAIL', 'Email changed'); + $response->success(['message' => 'Email changed']); + } + }, + [ + 'user_security_change_email' => 'Change the email address of the user', + ] + ); + + $this->post('/account/security/validate-password', function () { + // Require the user to be logged in + global $response; + self::requirePermission('user_security_validate_password'); + $user = (new authentication())->get_user(); + if (!$user) { + (new logs_o())->add('user_security', 'global', 0, 0, 'USER_SECURITY_VALIDATE_PASSWORD', 'User not logged in'); + $response->error('Invalid session', 400); + } + // Require the password parameter + self::requireParameters(['password']); + // Validate the password + $password = (string)self::getParameter('password'); + self::requireMinLength('password', 5); + self::requireMaxLength('password', 255); + self::requireType($password, self::type_string()); + if (!$user->passwordMatches($password)) { + (new logs_o())->add('user_security', 'global', 0, $user->id, 'USER_SECURITY_VALIDATE_PASSWORD', 'Invalid password'); + $response->error('Invalid password', 400); + } else { + (new logs_o())->add('user_security', 'global', 0, $user->id, 'USER_SECURITY_VALIDATE_PASSWORD', 'Password validated'); + $response->success(['message' => 'Password validated']); + } + }, + [ + 'user_security_validate_password' => 'Validate the password of the user', + ] + ); + } +} \ No newline at end of file