Refactor subuser management payload and enhance grant deduplication logic

This commit is contained in:
Jeppe Bundgaard
2026-07-08 12:13:12 +02:00
parent 6b7592921d
commit b51006d9d1
2 changed files with 200 additions and 37 deletions
File diff suppressed because one or more lines are too long
+199 -36
View File
@@ -454,8 +454,8 @@ class subusersRoute
'id' => 's.`id`',
'created_at' => 's.`created_at`',
'updated_at' => 'row_updated_at',
'customer_number' => 'g.`billing_customer_number`',
'grant_id' => 'g.`id`',
'customer_number' => 'customer_number_sort',
'grant_id' => 'grant_id_sort',
'name' => 's.`name`',
];
@@ -487,11 +487,10 @@ class subusersRoute
$statement->bind_param($types, ...$refs);
}
private function buildSuperuserSubuserManagementPayload(array $row): array
private function buildSuperuserGrantPayload(array $row, bool $setupRequired): array
{
$grantPermissions = subuser_grants_o::normalizePermissionsValue($row['grant_permissions'] ?? null);
$templateService = new subuser_permission_templates_service();
$setupRequired = !is_string($row['password'] ?? null) || trim((string)$row['password']) === '';
$grantEnabled = (bool)((int)($row['grant_enabled'] ?? 0));
$accessState = 'inactive';
@@ -501,6 +500,44 @@ class subusersRoute
$accessState = 'disabled';
}
return [
'grant_id' => (int)$row['grant_id'],
'customer_number' => (int)$row['customer_number'],
'customer_name' => $row['customer_name'] ?: null,
'grant_enabled' => $grantEnabled,
'grant_note' => $row['grant_note'] ?? null,
'grant_permissions' => $grantPermissions,
'permissions' => $grantPermissions,
'permission_template_key' => $templateService->classify($grantPermissions, $grantEnabled),
'permission_groups' => $templateService->permissionGroups($grantPermissions),
'grant_created_at' => $row['grant_created_at'] ?? null,
'grant_updated_at' => $row['grant_updated_at'] ?? null,
'access_state' => $accessState,
];
}
private function buildSuperuserSubuserManagementPayload(array $row, array $grantRows = []): array
{
$setupRequired = !is_string($row['password'] ?? null) || trim((string)$row['password']) === '';
if ($grantRows === [] && !empty($row['grant_id'])) {
$grantRows = [$row];
}
$grants = array_map(
fn (array $grantRow): array => $this->buildSuperuserGrantPayload($grantRow, $setupRequired),
$this->dedupeSuperuserGrantRows($grantRows)
);
$primaryGrant = $grants[0] ?? null;
$accessState = 'inactive';
if (array_filter($grants, static fn (array $grant): bool => $grant['access_state'] === 'active') !== []) {
$accessState = 'active';
} elseif (array_filter($grants, static fn (array $grant): bool => $grant['access_state'] === 'pending_setup') !== []) {
$accessState = 'pending_setup';
} elseif ($grants !== []) {
$accessState = 'disabled';
}
return [
'id' => (int)$row['id'],
'username' => $row['username'] ?? null,
@@ -516,21 +553,71 @@ class subusersRoute
'invite_accepted' => !$setupRequired,
'can_resend_invite' => $setupRequired,
'profile_editable_by_manager' => false,
'customer_number' => (int)$row['customer_number'],
'customer_name' => $row['customer_name'] ?: null,
'grant_id' => (int)$row['grant_id'],
'grant_enabled' => $grantEnabled,
'grant_note' => $row['grant_note'] ?? null,
'grant_permissions' => $grantPermissions,
'permissions' => $grantPermissions,
'permission_template_key' => $templateService->classify($grantPermissions, $grantEnabled),
'permission_groups' => $templateService->permissionGroups($grantPermissions),
'grant_created_at' => $row['grant_created_at'] ?? null,
'grant_updated_at' => $row['grant_updated_at'] ?? null,
'customer_number' => $primaryGrant['customer_number'] ?? null,
'customer_name' => $primaryGrant['customer_name'] ?? null,
'grant_id' => $primaryGrant['grant_id'] ?? null,
'grant_enabled' => $primaryGrant['grant_enabled'] ?? false,
'grant_note' => $primaryGrant['grant_note'] ?? null,
'grant_permissions' => $primaryGrant['grant_permissions'] ?? [],
'permissions' => $primaryGrant['permissions'] ?? [],
'permission_template_key' => $primaryGrant['permission_template_key'] ?? subuser_permission_templates_service::TEMPLATE_DEACTIVATED,
'permission_groups' => $primaryGrant['permission_groups'] ?? [],
'grant_created_at' => $primaryGrant['grant_created_at'] ?? null,
'grant_updated_at' => $primaryGrant['grant_updated_at'] ?? null,
'grants' => $grants,
'grant_count' => count($grants),
'customer_numbers' => array_values(array_unique(array_map(
static fn (array $grant): int => (int)$grant['customer_number'],
$grants
))),
'access_state' => $accessState,
];
}
private function dedupeSuperuserGrantRows(array $grantRows): array
{
$byCustomer = [];
foreach ($grantRows as $grantRow) {
$customerNumber = (int)($grantRow['customer_number'] ?? 0);
if ($customerNumber <= 0) {
continue;
}
$existing = $byCustomer[$customerNumber] ?? null;
if ($existing === null || $this->compareSuperuserGrantRows($grantRow, $existing) < 0) {
$byCustomer[$customerNumber] = $grantRow;
}
}
$deduped = array_values($byCustomer);
usort($deduped, fn (array $left, array $right): int => $this->compareSuperuserGrantRows($left, $right));
return $deduped;
}
private function compareSuperuserGrantRows(array $left, array $right): int
{
$leftEnabled = (int)($left['grant_enabled'] ?? 0);
$rightEnabled = (int)($right['grant_enabled'] ?? 0);
if ($leftEnabled !== $rightEnabled) {
return $rightEnabled <=> $leftEnabled;
}
$leftCustomer = (int)($left['customer_number'] ?? 0);
$rightCustomer = (int)($right['customer_number'] ?? 0);
if ($leftCustomer !== $rightCustomer) {
return $leftCustomer <=> $rightCustomer;
}
$leftUpdated = strtotime((string)($left['grant_updated_at'] ?? $left['grant_created_at'] ?? '')) ?: 0;
$rightUpdated = strtotime((string)($right['grant_updated_at'] ?? $right['grant_created_at'] ?? '')) ?: 0;
if ($leftUpdated !== $rightUpdated) {
return $rightUpdated <=> $leftUpdated;
}
return (int)($right['grant_id'] ?? 0) <=> (int)($left['grant_id'] ?? 0);
}
private function routePositiveInt(string $name): int
{
global $response;
@@ -645,7 +732,12 @@ class subusersRoute
OR CAST(s.`phone` AS CHAR) LIKE ?
OR CAST(g.`billing_customer_number` AS CHAR) LIKE ?
OR g.`note` LIKE ?
OR u.`display_name` LIKE ?
OR EXISTS (
SELECT 1
FROM `users` search_u
WHERE search_u.`customer_number` = g.`billing_customer_number`
AND search_u.`display_name` LIKE ?
)
)";
$search = '%' . $pagination['search'] . '%';
for ($i = 0; $i < 9; $i++) {
@@ -658,10 +750,9 @@ class subusersRoute
$fromSql = "
FROM `subuser_grants` g
INNER JOIN `subusers` s ON s.`id` = g.`subuser`
LEFT JOIN `users` u ON u.`customer_number` = g.`billing_customer_number`
";
$countSql = "SELECT COUNT(*) AS `count` $fromSql $whereSql";
$countSql = "SELECT COUNT(DISTINCT s.`id`) AS `count` $fromSql $whereSql";
$countStatement = $db->conn->prepare($countSql);
if ($countStatement === false) {
throw new Exception('Failed to prepare subuser count query: ' . $db->conn->error);
@@ -672,7 +763,7 @@ class subusersRoute
$total = (int)($countResult->fetch_assoc()['count'] ?? 0);
$countStatement->close();
$dataSql = "
$pageSql = "
SELECT
s.`id`,
s.`username`,
@@ -685,31 +776,100 @@ class subusersRoute
s.`created_at`,
s.`updated_at`,
s.`suspended_at`,
g.`id` AS `grant_id`,
g.`billing_customer_number` AS `customer_number`,
g.`enabled` AS `grant_enabled`,
g.`note` AS `grant_note`,
g.`permissions` AS `grant_permissions`,
g.`created_at` AS `grant_created_at`,
g.`updated_at` AS `grant_updated_at`,
COALESCE(g.`updated_at`, s.`updated_at`) AS `row_updated_at`,
u.`display_name` AS `customer_name`
MAX(COALESCE(g.`updated_at`, s.`updated_at`)) AS `row_updated_at`,
MIN(g.`billing_customer_number`) AS `customer_number_sort`,
MAX(g.`id`) AS `grant_id_sort`
$fromSql
$whereSql
GROUP BY
s.`id`,
s.`username`,
s.`password`,
s.`name`,
s.`email`,
s.`phone_country_code`,
s.`phone`,
s.`two_factor_enabled`,
s.`created_at`,
s.`updated_at`,
s.`suspended_at`
ORDER BY {$pagination['order_sql']} {$pagination['order_direction']}
LIMIT ? OFFSET ?
";
$dataStatement = $db->conn->prepare($dataSql);
if ($dataStatement === false) {
$pageStatement = $db->conn->prepare($pageSql);
if ($pageStatement === false) {
throw new Exception('Failed to prepare subuser list query: ' . $db->conn->error);
}
$dataParams = [...$params, (int)$pagination['limit'], $offset];
$this->bindStatementParameters($dataStatement, $types . 'ii', $dataParams);
$dataStatement->execute();
$result = $dataStatement->get_result();
$pageParams = [...$params, (int)$pagination['limit'], $offset];
$this->bindStatementParameters($pageStatement, $types . 'ii', $pageParams);
$pageStatement->execute();
$result = $pageStatement->get_result();
$rows = $result->fetch_all(MYSQLI_ASSOC);
$dataStatement->close();
$pageStatement->close();
$subuserIds = array_map(static fn (array $row): int => (int)$row['id'], $rows);
$grantRowsBySubuserId = [];
if ($subuserIds !== []) {
$placeholders = implode(',', array_fill(0, count($subuserIds), '?'));
$grantWhere = [
'g.`deleted_at` IS NULL',
'g.`subuser` IN (' . $placeholders . ')',
];
$grantParams = $subuserIds;
$grantTypes = str_repeat('i', count($subuserIds));
if (!$includeNonEnabled) {
$grantWhere[] = 'g.`enabled` = 1';
}
if ($customerNumber !== null) {
$grantWhere[] = 'g.`billing_customer_number` = ?';
$grantParams[] = $customerNumber;
$grantTypes .= 'i';
}
$grantSql = "
SELECT
g.`subuser` AS `subuser_id`,
g.`id` AS `grant_id`,
g.`billing_customer_number` AS `customer_number`,
g.`enabled` AS `grant_enabled`,
g.`note` AS `grant_note`,
g.`permissions` AS `grant_permissions`,
g.`created_at` AS `grant_created_at`,
g.`updated_at` AS `grant_updated_at`
FROM `subuser_grants` g
WHERE " . implode(' AND ', $grantWhere) . "
ORDER BY
g.`subuser` ASC,
g.`enabled` DESC,
g.`billing_customer_number` ASC,
COALESCE(g.`updated_at`, g.`created_at`) DESC,
g.`id` DESC
";
$grantStatement = $db->conn->prepare($grantSql);
if ($grantStatement === false) {
throw new Exception('Failed to prepare subuser grant list query: ' . $db->conn->error);
}
$this->bindStatementParameters($grantStatement, $grantTypes, $grantParams);
$grantStatement->execute();
$grantResult = $grantStatement->get_result();
$grantRows = $grantResult->fetch_all(MYSQLI_ASSOC);
$grantStatement->close();
$customerNames = $this->resolveCustomerNames(array_map(
static fn (array $grantRow): int => (int)($grantRow['customer_number'] ?? 0),
$grantRows
));
foreach ($grantRows as $grantRow) {
$subuserId = (int)$grantRow['subuser_id'];
$customerNumberForGrant = (int)$grantRow['customer_number'];
$grantRow['customer_name'] = $this->resolveCustomerName($customerNumberForGrant, $customerNames);
$grantRowsBySubuserId[$subuserId] ??= [];
$grantRowsBySubuserId[$subuserId][] = $grantRow;
}
}
$response->paginate(
(int)$pagination['page'],
@@ -720,7 +880,10 @@ class subusersRoute
[$pagination['order_field'] => $pagination['order_direction']]
);
return array_map(fn(array $row): array => $this->buildSuperuserSubuserManagementPayload($row), $rows);
return array_map(
fn(array $row): array => $this->buildSuperuserSubuserManagementPayload($row, $grantRowsBySubuserId[(int)$row['id']] ?? []),
$rows
);
}
private function getGrantForScopedUserOrFail(int $grantId, int $customerNumber): subuser_grants_o