Add /subusers and /subusers/{id} endpoints for subuser management with permission-based visibility

- Implement routes to list and retrieve subusers based on grant visibility tied to the authenticated user's customer number.
- Extend OpenAPI documentation with detailed descriptions, parameters, and response schemas for the new endpoints.
This commit is contained in:
Jeppe Bundgaard
2026-02-11 17:56:59 +01:00
parent 6b90fe8d8e
commit 34ea4937e0
2 changed files with 220 additions and 0 deletions
+137
View File
@@ -78,6 +78,81 @@ tags:
paths:
# Subusers (public registration + setup)
/subusers:
get:
tags:
- Subusers
summary: List subusers visible to the authenticated user
description: |
Returns a paginated list of subusers (drivers) that have enabled grants tied to the
authenticated user's customer number. Only subusers with at least one enabled, non-deleted
grant for the caller's customer are returned.
operationId: listSubusers
parameters:
- name: page
in: query
required: false
schema:
type: integer
minimum: 1
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 1000
- name: search
in: query
required: false
schema:
type: string
responses:
'200':
description: List of visible subusers
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
username:
type: string
nullable: true
name:
type: string
nullable: true
email:
type: string
format: email
nullable: true
phone_country_code:
type: integer
nullable: true
phone:
type: integer
nullable: true
created_at:
type: string
format: date-time
nullable: true
updated_at:
type: string
format: date-time
nullable: true
suspended_at:
type: string
format: date-time
nullable: true
permissions:
type: array
description: Aggregated permission keys granted for the caller's customer
items:
type: string
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalServerError' }
post:
tags:
- Subusers
@@ -130,6 +205,68 @@ paths:
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalServerError' }
/subusers/{id}:
get:
tags:
- Subusers
summary: Get a subuser by ID (visible by grant)
description: |
Returns the subuser if the authenticated user has at least one enabled, non-deleted grant
for their customer number to this subuser. Otherwise returns 404.
operationId: getSubuser
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Subuser details
content:
application/json:
schema:
type: object
properties:
id:
type: integer
username:
type: string
nullable: true
name:
type: string
nullable: true
email:
type: string
format: email
nullable: true
phone_country_code:
type: integer
nullable: true
phone:
type: integer
nullable: true
created_at:
type: string
format: date-time
nullable: true
updated_at:
type: string
format: date-time
nullable: true
suspended_at:
type: string
format: date-time
nullable: true
permissions:
type: array
description: Aggregated permission keys granted for the caller's customer
items:
type: string
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalServerError' }
/subusers/setup:
get:
tags:
@@ -429,5 +429,88 @@ class subusersRoute
$response->error($e->getMessage(), 500);
}
});
// =============================
// Subusers - List & Get (with grant visibility)
// =============================
$this->get('/subusers', function () {
global $response;
// Require authenticated user
$user = (new authentication())->get_user();
if ($user === false) {
$response->error('Unauthorized', 401);
}
$customerNumber = (int)$user->customer_number->value();
if ($customerNumber === 0) {
$response->error('Unauthorized', 401);
}
// 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 sg.`enabled` = 1 AND sg.`deleted_at` IS NULL AND sg.`billing_customer_number` = %d)",
$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,
];
}, null, [], $existsClause);
$response->success($objects);
}, []);
$this->get('/subusers/{id}', function () {
global $response;
$user = (new authentication())->get_user();
if ($user === false) {
$response->error('Unauthorized', 401);
}
$customerNumber = (int)$user->customer_number->value();
if ($customerNumber === 0) {
$response->error('Unauthorized', 401);
}
$id = (int)self::fromRoute('id');
self::requireType($id, self::type_int());
$subuser = (new subusers_o())->select($id);
if (!$subuser->exists()) {
$response->error('Subuser not found', 404);
}
// Check visibility via grants
$permissions = (new subuser_grants_o())->getGrantsForSubuserAndCustomer($id, $customerNumber);
if (empty($permissions)) {
// Hide existence if not visible
$response->error('Subuser not found', 404);
}
$response->success([
'id' => (int)$subuser->id,
'username' => $subuser->username->value(),
'name' => $subuser->name->value(),
'email' => $subuser->email->value(),
'phone_country_code' => (int)$subuser->phone_country_code->value(),
'phone' => (int)$subuser->phone->value(),
'created_at' => $subuser->created_at->value(),
'updated_at' => $subuser->updated_at->value(),
'suspended_at' => $subuser->suspended_at->value(),
'permissions' => $permissions,
]);
}, []);
}
}