Add subuser management route and token-based setup flow

- Introduce `/subusers` route for creating subusers and linking them to companies via phone number validation.
- Add token-based subuser setup flow including token generation, validation, and expiration handling.
- Extend `subusers_o` with methods for subuser lookup, token handling, and secure password management.
- Implement enhanced input validation for subuser creation, ensuring stricter checks for name, username, and email fields.
This commit is contained in:
Jeppe Bundgaard
2026-02-10 16:20:02 +01:00
parent 130cacddaf
commit 9862a1856e
2 changed files with 263 additions and 14 deletions
+100 -14
View File
@@ -5,6 +5,7 @@ namespace objects;
use classes\db;
use classes\object_property;
use Exception;
use Random\RandomException;
use traits\db_object_t;
class subusers_o extends db
@@ -47,20 +48,18 @@ class subusers_o extends db
/**
* Add a new subuser to the database. The password is optional, but if it is set, it must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number. The password will be hashed before being stored in the database.
* @param string $username
* @param string|null $username
* @param string|null $password
* @param string $name
* @param string $email
* @param string|null $name
* @param string|null $email
* @param int $phone_country_code
* @param int $phone
* @return $this
*/
public function add(string $username, ?string $password, string $name, string $email, int $phone_country_code, int $phone): subusers_o
public function add(?string $username, ?string $password, ?string $name, ?string $email, int $phone_country_code, int $phone): subusers_o
{
global $db, $response;
try {
// Avoid SQL injection
$username = $db->escape_string($username);
if (!empty($password)) {
// Validate the password (at least 8 characters, at least one uppercase letter, at least one lowercase letter, at least one number)
if (!preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/', $password)) {
@@ -69,19 +68,37 @@ class subusers_o extends db
// Hash the password
$password = password_hash($password, PASSWORD_DEFAULT);
}
$name = $db->escape_string($name);
$email = $db->escape_string($email);
if (!empty($email)) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new Exception('Invalid email address.');
}
$email = $db->escape_string($email);
}
if (!empty($username)) {
if (strlen($username) < 3 || strlen($username) > 50) {
throw new Exception('Username must be between 3 and 50 characters long.');
}
$username = $db->escape_string($username);
}
if (!empty($name)) {
if (strlen($name) < 2 || strlen($name) > 100) {
throw new Exception('Name must be between 2 and 100 characters long.');
}
$name = $db->escape_string($name);
}
// Escape the other parameters
$phone_country_code = $db->escape_string($phone_country_code);
$phone = $db->escape_string($phone);
// Create a new record in the database
$tmp = $this->add_object([
'username' => $username,
'name' => $name,
'email' => $email,
'phone_country_code' => $phone_country_code,
'phone' => $phone,
...(!empty($username) ? ['username' => $username] : []),
...(!empty($name) ? ['name' => $name] : []),
...(!empty($email) ? ['email' => $email] : []),
'phone_country_code' => (int)$phone_country_code,
'phone' => (int)$phone,
// Add the password only if it is set
'password' => !empty($password) ? $password : null,
...(!empty($password) ? ['password' => (string)$password] : []),
]);
// Get the id of the new record
@@ -106,4 +123,73 @@ class subusers_o extends db
$this->password->set((string)password_hash($password, PASSWORD_DEFAULT));
return $this;
}
public function getSubuserByPhone(int $phone_country_code, int $phone): ?subusers_o
{
$tmp = self::getFieldsWhere([
'phone_country_code' => $phone_country_code,
'phone' => $phone,
], ['id']);
if (count($tmp) === 0) {
return null;
}
$subuser = (new subusers_o())->select((int)$tmp[0]['id']);
$subuser->getObjectProperties();
return $subuser;
}
/**
* Generate a setup token for the subuser. The setup token is a random string that can be used to link the subuser to a company user. The setup token is valid for 24 hours and can only be used once.
* @return string The setup token
* @throws RandomException If there was an error generating the random bytes for the token
* @throws Exception If no subuser is selected
*/
public function generateSetupToken(): string
{
self::requireSelected();
try {
$token = bin2hex(random_bytes(16));
} catch (Exception $e) {
throw new RandomException('Error generating random bytes for setup token', 0, $e);
}
$cache_key = 'setup_token:' . $token;
$cashe_object_id = 'subuser_setup_token';
$this->cache($cache_key, $this->id, $cashe_object_id);
$this->setCachedExpiration($cache_key, 24 * 60 * 60, $cashe_object_id); // Set the cache expiration to 24 hours
return $token;
}
/**
* Get the subuser id by the setup token. If the token is valid, it will return the subuser id and delete the token from the cache. If the token is invalid or expired, it will return null.
* @param string $token The setup token
* @return int|null The subuser id or null if the token is invalid or expired
* @throws Exception If there was an error getting the subuser id from the cache
*/
public function getSubuserIdBySetupToken(string $token): ?int
{
$cache_key = 'setup_token:' . $token;
$cache_object_id = 'subuser_setup_token';
$subuser_id = $this->getCached($cache_key, $cache_object_id);
if ($subuser_id === null) {
return null;
}
return $subuser_id;
}
/**
* Get the subuser by the setup token. If the token is valid, it will return the subuser object and delete the token from the cache. If the token is invalid or expired, it will return null.
* @param string $token The setup token
* @return subusers_o|null The subuser object or null if the token is invalid or expired
* @throws Exception If there was an error getting the subuser object from the cache
*/
public function getSubuserBySetupToken(string $token): ?subusers_o
{
$subuser_id = $this->getSubuserIdBySetupToken($token);
if ($subuser_id === null) {
return null;
}
$subuser = (new subusers_o())->select((int)$subuser_id);
$subuser->getObjectProperties();
return $subuser;
}
}