Files
api/services/nginx/app/objects/subusers_o.php
T
Jeppe Bundgaard 0f5156cdac Add subuser authentication via password and session generation
- Add `/subusers/auth/password` route for subuser authentication using password or other username types (phone, ID, etc.).
- Implement `getSubuserByUsername` in `subusers_o` for retrieving subusers by username.
- Introduce `generateSession` in `subusers_o` for creating and caching session tokens with expiration logic.
2026-02-11 14:51:17 +01:00

233 lines
9.4 KiB
PHP

<?php
namespace objects;
use classes\db;
use classes\object_property;
use Exception;
use Random\RandomException;
use traits\db_object_t;
class subusers_o extends db
{
use db_object_t;
public object_property $username;
public object_property $password;
public object_property $name;
public object_property $email;
public object_property $phone_country_code;
public object_property $phone;
public object_property $created_at;
public object_property $updated_at;
public object_property $suspended_at;
public function structure(): void
{
$this->setTable('subusers');
}
public function objectChanged(): void
{
// No need to invalidate the cache, since the plate_scans object is not cached
}
public function getObjectProperties(): void
{
$this->username = new object_property($this->table, $this->id, 'username', 'string');
$this->password = new object_property($this->table, $this->id, 'password', 'string');
$this->name = new object_property($this->table, $this->id, 'name', 'string');
$this->email = new object_property($this->table, $this->id, 'email', 'string');
$this->phone_country_code = new object_property($this->table, $this->id, 'phone_country_code', 'int');
$this->phone = new object_property($this->table, $this->id, 'phone', 'int');
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'timestamp');
$this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'timestamp');
$this->suspended_at = new object_property($this->table, $this->id, 'suspended_at', 'timestamp');
}
/**
* 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|null $username
* @param string|null $password
* @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
{
global $db, $response;
try {
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)) {
throw new Exception('Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number.');
}
// Hash the password
$password = password_hash($password, PASSWORD_DEFAULT);
}
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([
...(!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
...(!empty($password) ? ['password' => (string)$password] : []),
]);
// Get the id of the new record
$this->id = (int)$tmp;
// Set the values of the object properties
$this->getObjectProperties();
return $this;
} catch (Exception $e) {
$response->error($e->getMessage());
}
}
/**
* Set the password for the subuser. The password 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 $password
* @return self
* @throws Exception If the password does not meet the requirements or if no subuser is selected.
*/
public function setPassword(string $password): self
{
self::requireSelected();
$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;
}
public function invalidateSetupToken(string $token): void
{
$object_id = 'subuser_setup_token';
$cache_key = 'setup_token:' . $token;
$this->deleteCached($cache_key, $object_id);
}
/**
* @throws Exception
*/
public function getSubuserByUsername(string $username): ?subusers_o
{
global $db;
$username = $db->escape_string($username);
$tmp = self::getFieldsWhere([
'username' => $username,
], ['id']);
if (count($tmp) === 0) {
return null;
}
$subuser = (new subusers_o())->select((int)$tmp[0]['id']);
$subuser->getObjectProperties();
return $subuser;
}
/**
* @throws RandomException
* @throws Exception
*/
public function generateSession(): string
{
self::requireSelected();
$session_token = bin2hex(random_bytes(32));
$this->cache('session_token:' . $session_token, $this->id, 'subuser_sessions');
$this->setCachedExpiration('session_token:' . $session_token, 7 * 24 * 60 * 60, 'subuser_sessions'); // Set the session to expire after 7 days
return $session_token;
}
}