Three fixes for the failing CI checks (PHP api, PHP integration): 1. RouteScopeTest.php: Pest's toContain() is variadic, so both arguments are treated as needles. The second 'description' argument was being treated as a needle, causing every file to fail. Removed the misleading second argument. 2. Added ScopeMiddleware::requireScope() calls and the matching Scope/ScopeMiddleware imports to 15 protected route files that the integration test contract requires. 3. documentation/auth/route-scope-audit.md: added the missing Scope::SUPERUSER_WRITE reference and a constants reference table. Also registered tests/auth/StripeInvoiceEmailTemplateTest.php in the legacy test manifest.
273 lines
13 KiB
PHP
273 lines
13 KiB
PHP
<?php
|
|
|
|
namespace routes;
|
|
|
|
use classes\authentication;
|
|
use classes\limited_backoffice_exception;
|
|
use classes\limited_backoffice_login_grant_service;
|
|
use classes\limited_backoffice_service;
|
|
use traits\route_t;
|
|
|
|
use app\auth\Scope;
|
|
use app\auth\ScopeMiddleware;
|
|
|
|
require_once WD . '/classes/limited_backoffice_login_grant_service.php';
|
|
|
|
class limitedBackofficeRoute
|
|
{
|
|
use route_t;
|
|
|
|
public function run(): void
|
|
{
|
|
$this->get('/limited-backoffice/departments', function () {
|
|
ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/limited-backoffice/departments');
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
|
return $service->departmentsForUser($user);
|
|
});
|
|
}, [
|
|
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
|
]);
|
|
|
|
$this->get('/limited-backoffice/departments/{departmentId}/prices', function () {
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
|
$departmentId = $this->routePositiveInt('departmentId');
|
|
return $service->getDepartmentPrices($user, $departmentId);
|
|
});
|
|
}, [
|
|
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
|
]);
|
|
|
|
$this->put('/limited-backoffice/departments/{departmentId}/prices', function () {
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_MANAGE_PRICES);
|
|
$departmentId = $this->routePositiveInt('departmentId');
|
|
return $service->updateDepartmentPrices($user, $departmentId, $this->requestPayload());
|
|
});
|
|
}, [
|
|
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
|
limited_backoffice_service::PERMISSION_MANAGE_PRICES => 'Manage limited backoffice department prices',
|
|
]);
|
|
|
|
$this->get('/limited-backoffice/departments/{departmentId}/customer-pricing', function () {
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_VIEW_CUSTOMER_PRICING);
|
|
$departmentId = $this->routePositiveInt('departmentId');
|
|
$customerUserId = $this->queryCustomerUserId();
|
|
return $service->getDepartmentCustomerPricing($user, $departmentId, $customerUserId);
|
|
});
|
|
}, [
|
|
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
|
limited_backoffice_service::PERMISSION_VIEW_CUSTOMER_PRICING => 'View limited backoffice department customer pricing',
|
|
]);
|
|
|
|
$this->put('/limited-backoffice/departments/{departmentId}/customer-pricing', function () {
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_MANAGE_CUSTOMER_PRICING);
|
|
$departmentId = $this->routePositiveInt('departmentId');
|
|
$payload = $this->requestPayload();
|
|
$customerUserId = $this->payloadCustomerUserId($payload);
|
|
return $service->updateDepartmentCustomerPricing($user, $departmentId, $customerUserId, $payload);
|
|
});
|
|
}, [
|
|
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
|
limited_backoffice_service::PERMISSION_MANAGE_CUSTOMER_PRICING => 'Manage limited backoffice department customer pricing',
|
|
]);
|
|
|
|
$this->get('/limited-backoffice/roles', function () {
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES);
|
|
return $service->rolePresets($user);
|
|
});
|
|
}, [
|
|
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
|
limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES => 'Manage limited backoffice employees',
|
|
]);
|
|
|
|
$this->get('/limited-backoffice/employees', function () {
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES);
|
|
$includeInactive = strtolower((string)($this->fromQuery('include_inactive') ?? 'false')) === 'true';
|
|
return $service->employeesForUser($user, $includeInactive);
|
|
});
|
|
}, [
|
|
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
|
limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES => 'Manage limited backoffice employees',
|
|
]);
|
|
|
|
$this->post('/limited-backoffice/employees', function () {
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES);
|
|
return $service->createEmployee($user, $this->requestPayload());
|
|
});
|
|
}, [
|
|
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
|
limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES => 'Manage limited backoffice employees',
|
|
]);
|
|
|
|
$this->post('/limited-backoffice/employees/{employeeId}/migrate', function () {
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission('superuser');
|
|
return $service->migrateEmployee($user, $this->routePositiveInt('employeeId'), $this->requestPayload());
|
|
});
|
|
}, [
|
|
'superuser' => 'Migrate existing employees to limited backoffice employees',
|
|
]);
|
|
|
|
$this->post('/limited-backoffice/employees/{employeeId}/login-link', function () {
|
|
global $response;
|
|
$response->error(
|
|
'Reusable employee login links are retired. Create a short-lived one-time login grant instead.',
|
|
410
|
|
);
|
|
});
|
|
|
|
$this->post('/limited-backoffice/employees/{employeeId}/login-grants', function () {
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES);
|
|
return (new limited_backoffice_login_grant_service())->create(
|
|
$user,
|
|
$this->routePositiveInt('employeeId'),
|
|
$this->requestPayload()
|
|
);
|
|
});
|
|
}, [
|
|
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
|
limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES => 'Manage limited backoffice employees',
|
|
]);
|
|
|
|
$this->delete('/limited-backoffice/employees/{employeeId}/login-grants', function () {
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES);
|
|
return (new limited_backoffice_login_grant_service())->revokeForEmployee(
|
|
$user,
|
|
$this->routePositiveInt('employeeId')
|
|
);
|
|
});
|
|
}, [
|
|
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
|
limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES => 'Manage limited backoffice employees',
|
|
]);
|
|
|
|
$this->put('/limited-backoffice/employees/{employeeId}', function () {
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES);
|
|
return $service->updateEmployee($user, $this->routePositiveInt('employeeId'), $this->requestPayload());
|
|
});
|
|
}, [
|
|
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
|
limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES => 'Manage limited backoffice employees',
|
|
]);
|
|
|
|
$this->delete('/limited-backoffice/employees/{employeeId}', function () {
|
|
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
|
$this->requirePermission(limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES);
|
|
return $service->deactivateEmployee($user, $this->routePositiveInt('employeeId'));
|
|
});
|
|
}, [
|
|
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
|
limited_backoffice_service::PERMISSION_MANAGE_EMPLOYEES => 'Manage limited backoffice employees',
|
|
]);
|
|
}
|
|
|
|
private function withLimitedBackoffice(callable $callback): void
|
|
{
|
|
global $response;
|
|
|
|
try {
|
|
$user = (new authentication())->get_user();
|
|
if (!$user) {
|
|
$response->error('Invalid session', 400);
|
|
}
|
|
|
|
$response->success($callback(new limited_backoffice_service(), $user));
|
|
} catch (limited_backoffice_exception $exception) {
|
|
$response->error($exception->payload(), $exception->statusCode());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function requestPayload(): array
|
|
{
|
|
$payload = json_decode(file_get_contents('php://input'), true);
|
|
return is_array($payload) ? $payload : [];
|
|
}
|
|
|
|
private function routePositiveInt(string $name): int
|
|
{
|
|
$value = $this->fromRoute($name);
|
|
if (!is_string($value) || !ctype_digit($value) || (int)$value <= 0) {
|
|
throw new limited_backoffice_exception('Invalid route parameter.', 400);
|
|
}
|
|
return (int)$value;
|
|
}
|
|
|
|
private function queryPositiveInt(string $name): int
|
|
{
|
|
$value = $this->fromQuery($name);
|
|
if (!is_string($value) || !ctype_digit($value) || (int)$value <= 0) {
|
|
throw new limited_backoffice_exception('Invalid query parameter.', 400);
|
|
}
|
|
return (int)$value;
|
|
}
|
|
|
|
private function queryCustomerUserId(): int
|
|
{
|
|
if ($this->fromQuery('user_id') !== null) {
|
|
return $this->queryPositiveInt('user_id');
|
|
}
|
|
|
|
return $this->userIdFromCustomerNumber($this->queryPositiveInt('customer_number'));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function payloadPositiveInt(array $payload, string $name): int
|
|
{
|
|
$value = $payload[$name] ?? null;
|
|
if (is_int($value) && $value > 0) {
|
|
return $value;
|
|
}
|
|
if (!is_string($value) || !ctype_digit($value) || (int)$value <= 0) {
|
|
throw new limited_backoffice_exception('Invalid request parameter.', 400);
|
|
}
|
|
return (int)$value;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function payloadCustomerUserId(array $payload): int
|
|
{
|
|
if (array_key_exists('user_id', $payload)) {
|
|
return $this->payloadPositiveInt($payload, 'user_id');
|
|
}
|
|
|
|
return $this->userIdFromCustomerNumber($this->payloadPositiveInt($payload, 'customer_number'));
|
|
}
|
|
|
|
private function userIdFromCustomerNumber(int $customerNumber): int
|
|
{
|
|
$customer = (new \objects\users_o())->getUserByCustomerNumber($customerNumber);
|
|
if (!$customer->exists()) {
|
|
throw new limited_backoffice_exception('Customer not found', 404);
|
|
}
|
|
|
|
return (int)$customer->id;
|
|
}
|
|
}
|