error($error_message, 400); } } public function run(): void { // ============================= // Subuser Grant Management // ============================= $this->get('/subusers/grants', function () { global $response; /** Permissions (subuser-aware) */ $permission_own = self::definePermission('list_own_subuser_grants', subusers_permission_node_key::SUBUSERS_LIST); $permission_other = self::definePermission('list_subuser_grants'); $has_permission_other = self::hasPermission($permission_other); // Optional filters: customer_number, subuser_id $filters = ['deleted_at' => null]; $hasFilter = false; $requestedCustomer = null; if (self::isParametersSet(['customer_number'])) { $customer_number = (int)self::getParameter('customer_number'); self::requireType($customer_number, self::type_int()); $filters['billing_customer_number'] = $customer_number; $requestedCustomer = (int)$customer_number; $hasFilter = true; } if (self::isParametersSet(['subuser_id'])) { $subuser_id = (int)self::getParameter('subuser_id'); self::requireType($subuser_id, self::type_int()); $filters['subuser'] = $subuser_id; $hasFilter = true; } // Effective customer context (classic or subuser header) $effectiveCustomer = self::resolveEffectiveCustomerNumber(); // Allow via own-scope or admin-scope self::allowOwnOrDepartmentAccess( $permission_own, $permission_other, $requestedCustomer !== null ? (int)$requestedCustomer : ($effectiveCustomer !== null ? (int)$effectiveCustomer : null), null, null, 'You do not have permission to list subuser grants.' ); // If not admin/department permission, force restrict to effective customer if (!$has_permission_other) { if ($effectiveCustomer === null) { $response->error('Missing customer context. Subusers must provide X-Customer-Number header.', 403); } $filters['billing_customer_number'] = (int)$effectiveCustomer; $hasFilter = true; // ensure we don't fail below } // Fallback: try to infer from session for classic users if (!$hasFilter) { $user = (new authentication())->get_user(); if ($user && !empty($user->customer_number->value())) { $filters['billing_customer_number'] = (int)$user->customer_number->value(); $hasFilter = true; } } if (!$hasFilter) { $response->error('You must provide at least one of: customer_number or subuser_id', 400); } $paginatedResponse = (new subuser_grants_o())->listObjectsWithPaginationIfSet(function ($o) { $o = (object)$o; $subuser = (new subusers_o())->select((int)$o->subuser); return [ 'id' => $o->id, 'billing_customer_number' => $o->billing_customer_number, 'subuser' => $o->subuser, 'name' => $subuser->name->value(), 'enabled' => $o->enabled, 'note' => $o->note, 'permissions' => json_decode($o->permissions, true) ?: [], 'created_at' => $o->created_at, 'updated_at' => $o->updated_at, ]; }, $filters); $response->success($paginatedResponse); }, [ 'list_subuser_grants' => 'List subuser grants for any customer (admin).', 'list_own_subuser_grants' => 'List subuser grants for the effective customer. Subusers require node: SUBUSERS_LIST and X-Customer-Number header.' ]); $this->post('/subusers/grants', function () { global $response; /** Permissions (subuser-aware) */ $permission_own = self::definePermission('add_own_subusers', subusers_permission_node_key::SUBUSERS_ADD); $permission_other = self::definePermission('manage_subuser_grants'); self::requireParameters(['customer_number', 'subuser_id']); $customer_number = (int)self::getParameter('customer_number'); $subuser_id = (int)self::getParameter('subuser_id'); self::requireType($customer_number, self::type_int()); self::requireType($subuser_id, self::type_int()); // Enforce own-vs-admin access using target customer number for context self::allowOwnOrDepartmentAccess( $permission_own, $permission_other, (int)$customer_number, null, null, 'You do not have permission to create subuser grants for this customer.' ); $enabled = true; if (self::isParametersSet(['enabled'])) { $enabled = (bool)filter_var(self::getParameter('enabled'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($enabled === null) { $enabled = true; } } $note = null; if (self::isParametersSet(['note'])) { $note = (string)self::getParameter('note'); self::requireType($note, self::type_string()); self::requireMaxLength('note', 65535); } $permissions = null; if (self::isParametersSet(['permissions'])) { $raw = self::getParameter('permissions'); // Expect array (already parsed) or JSON string if (is_string($raw)) { $decoded = json_decode($raw, true); if (!is_array($decoded)) { $response->error('Invalid permissions payload', 400); } $permissions = $decoded; } elseif (is_array($raw)) { $permissions = $raw; } else { $response->error('Invalid permissions type', 400); } // Validate each permission is a known key foreach ($permissions as $perm) { if (!is_string($perm) || subusers_permission_node_key::tryFrom($perm) === null) { $response->error('Unknown permission key: ' . (string)$perm, 400); } } } try { $grant = (new subuser_grants_o())->add($customer_number, $subuser_id, (bool)$enabled, $note, $permissions); $response->success(['grant' => $grant->asArray()]); } catch (Exception $e) { $response->error('Failed to add subuser grant', 500); } }, [ 'manage_subuser_grants' => 'Create subuser grants for any customer (admin).', 'add_own_subusers' => 'Create subuser grants for own customer. Subusers require node: SUBUSERS_ADD and X-Customer-Number header.' ]); $this->put('/subusers/grants', function () { global $response; /** Permissions (subuser-aware) */ $permission_own = self::definePermission('edit_own_subusers', subusers_permission_node_key::SUBUSERS_EDIT); $permission_other = self::definePermission('manage_subuser_grants'); self::requireParameters(['id']); self::requireType(self::getParameter('id'), self::type_int()); $grant = (new subuser_grants_o())->select((int)self::getParameter('id')); if (!$grant->exists()) { $response->error('Grant not found', 404); } // Enforce own-vs-admin using the grant's customer number $targetCustomer = (int)$grant->billing_customer_number->value(); self::allowOwnOrDepartmentAccess( $permission_own, $permission_other, $targetCustomer, null, null, 'You do not have permission to modify this subuser grant.' ); // Update fields provided in the request if (self::isParametersSet(['enabled'])) { $enabled = (bool)filter_var(self::getParameter('enabled'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($enabled === null) { $enabled = false; } $grant->enabled->set($enabled); } if (self::isParametersSet(['note'])) { $note = (string)self::getParameter('note'); self::requireType($note, self::type_string()); self::requireMaxLength('note', 65535); $grant->note->set($note); } if (self::isParametersSet(['permissions'])) { $raw = self::getParameter('permissions'); // Expect array (already parsed) or JSON string if (is_string($raw)) { $decoded = json_decode($raw, true); if (!is_array($decoded)) { $response->error('Invalid permissions payload', 400); } $permissions = $decoded; } elseif (is_array($raw)) { $permissions = $raw; } else { $response->error('Invalid permissions type', 400); } // Validate each permission is a known key foreach ($permissions as $perm) { if (!is_string($perm) || subusers_permission_node_key::tryFrom($perm) === null) { $response->error('Unknown permission key: ' . (string)$perm, 400); } } $grant->permissions->set($permissions); } $response->success($grant->asArray()); }, [ 'manage_subuser_grants' => 'Edit subuser grants for any customer (admin).', 'edit_own_subusers' => 'Edit subuser grants for own customer. Subusers require node: SUBUSERS_EDIT and X-Customer-Number header.' ] ); $this->get('/subusers/permission-nodes', function () { global $response; // Build groups $groups = [ new subusers_permission_nodes_bookings(), new subusers_permission_nodes_orders(), new subusers_permission_nodes_selfserve(), new subusers_permission_nodes_subusers(), new subusers_permission_nodes_vehicles(), ]; $out = []; foreach ($groups as $group) { $nodes = []; foreach ($group->nodes as $key => $node) { $nodes[] = [ 'key' => $key, 'name' => $node->name, 'description' => $node->description, 'type' => $node->type->name, 'default' => (bool)($node->value ?? false), ]; } $out[] = [ 'group' => $group->group_name, 'description' => $group->description, 'nodes' => $nodes, ]; } $response->success($out); }, []); $this->post('/subusers', function () { global /** @var response $response */ $response; self::requireParameters([ 'cvr', 'phone_country_code', 'phone' ]); $cvr = (int)self::getParameter('cvr'); $phone_country_code =(int) self::getParameter('phone_country_code'); $phone = (int)self::getParameter('phone'); /** Validation */ // Check if the CVR number is valid (8 digits) self::requireType($cvr, self::type_int()); self::requireMinLength('cvr', 8); self::requireMaxLength('cvr', 8); // Check if the phone country code is valid (1-3 digits) self::requireType($phone_country_code, self::type_int()); self::requireMinLength('phone_country_code', 1); self::requireMaxLength('phone_country_code', 3); // Check if the phone number is valid (4-15 digits) self::requireType($phone, self::type_int()); self::requireMinLength('phone', 4); self::requireMaxLength('phone', 15); // Check if the CVR is registered to a company in the database $economic = new economic(); $results = $economic->customers->customers->search([ 'corporateIdentificationNumber' => (string)$cvr, ],[ 'skipPages' => 0, 'pageSize' => 1000, // Since the limit is 1000, we need to set the page size to 1000. ])->collection; if (count($results) === 0) { $response->error('Customer not found', 404); } /** At this point, we know that the CVR is valid and that the company exists in the database. */ $company_user = (new users_o())->getUserByCustomerNumber((int)$results[0]->customerNumber); $company_user->requireSelected(); /** We can now check if a subuser already exists with the same phone number. */ $subuser = (new subusers_o())->getSubuserByPhone($phone_country_code, $phone); if ($subuser !== null) { $response->error('Account already exists with this phone number', 400); } /** We can now create the subuser and send a request to the company user to link the subuser to the company. */ $subuser = (new subusers_o())->add( null, null, null, null, (int)$phone_country_code, (int)$phone ); // Send an SMS with a link to complete the registration process. $gatewayAPI = new gatewayapi(); if ($gatewayAPI->isEnabled()) { $token = $subuser->generateSetupToken(); $link = 'https://truckwash.io/complete-registration?token=' . $token; $message = 'Tak for din oprettelse af chaufførkonto hos Truck Wash! Klik på linket for at fuldføre registreringen: ' . $link; $phone_number_array = [(string)$phone_country_code . (string)$phone]; $gatewayAPI->send($phone_number_array, $message); } // Add the grant request $subuser_grants_o = new subuser_grants_o(); try { $subuser_grants_o->add($results[0]->customerNumber, $subuser->id, false, null); } catch (Exception $e) { $response->error('Failed to add subuser grant', 500); } $response->success(['cvr' => $cvr, 'customer_number' => $results[0]->customerNumber]); // Code for creating a new subuser would go here }); $this->get('/subusers/setup', function () { // Require the user to be logged in global $response; self::requireParameters(['token']); $token = self::getParameter('token'); // Get the subuser with the setup token $subuser = (new subusers_o())->getSubuserBySetupToken($token); if ($subuser === null) { $response->error('Invalid or expired token', 400); } $response->success(['message' => 'Token is valid', 'subuser_id' => $subuser->id]); }); $this->post('/subusers/setup', function () { // Require the user to be logged in global $response; self::requireParameters(['token', 'password', 'name']); $token = (string)self::getParameter('token'); $password = (string)self::getParameter('password'); $name = (string)self::getParameter('name'); self::requireType($password, self::type_string()); self::requireMinLength('password', 8); self::requireMaxLength('password', 255); self::requireRegex($password, '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/', 'Password must contain at least one uppercase letter, one lowercase letter, and one number'); self::requireType($name, self::type_string()); self::requireMinLength('name', 3); self::requireMaxLength('name', 255); /** * Optional fields: * - username: string, 3-255 characters * - email: string, valid email format, 3-255 characters */ $username = null; $email = null; if (self::isParametersSet(['username'])) { $username = (string)self::getParameter('username'); self::requireType($username, self::type_string()); self::requireMinLength('username', 3); self::requireMaxLength('username', 255); } if (self::isParametersSet(['email'])) { $email = (string)self::getParameter('email'); self::requireType($email, self::type_string()); self::requireMinLength('email', 3); self::requireMaxLength('email', 255); if(!filter_var($email, FILTER_VALIDATE_EMAIL)) { $response->error('Invalid email format', 400); } } // Get the subuser with the setup token $subuser = (new subusers_o())->getSubuserBySetupToken($token); if ($subuser === null) { $response->error('Invalid or expired token', 400); } // Set the password for the subuser try { $subuser->setPassword($password); if (!empty($username) || !empty($email) || !empty($name)) { $subuser->update([ ...(!empty($username) ? ['username' => $username] : []), ...(!empty($email) ? ['email' => $email] : []), ...(!empty($name) ? ['name' => $name] : []), ]); } // Invalidate the setup token $subuser->invalidateSetupToken($token); $response->success(['message' => 'Complete registration successful']); } catch (Exception $e) { $response->error($e->getMessage(), 400); } }); $this->post('/subusers/auth/password', function () { global $response; /** * Username types: * 1. Phone number & country code * 2. Subuser id * 3. Username */ $phone_country_code = null; $phone = null; $subuser_id = null; $username = null; if (self::isParametersSet(['phone_country_code', 'phone'])) { $phone_country_code = (int)self::getParameter('phone_country_code'); $phone = (int)self::getParameter('phone'); self::requireType($phone_country_code, self::type_int()); self::requireMinLength('phone_country_code', 1); self::requireMaxLength('phone_country_code', 3); self::requireType($phone, self::type_int()); self::requireMinLength('phone', 4); self::requireMaxLength('phone', 15); } elseif (self::isParametersSet(['subuser_id'])) { $subuser_id = (int)self::getParameter('subuser_id'); self::requireType($subuser_id, self::type_int()); } elseif (self::isParametersSet(['username'])) { $username = (string)self::getParameter('username'); self::requireType($username, self::type_string()); self::requireMinLength('username', 3); self::requireMaxLength('username', 255); } else { $response->error('You must provide either phone_country_code & phone, subuser_id or username', 400); } // Get the subuser based on the provided username type $subuser = null; if ($phone_country_code !== null && $phone !== null) { $subuser = (new subusers_o())->getSubuserByPhone($phone_country_code, $phone); } elseif ($subuser_id !== null) { $subuser = (new subusers_o())->select($subuser_id); } elseif ($username !== null) { $subuser = (new subusers_o())->getSubuserByUsername($username); } if ($subuser === null) { $response->error('Subuser not found', 404); } self::requireParameters(['password']); $password = (string)self::getParameter('password'); self::requireType($password, self::type_string()); self::requireMinLength('password', 8); self::requireMaxLength('password', 255); try { if (password_verify($password, $subuser->password->value())) { if ($subuser->isTwoFactorEnabled()) { $token = (new authentication())->create_2fa_token($subuser->id, '2FA_VERIFICATION_SUBUSER'); $response->success(['2fa_required' => true, '2fa_token' => $token]); } // Generate a new session for the subuser $session = $subuser->generateSession(); $response->success(['session' => $session]); } else { $response->error('Invalid password', 400); } } catch (Exception $e) { $response->error($e->getMessage(), 500); } }); // ============================= // Subusers - List & Get (with grant visibility) // ============================= $this->get('/subusers', function () { global $response; // Require authenticated principal (user or subuser) $auth = new authentication(); $user = $auth->get_user(); $subuser = $auth->get_subuser(); if ($user === false && $subuser === false) { $response->error('Unauthorized', 401); } // Link route permission to Subusers node so subusers can be constrained by grants. // For classic users we keep current behavior (no extra user permission enforced here). $permission_list = self::definePermission('list_own_subusers', subusers_permission_node_key::SUBUSERS_LIST); if ($subuser !== false) { // Only enforce for subuser principals; classic users are governed by existing user ACLs elsewhere. self::requirePermission($permission_list); } // Determine effective customer number (user's customer number or subuser's target header) if ($user !== false) { $customerNumber = (int)$user->customer_number->value(); } else { $customerNumber = (int)$auth->get_subuser_customer_number_target(); } if (empty($customerNumber)) { $response->error('Unauthorized', 401); } // Optional: include_non_enabled (boolean) — when true, include subusers that only have non-enabled grants $includeNonEnabled = false; if (self::isParametersSet(['include_non_enabled'])) { $tmp = filter_var(self::getParameter('include_non_enabled'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); $includeNonEnabled = $tmp === null ? false : (bool)$tmp; } // Only list subusers that have an enabled grant for the caller's customer number $existsClause = sprintf( "EXISTS (SELECT 1 FROM `subuser_grants` sg WHERE sg.`subuser` = `subusers`.`id` AND %ssg.`deleted_at` IS NULL AND sg.`billing_customer_number` = %d)", $includeNonEnabled ? '' : 'sg.`enabled` = 1 AND ', $customerNumber ); // Use pagination helper with additional where $objects = (new subusers_o()) ->listObjectsWithPaginationIfSet(function ($o) use ($customerNumber) { $o = (object)$o; $permissions = (new subuser_grants_o()) ->getGrantsForSubuserAndCustomer((int)$o->id, $customerNumber); return [ 'id' => (int)$o->id, 'username' => $o->username, 'name' => $o->name, 'email' => $o->email, 'phone_country_code' => isset($o->phone_country_code) ? (int)$o->phone_country_code : null, 'phone' => isset($o->phone) ? (int)$o->phone : null, 'created_at' => $o->created_at ?? null, 'updated_at' => $o->updated_at ?? null, 'suspended_at' => $o->suspended_at ?? null, 'permissions' => $permissions, 'two_factor_enabled' => $o->isTwoFactorEnabled(), ]; }, null, [], $existsClause); $response->success($objects); }, []); $this->get('/subusers/me', function () { global $response; $subuser = (new authentication())->get_subuser(); if ($subuser === false) { $response->error('Unauthorized', 401); } $grants = (new subuser_grants_o())->getFieldsWhere([ 'subuser' => $subuser->id, 'enabled' => 1, 'deleted_at' => null, ], ['permissions', 'billing_customer_number']); $result = [ "id" => (int)$subuser->id, "username" => $subuser->username->value(), "name" => $subuser->name->value(), "email" => $subuser->email->value(), "phone_country_code" => $subuser->phone_country_code->value() !== null ? (int)$subuser->phone_country_code->value() : null, "phone" => $subuser->phone->value() !== null ? (int)$subuser->phone->value() : null, "grants" => array_map(function ($grant) { return [ 'name' => (new users_o())->getCustomerName((int)$grant['billing_customer_number']), 'billing_customer_number' => (int)$grant['billing_customer_number'], 'permissions' => json_decode($grant['permissions'], true) ?: [], ]; }, $grants), "created_at" => $subuser->created_at->value() ?? null, "updated_at" => $subuser->updated_at->value() ?? null, "suspended_at" => $subuser->suspended_at->value() ?? null, "two_factor_enabled" => $subuser->isTwoFactorEnabled(), ]; $response->success($result); }, []); // Public registration endpoint (alias of POST /subusers) matching OpenAPI: POST /subusers/me $this->post('/subusers/me', function () { global /** @var response $response */ $response; // Required input self::requireParameters([ 'cvr', 'phone_country_code', 'phone' ]); $cvr = (int)self::getParameter('cvr'); $phone_country_code = (int)self::getParameter('phone_country_code'); $phone = (int)self::getParameter('phone'); // Validation self::requireType($cvr, self::type_int()); self::requireMinLength('cvr', 8); self::requireMaxLength('cvr', 8); self::requireType($phone_country_code, self::type_int()); self::requireMinLength('phone_country_code', 1); self::requireMaxLength('phone_country_code', 3); self::requireType($phone, self::type_int()); self::requireMinLength('phone', 4); self::requireMaxLength('phone', 15); // Look up company by CVR in e-conomic $economic = new economic(); $results = $economic->customers->customers->search([ 'corporateIdentificationNumber' => (string)$cvr, ], [ 'skipPages' => 0, 'pageSize' => 1000, ])->collection; if (count($results) === 0) { $response->error('Customer not found', 404); } // Ensure company user exists (sanity) and phone is not already registered $company_user = (new users_o())->getUserByCustomerNumber((int)$results[0]->customerNumber); $company_user->requireSelected(); $existing = (new subusers_o())->getSubuserByPhone($phone_country_code, $phone); if ($existing !== null) { $response->error('Account already exists with this phone number', 400); } // Create subuser skeleton $subuser = (new subusers_o())->add( null, null, null, null, (int)$phone_country_code, (int)$phone ); // Optionally send SMS with setup link $gatewayAPI = new gatewayapi(); if ($gatewayAPI->isEnabled()) { $token = $subuser->generateSetupToken(); $link = 'https://truckwash.io/complete-registration?token=' . $token; $message = 'Tak for din oprettelse af chaufførkonto hos Truck Wash! Klik på linket for at fuldføre registreringen: ' . $link; $phone_number_array = [(string)$phone_country_code . (string)$phone]; $gatewayAPI->send($phone_number_array, $message); } // Create a pending grant request for the company $subuser_grants_o = new subuser_grants_o(); try { $subuser_grants_o->add($results[0]->customerNumber, $subuser->id, false, null); } catch (Exception $e) { $response->error('Failed to add subuser grant', 500); } $response->success(['cvr' => $cvr, 'customer_number' => $results[0]->customerNumber]); }); } }