major update

This commit is contained in:
2026-03-04 15:56:58 +01:00
parent 16759a2732
commit 8f4dd5840d
478 changed files with 24313 additions and 8201 deletions

View File

@@ -2,7 +2,7 @@
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Service\Auth\ApiTokenEndpointService;
use MintyPHP\Service\Auth\AuthServicesFactory;
use MintyPHP\Service\Security\SecurityServicesFactory;
use MintyPHP\Service\User\UserServicesFactory;
@@ -10,10 +10,12 @@ use MintyPHP\Service\User\UserServicesFactory;
ApiBootstrap::init(false);
ApiResponse::requireMethod('POST');
$input = ApiResponse::readJsonBody();
$request = requestInput();
$input = $request->bodyAll();
$apiTokenEndpointService = app(ApiTokenEndpointService::class);
// ---- Login-specific rate limits (stricter than the general API rate limit in ApiBootstrap) ----
$rateLimiter = (new SecurityServicesFactory())->createRateLimiterService();
$rateLimiter = (app(SecurityServicesFactory::class))->createRateLimiterService();
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$ipResult = $rateLimiter->isBlocked('api.auth.login.ip', $ip);
@@ -27,15 +29,15 @@ $password = (string) ($input['password'] ?? '');
$tokenName = trim((string) ($input['token_name'] ?? '')) ?: 'Login';
$tenantUuid = trim((string) ($input['tenant_uuid'] ?? ''));
$validationErrors = [];
$validationErrors = formErrors();
if ($email === '') {
$validationErrors['email'] = ['required'];
$validationErrors->add('email', 'required');
}
if ($password === '') {
$validationErrors['password'] = ['required'];
$validationErrors->add('password', 'required');
}
if ($validationErrors) {
ApiResponse::validationError($validationErrors);
if ($validationErrors->hasAny()) {
ApiResponse::validationFromFormErrors($validationErrors);
}
// ---- Email+IP rate limit ----
@@ -46,7 +48,7 @@ if (!($emailResult['allowed'] ?? true)) {
}
// ---- User lookup ----
$userReadRepository = (new UserServicesFactory())->createUserReadRepository();
$userReadRepository = (app(UserServicesFactory::class))->createUserReadRepository();
$user = $userReadRepository->findByEmail($email);
if (!$user) {
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
@@ -87,24 +89,19 @@ $rateLimiter->reset('api.auth.login.email_ip', $emailKey);
// ---- Optional tenant scope ----
$tenantId = null;
if ($tenantUuid !== '') {
$tenant = (new TenantRepository())->findByUuid($tenantUuid);
if (!$tenant) {
ApiResponse::error('invalid_tenant_uuid', 422);
}
$tenantId = (int) ($tenant['id'] ?? 0);
if ($tenantId <= 0) {
ApiResponse::error('invalid_tenant_uuid', 422);
}
$userId = (int) ($user['id'] ?? 0);
$userTenantIds = (new AuthServicesFactory())->createAuthScopeGateway()->getUserTenantIds($userId);
if (!in_array($tenantId, $userTenantIds, true)) {
ApiResponse::error('tenant_not_assigned', 403);
$tenantResolution = $apiTokenEndpointService->resolveLoginTenant((int) ($user['id'] ?? 0), $tenantUuid);
if (!($tenantResolution['ok'] ?? false)) {
ApiResponse::error(
(string) ($tenantResolution['error'] ?? 'invalid_tenant_uuid'),
(int) ($tenantResolution['status'] ?? 422)
);
}
$tenantId = array_key_exists('tenant_id', $tenantResolution) ? (int) ($tenantResolution['tenant_id'] ?? 0) : null;
}
// ---- Create token ----
$userId = (int) ($user['id'] ?? 0);
$result = (new AuthServicesFactory())->createApiTokenService()->create(
$result = (app(AuthServicesFactory::class))->createApiTokenService()->create(
$userId,
$tokenName,
$tenantId,

View File

@@ -3,28 +3,30 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\PermissionService;
ApiBootstrap::init();
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$request = requestInput();
$method = $request->method();
if ($method === 'GET') {
ApiResponse::requirePermission(PermissionService::DEPARTMENTS_VIEW);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_DEPARTMENTS_VIEW);
$limit = $request->queryInt('limit', 25);
$offset = $request->queryInt('offset', 0);
$options = [
'limit' => (int) ($_GET['limit'] ?? 25),
'offset' => (int) ($_GET['offset'] ?? 0),
'search' => trim((string) ($_GET['search'] ?? '')),
'order' => (string) ($_GET['order'] ?? 'description'),
'dir' => (string) ($_GET['dir'] ?? 'asc'),
'limit' => $limit,
'offset' => $offset,
'search' => $request->queryString('search'),
'order' => $request->queryString('order', 'description'),
'dir' => $request->queryString('dir', 'asc'),
'tenantUserId' => ApiAuth::userId(),
];
$scopedTenantId = ApiAuth::scopedTenantId();
if ($scopedTenantId) {
$options['tenantIds'] = [$scopedTenantId];
}
$result = directoryServicesFactory()->createDepartmentService()->listPaged($options);
$result = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createDepartmentService()->listPaged($options);
$rows = [];
foreach ($result['rows'] as $row) {
@@ -41,24 +43,24 @@ if ($method === 'GET') {
ApiResponse::success([
'data' => $rows,
'total' => $result['total'] ?? 0,
'limit' => (int) ($_GET['limit'] ?? 25),
'offset' => (int) ($_GET['offset'] ?? 0),
'limit' => $limit,
'offset' => $offset,
]);
}
if ($method === 'POST') {
ApiResponse::requirePermission(PermissionService::DEPARTMENTS_CREATE);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_DEPARTMENTS_CREATE);
$input = ApiResponse::readJsonBody();
$input = $request->bodyAll();
if (array_key_exists('tenant_id', $input)) {
ApiResponse::validationError(['tenant_id' => ['use_tenant_uuid']]);
ApiResponse::validationFromFormErrors(formErrorsFrom(['tenant_id' => ['use_tenant_uuid']]));
}
if (array_key_exists('tenant_uuid', $input)) {
$tenantUuid = trim((string) ($input['tenant_uuid'] ?? ''));
if ($tenantUuid !== '') {
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($tenantUuid);
$tenant = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createTenantService()->findByUuid($tenantUuid);
if (!$tenant) {
ApiResponse::validationError(['tenant_uuid' => ['not_found']]);
ApiResponse::validationFromFormErrors(formErrorsFrom(['tenant_uuid' => ['not_found']]));
}
$input['tenant_id'] = (int) ($tenant['id'] ?? 0);
}
@@ -72,7 +74,7 @@ if ($method === 'POST') {
}
$input['tenant_id'] = $scopedTenantId;
}
$result = directoryServicesFactory()->createDepartmentService()->createFromAdmin($input, ApiAuth::userId());
$result = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createDepartmentService()->createFromAdmin($input, ApiAuth::userId());
ApiResponse::fromServiceResult($result, 201);
}

View File

@@ -3,21 +3,21 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\PermissionService;
ApiBootstrap::init();
$request = requestInput();
$uuid = trim((string) ($id ?? ''));
if ($uuid === '') {
ApiResponse::notFound();
}
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$method = $request->method();
if ($method === 'GET') {
ApiResponse::requirePermission(PermissionService::DEPARTMENTS_VIEW);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_DEPARTMENTS_VIEW);
$department = directoryServicesFactory()->createDepartmentService()->findByUuid($uuid);
$department = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createDepartmentService()->findByUuid($uuid);
if (!$department) {
ApiResponse::notFound();
}
@@ -27,7 +27,7 @@ if ($method === 'GET') {
$tenantUuid = '';
$tenantId = (int) ($department['tenant_id'] ?? 0);
if ($tenantId > 0) {
$tenant = directoryServicesFactory()->createTenantService()->findById($tenantId);
$tenant = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createTenantService()->findById($tenantId);
$tenantUuid = (string) ($tenant['uuid'] ?? '');
}
@@ -46,9 +46,9 @@ if ($method === 'GET') {
}
if ($method === 'PUT' || $method === 'PATCH') {
ApiResponse::requirePermission(PermissionService::DEPARTMENTS_UPDATE);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_DEPARTMENTS_UPDATE);
$department = directoryServicesFactory()->createDepartmentService()->findByUuid($uuid);
$department = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createDepartmentService()->findByUuid($uuid);
if (!$department) {
ApiResponse::notFound();
}
@@ -56,16 +56,16 @@ if ($method === 'PUT' || $method === 'PATCH') {
$departmentId = (int) ($department['id'] ?? 0);
ApiAuth::requireResourceAccess('departments', $departmentId);
$input = ApiResponse::readJsonBody();
$input = $request->bodyAll();
if (array_key_exists('tenant_id', $input)) {
ApiResponse::validationError(['tenant_id' => ['use_tenant_uuid']]);
ApiResponse::validationFromFormErrors(formErrorsFrom(['tenant_id' => ['use_tenant_uuid']]));
}
if (array_key_exists('tenant_uuid', $input)) {
$tenantUuid = trim((string) ($input['tenant_uuid'] ?? ''));
if ($tenantUuid !== '') {
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($tenantUuid);
$tenant = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createTenantService()->findByUuid($tenantUuid);
if (!$tenant) {
ApiResponse::validationError(['tenant_uuid' => ['not_found']]);
ApiResponse::validationFromFormErrors(formErrorsFrom(['tenant_uuid' => ['not_found']]));
}
$input['tenant_id'] = (int) ($tenant['id'] ?? 0);
} else {
@@ -81,14 +81,14 @@ if ($method === 'PUT' || $method === 'PATCH') {
}
$input['tenant_id'] = $scopedTenantId;
}
$result = directoryServicesFactory()->createDepartmentService()->updateFromAdmin($departmentId, $input, ApiAuth::userId());
$result = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createDepartmentService()->updateFromAdmin($departmentId, $input, ApiAuth::userId());
ApiResponse::fromServiceResult($result);
}
if ($method === 'DELETE') {
ApiResponse::requirePermission(PermissionService::DEPARTMENTS_DELETE);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_DEPARTMENTS_DELETE);
$department = directoryServicesFactory()->createDepartmentService()->findByUuid($uuid);
$department = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createDepartmentService()->findByUuid($uuid);
if (!$department) {
ApiResponse::notFound();
}
@@ -96,7 +96,7 @@ if ($method === 'DELETE') {
$departmentId = (int) ($department['id'] ?? 0);
ApiAuth::requireResourceAccess('departments', $departmentId);
$result = directoryServicesFactory()->createDepartmentService()->deleteByUuid($uuid);
$result = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createDepartmentService()->deleteByUuid($uuid);
ApiResponse::fromServiceResult($result);
}

View File

@@ -3,21 +3,21 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\User\UserServicesFactory;
define('MINTY_ALLOW_OUTPUT', true);
ApiBootstrap::init();
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$userServicesFactory = new UserServicesFactory();
$request = requestInput();
$method = $request->method();
$userServicesFactory = app(UserServicesFactory::class);
$userAvatarService = $userServicesFactory->createUserAvatarService();
$user = ApiAuth::user();
$uuid = (string) ($user['uuid'] ?? '');
if ($method === 'GET') {
$size = isset($_GET['size']) ? (int) $_GET['size'] : null;
$size = $request->hasQuery('size') ? $request->queryInt('size') : null;
$path = $userAvatarService->findAvatarPath($uuid, $size);
if (!$path || !is_file($path)) {
@@ -38,11 +38,9 @@ if ($method === 'GET') {
}
if ($method === 'POST') {
if (!ApiAuth::hasPermission(PermissionService::USERS_SELF_UPDATE)) {
ApiResponse::forbidden();
}
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_ME_SELF_UPDATE);
$result = $userAvatarService->saveUpload($uuid, $_FILES['avatar'] ?? []);
$result = $userAvatarService->saveUpload($uuid, $request->file('avatar', []));
if (!($result['ok'] ?? false)) {
$error = $result['error'] ?? 'upload_failed';
ApiResponse::error($error, 422);
@@ -52,9 +50,7 @@ if ($method === 'POST') {
}
if ($method === 'DELETE') {
if (!ApiAuth::hasPermission(PermissionService::USERS_SELF_UPDATE)) {
ApiResponse::forbidden();
}
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_ME_SELF_UPDATE);
$userAvatarService->delete($uuid);
ApiResponse::noContent();

View File

@@ -3,14 +3,14 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
use MintyPHP\Service\User\UserServicesFactory;
ApiBootstrap::init();
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$userServicesFactory = new UserServicesFactory();
$request = requestInput();
$method = $request->method();
$userServicesFactory = app(UserServicesFactory::class);
$userAssignmentService = $userServicesFactory->createUserAssignmentService();
if ($method === 'GET') {
@@ -23,19 +23,17 @@ if ($method === 'GET') {
}
if ($method === 'PUT' || $method === 'PATCH') {
if (!ApiAuth::hasPermission(PermissionService::USERS_SELF_UPDATE)) {
ApiResponse::forbidden();
}
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_ME_SELF_UPDATE);
$userId = ApiAuth::userId();
$input = ApiResponse::readJsonBody();
$input = $request->bodyAll();
$userAccountService = $userServicesFactory->createUserAccountService();
$result = $userAccountService->updateSelfProfile($userId, $input);
if (!($result['ok'] ?? false)) {
$errors = $result['errors'] ?? [];
if (is_array($errors) && $errors !== []) {
ApiResponse::validationError(['profile' => array_values($errors)]);
ApiResponse::validationFromFormErrors(formErrors()->addMany('profile', array_values($errors)));
}
ApiResponse::fromServiceResult($result);
}
@@ -70,9 +68,9 @@ function buildMeResponse(array $user, int $userId, $userAssignmentService): arra
$primaryTenant = $userAssignmentService->findTenantSummaryInAssignments($assignments, $primaryTenantId);
$publicAssignments = $userAssignmentService->mapAssignmentsToPublic($assignments);
$canViewPermissions = ApiAuth::hasPermission(PermissionService::PERMISSIONS_VIEW);
$canViewPermissions = ApiAuth::can(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_PERMISSIONS_VIEW);
$publicPermissions = $canViewPermissions ? ApiAuth::permissions() : [];
$publicCustomFields = UserCustomFieldValueService::buildPublicValuesByTenant(
$publicCustomFields = app(UserCustomFieldValueService::class)->buildPublicValuesByTenant(
$userId,
$assignments['tenants'] ?? [],
ApiAuth::scopedTenantId()

View File

@@ -3,36 +3,35 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\User\UserServicesFactory;
ApiBootstrap::init();
ApiResponse::requireMethod('POST');
if (!ApiAuth::hasPermission(PermissionService::USERS_SELF_UPDATE)) {
ApiResponse::forbidden();
}
$request = requestInput();
$input = ApiResponse::readJsonBody();
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_ME_SELF_UPDATE);
$input = $request->bodyAll();
$currentPassword = (string) ($input['current_password'] ?? '');
$password = (string) ($input['password'] ?? '');
$password2 = (string) ($input['password2'] ?? '');
$validationErrors = [];
$validationErrors = formErrors();
if ($currentPassword === '') {
$validationErrors['current_password'] = ['required'];
$validationErrors->add('current_password', 'required');
}
if ($password === '') {
$validationErrors['password'] = ['required'];
$validationErrors->add('password', 'required');
}
if ($password2 === '') {
$validationErrors['password2'] = ['required'];
$validationErrors->add('password2', 'required');
}
if ($validationErrors) {
ApiResponse::validationError($validationErrors);
if ($validationErrors->hasAny()) {
ApiResponse::validationFromFormErrors($validationErrors);
}
$userServicesFactory = new UserServicesFactory();
$userServicesFactory = app(UserServicesFactory::class);
$userAccountService = $userServicesFactory->createUserAccountService();
$userPasswordService = $userServicesFactory->createUserPasswordService();
@@ -43,14 +42,14 @@ if (!$user) {
}
if (!password_verify($currentPassword, (string) ($user['password'] ?? ''))) {
ApiResponse::validationError(['current_password' => ['invalid']]);
ApiResponse::validationFromFormErrors(formErrorsFrom(['current_password' => ['invalid']]));
}
$result = $userPasswordService->resetPassword($userId, $password, $password2);
if (!($result['ok'] ?? false)) {
$errors = $result['errors'] ?? [];
if ($errors) {
ApiResponse::validationError(['password' => $errors]);
ApiResponse::validationFromFormErrors(formErrors()->addMany('password', $errors));
}
ApiResponse::error('password_change_failed', 422);
}

View File

@@ -3,13 +3,13 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Repository\Auth\ApiTokenRepository;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Service\Auth\ApiTokenEndpointService;
use MintyPHP\Service\Auth\AuthServicesFactory;
ApiBootstrap::init();
$apiTokenService = (new AuthServicesFactory())->createApiTokenService();
$apiTokenRepository = new ApiTokenRepository();
$apiTokenService = (app(AuthServicesFactory::class))->createApiTokenService();
$apiTokenEndpointService = app(ApiTokenEndpointService::class);
$request = requestInput();
$userId = ApiAuth::userId();
if ($userId <= 0) {
@@ -18,60 +18,12 @@ if ($userId <= 0) {
ApiAuth::requireSelfManageTokens();
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$method = $request->method();
if ($method === 'GET') {
$limit = (int) ($_GET['limit'] ?? 50);
if ($limit < 1) {
$limit = 1;
} elseif ($limit > 100) {
$limit = 100;
}
$limit = $apiTokenEndpointService->normalizeLimit($request->queryInt('limit', 50));
$scopedTenantId = ApiAuth::scopedTenantId();
$tokens = $apiTokenRepository->listByUserId($userId, $limit);
$tenantIds = [];
foreach ($tokens as $token) {
$tokenTenantId = isset($token['tenant_id']) ? (int) $token['tenant_id'] : 0;
if ($tokenTenantId > 0) {
$tenantIds[] = $tokenTenantId;
}
}
$tenantIds = array_values(array_unique($tenantIds));
$tenantUuidById = [];
if ($tenantIds) {
foreach ((new TenantRepository())->listByIds($tenantIds) as $tenant) {
$tenantId = (int) ($tenant['id'] ?? 0);
$tenantUuid = trim((string) ($tenant['uuid'] ?? ''));
if ($tenantId > 0 && $tenantUuid !== '') {
$tenantUuidById[$tenantId] = $tenantUuid;
}
}
}
$rows = [];
foreach ($tokens as $token) {
$tokenTenantId = isset($token['tenant_id']) ? (int) $token['tenant_id'] : null;
if ($tokenTenantId !== null && $tokenTenantId <= 0) {
$tokenTenantId = null;
}
if ($scopedTenantId !== null && $scopedTenantId > 0 && $tokenTenantId !== $scopedTenantId) {
continue;
}
$rows[] = [
'uuid' => (string) ($token['uuid'] ?? ''),
'name' => (string) ($token['name'] ?? ''),
'selector_prefix' => substr((string) ($token['selector'] ?? ''), 0, 8),
'tenant_uuid' => $tokenTenantId !== null ? ($tenantUuidById[$tokenTenantId] ?? null) : null,
'last_used_at' => $token['last_used_at'] ?? null,
'last_ip' => (string) ($token['last_ip'] ?? ''),
'expires_at' => $token['expires_at'] ?? null,
'revoked_at' => $token['revoked_at'] ?? null,
'created' => $token['created'] ?? null,
'is_current_token' => (int) ($token['id'] ?? 0) === (int) (ApiAuth::tokenId() ?? 0),
];
}
$rows = $apiTokenEndpointService->listSelfTokens($userId, $limit, $scopedTenantId, ApiAuth::tokenId());
ApiResponse::success([
'data' => $rows,
@@ -80,76 +32,47 @@ if ($method === 'GET') {
}
if ($method === 'POST') {
$input = ApiResponse::readJsonBody();
$input = $request->bodyAll();
$name = trim((string) ($input['name'] ?? ''));
if ($name === '') {
ApiResponse::validationError(['name' => [t('Token name is required')]]);
ApiResponse::validationFromFormErrors(formErrorsFrom(['name' => [t('Token name is required')]]));
}
if (array_key_exists('tenant_id', $input)) {
ApiResponse::validationError(['tenant_id' => ['use_tenant_uuid']]);
ApiResponse::validationFromFormErrors(formErrorsFrom(['tenant_id' => ['use_tenant_uuid']]));
}
$tenantId = null;
$tenantUuid = null;
$tenantUuidInput = trim((string) ($input['tenant_uuid'] ?? ''));
if ($tenantUuidInput !== '') {
$tenant = (new TenantRepository())->findByUuid($tenantUuidInput);
if (!$tenant) {
ApiResponse::validationError(['tenant_uuid' => ['invalid_tenant_uuid']]);
$tenantResolution = $apiTokenEndpointService->resolveSelfCreateTenant(
$userId,
ApiAuth::scopedTenantId(),
trim((string) ($input['tenant_uuid'] ?? ''))
);
if (!($tenantResolution['ok'] ?? false)) {
$status = (int) ($tenantResolution['status'] ?? 422);
$error = (string) ($tenantResolution['error'] ?? 'invalid_tenant_uuid');
$field = trim((string) ($tenantResolution['field'] ?? ''));
if ($status === 403) {
ApiResponse::forbidden($error);
}
$tenantId = (int) ($tenant['id'] ?? 0);
if ($tenantId <= 0) {
ApiResponse::validationError(['tenant_uuid' => ['invalid_tenant_uuid']]);
if ($field !== '') {
ApiResponse::validationFromFormErrors(formErrorsFrom([$field => [$error]]));
}
$tenantUuid = (string) ($tenant['uuid'] ?? '');
ApiResponse::error($error, $status);
}
$tenantId = array_key_exists('tenant_id', $tenantResolution) ? (int) ($tenantResolution['tenant_id'] ?? 0) : null;
if ($tenantId !== null && $tenantId <= 0) {
$tenantId = null;
}
$tenantUuid = (string) ($tenantResolution['tenant_uuid'] ?? '') ?: null;
$scopedTenantId = ApiAuth::scopedTenantId();
if ($scopedTenantId !== null && $scopedTenantId > 0) {
$scopedTenant = (new TenantRepository())->find($scopedTenantId);
$scopedTenantUuid = trim((string) ($scopedTenant['uuid'] ?? ''));
if ($scopedTenantUuid === '') {
ApiResponse::forbidden('tenant_scoped_token_forbidden');
}
if ($tenantId !== null && $tenantId !== $scopedTenantId) {
ApiResponse::forbidden('tenant_scoped_token_forbidden');
}
$tenantId = $scopedTenantId;
$tenantUuid = $scopedTenantUuid;
}
$expiresAt = null;
$expiresInput = trim((string) ($input['expires_at'] ?? ''));
if ($expiresInput !== '') {
try {
$expiresDate = new \DateTimeImmutable($expiresInput);
$expiresDateUtc = $expiresDate->setTimezone(new \DateTimeZone('UTC'));
$nowUtc = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
if ($expiresDateUtc <= $nowUtc) {
ApiResponse::validationError(['expires_at' => ['must_be_in_future']]);
}
$expiresAt = $expiresDateUtc->format('Y-m-d H:i:s');
} catch (\Exception $e) {
ApiResponse::validationError(['expires_at' => ['invalid_datetime']]);
}
$expiresParse = $apiTokenEndpointService->parseExpiresAt((string) ($input['expires_at'] ?? ''));
if (!($expiresParse['ok'] ?? false)) {
ApiResponse::validationFromFormErrors(formErrorsFrom(['expires_at' => [(string) ($expiresParse['error'] ?? 'invalid_datetime')]]));
}
$expiresAt = $expiresParse['expires_at'] ?? null;
$currentTokenRecord = ApiAuth::tokenRecord();
$parentExpiresAt = trim((string) ($currentTokenRecord['expires_at'] ?? ''));
if ($parentExpiresAt !== '') {
try {
$parentExpiry = new \DateTimeImmutable($parentExpiresAt, new \DateTimeZone('UTC'));
$requestedExpiry = $expiresAt !== null
? new \DateTimeImmutable($expiresAt, new \DateTimeZone('UTC'))
: null;
if ($requestedExpiry === null || $requestedExpiry > $parentExpiry) {
$expiresAt = $parentExpiresAt;
}
} catch (\Exception $e) {
// Parent token has no parseable expiry — keep requested value.
}
}
$expiresAt = $apiTokenEndpointService->capExpiryToParent($expiresAt, (string) ($currentTokenRecord['expires_at'] ?? ''));
$result = $apiTokenService->create($userId, $name, $tenantId, $expiresAt, $userId);
if (!($result['ok'] ?? false)) {

View File

@@ -3,12 +3,11 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Service\Auth\AuthServicesFactory;
ApiBootstrap::init();
ApiResponse::requireMethod('POST');
$apiTokenService = (new AuthServicesFactory())->createApiTokenService();
$apiTokenService = app(\MintyPHP\Service\Auth\ApiTokenService::class);
$apiTokenEndpointService = app(\MintyPHP\Service\Auth\ApiTokenEndpointService::class);
$userId = ApiAuth::userId();
if ($userId <= 0) {
@@ -18,14 +17,7 @@ if ($userId <= 0) {
ApiAuth::requireSelfManageTokens();
$scopedTenantId = ApiAuth::scopedTenantId();
$scopedTenantUuid = null;
if ($scopedTenantId !== null && $scopedTenantId > 0) {
$tenant = (new TenantRepository())->find($scopedTenantId);
$scopedTenantUuid = trim((string) ($tenant['uuid'] ?? ''));
if ($scopedTenantUuid === '') {
$scopedTenantUuid = null;
}
}
$scopedTenantUuid = $apiTokenEndpointService->resolveTenantUuidById($scopedTenantId);
$result = $apiTokenService->revokeAllForUser($userId, $scopedTenantId);
ApiResponse::success([

View File

@@ -3,12 +3,14 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Service\Auth\ApiTokenEndpointService;
use MintyPHP\Service\Auth\AuthServicesFactory;
ApiBootstrap::init();
ApiResponse::requireMethod('POST');
$apiTokenService = (new AuthServicesFactory())->createApiTokenService();
$apiTokenService = (app(AuthServicesFactory::class))->createApiTokenService();
$apiTokenEndpointService = app(ApiTokenEndpointService::class);
$request = requestInput();
$userId = ApiAuth::userId();
if ($userId <= 0) {
@@ -22,7 +24,7 @@ if ($currentTokenId <= 0) {
ApiResponse::error('current_token_not_found', 404);
}
$input = ApiResponse::readJsonBody();
$input = $request->bodyAll();
$expiresInput = trim((string) ($input['expires_at'] ?? ''));
$expiresAt = $expiresInput !== '' ? $expiresInput : null;
@@ -44,11 +46,7 @@ if (!($result['ok'] ?? false)) {
$tenantUuid = null;
$tenantId = isset($result['tenant_id']) ? (int) ($result['tenant_id']) : 0;
if ($tenantId > 0) {
$tenant = (new TenantRepository())->find($tenantId);
$tenantUuid = trim((string) ($tenant['uuid'] ?? ''));
if ($tenantUuid === '') {
$tenantUuid = null;
}
$tenantUuid = $apiTokenEndpointService->resolveTenantUuidById($tenantId);
}
ApiResponse::success([

View File

@@ -3,11 +3,11 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Repository\Auth\ApiTokenRepository;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Service\Auth\ApiTokenEndpointService;
ApiBootstrap::init();
$apiTokenRepository = new ApiTokenRepository();
$apiTokenEndpointService = app(ApiTokenEndpointService::class);
$request = requestInput();
$userId = ApiAuth::userId();
if ($userId <= 0) {
@@ -17,33 +17,12 @@ if ($userId <= 0) {
ApiAuth::requireSelfManageTokens();
$tokenUuid = trim((string) ($id ?? ''));
if (!$apiTokenRepository->isUuid($tokenUuid)) {
ApiResponse::notFound();
}
$token = $apiTokenRepository->findByUuidForUser($tokenUuid, $userId);
$token = $apiTokenEndpointService->findSelfToken($tokenUuid, $userId, ApiAuth::scopedTenantId(), ApiAuth::tokenId());
if (!$token) {
ApiResponse::notFound();
}
$scopedTenantId = ApiAuth::scopedTenantId();
$tokenTenantId = isset($token['tenant_id']) ? (int) $token['tenant_id'] : null;
if ($tokenTenantId !== null && $tokenTenantId <= 0) {
$tokenTenantId = null;
}
if ($scopedTenantId !== null && $scopedTenantId > 0 && $tokenTenantId !== $scopedTenantId) {
ApiResponse::notFound();
}
$tenantUuid = null;
if ($tokenTenantId !== null) {
$tenant = (new TenantRepository())->find($tokenTenantId);
$tenantUuid = trim((string) ($tenant['uuid'] ?? ''));
if ($tenantUuid === '') {
$tenantUuid = null;
}
}
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$method = $request->method();
if ($method === 'GET') {
ApiResponse::success([
@@ -51,7 +30,7 @@ if ($method === 'GET') {
'uuid' => (string) ($token['uuid'] ?? ''),
'name' => (string) ($token['name'] ?? ''),
'selector_prefix' => substr((string) ($token['selector'] ?? ''), 0, 8),
'tenant_uuid' => $tenantUuid,
'tenant_uuid' => $token['tenant_uuid'] ?? null,
'last_used_at' => $token['last_used_at'] ?? null,
'last_ip' => (string) ($token['last_ip'] ?? ''),
'expires_at' => $token['expires_at'] ?? null,
@@ -63,7 +42,7 @@ if ($method === 'GET') {
}
if ($method === 'DELETE') {
$revoked = $apiTokenRepository->revokeByUuidForUser($tokenUuid, $userId);
$revoked = $apiTokenEndpointService->revokeSelfToken($tokenUuid, $userId);
if (!$revoked) {
ApiResponse::error('revoke_failed', 422);
}

View File

@@ -3,25 +3,27 @@
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\AccessServicesFactory;
use MintyPHP\Service\Access\PermissionService;
ApiBootstrap::init();
ApiResponse::requireMethod('GET');
ApiResponse::requirePermission(PermissionService::PERMISSIONS_VIEW);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_PERMISSIONS_VIEW);
$request = requestInput();
$permissionService = (new AccessServicesFactory())->createPermissionService();
$permissionService = (app(AccessServicesFactory::class))->createPermissionService();
$allowedOrders = ['key', 'description', 'active', 'created'];
$order = in_array($_GET['order'] ?? '', $allowedOrders, true) ? $_GET['order'] : 'key';
$dir = in_array(strtolower($_GET['dir'] ?? ''), ['asc', 'desc'], true) ? strtolower($_GET['dir']) : 'asc';
$limit = max(1, min(100, (int) ($_GET['limit'] ?? 25)));
$offset = max(0, (int) ($_GET['offset'] ?? 0));
$orderParam = $request->queryString('order');
$dirParam = strtolower($request->queryString('dir'));
$order = in_array($orderParam, $allowedOrders, true) ? $orderParam : 'key';
$dir = in_array($dirParam, ['asc', 'desc'], true) ? $dirParam : 'asc';
$limit = max(1, min(100, $request->queryInt('limit', 25)));
$offset = max(0, $request->queryInt('offset', 0));
$options = [
'limit' => $limit,
'offset' => $offset,
'search' => trim((string) ($_GET['search'] ?? '')),
'active' => $_GET['active'] ?? null,
'search' => $request->queryString('search'),
'active' => $request->query('active'),
'order' => $order,
'dir' => $dir,
];
@@ -29,7 +31,7 @@ $options = [
$result = $permissionService->listPaged($options);
$rows = [];
foreach (($result['data'] ?? []) as $row) {
foreach (($result['rows'] ?? []) as $row) {
$rows[] = [
'key' => $row['key'] ?? '',
'description' => $row['description'] ?? '',

View File

@@ -3,21 +3,23 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\PermissionService;
ApiBootstrap::init();
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$request = requestInput();
$method = $request->method();
if ($method === 'GET') {
ApiResponse::requirePermission(PermissionService::ROLES_VIEW);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_ROLES_VIEW);
$result = directoryServicesFactory()->createRoleService()->listPaged([
'limit' => (int) ($_GET['limit'] ?? 25),
'offset' => (int) ($_GET['offset'] ?? 0),
'search' => trim((string) ($_GET['search'] ?? '')),
'order' => (string) ($_GET['order'] ?? 'description'),
'dir' => (string) ($_GET['dir'] ?? 'asc'),
$limit = $request->queryInt('limit', 25);
$offset = $request->queryInt('offset', 0);
$result = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createRoleService()->listPaged([
'limit' => $limit,
'offset' => $offset,
'search' => $request->queryString('search'),
'order' => $request->queryString('order', 'description'),
'dir' => $request->queryString('dir', 'asc'),
]);
$rows = [];
@@ -34,16 +36,16 @@ if ($method === 'GET') {
ApiResponse::success([
'data' => $rows,
'total' => $result['total'] ?? 0,
'limit' => (int) ($_GET['limit'] ?? 25),
'offset' => (int) ($_GET['offset'] ?? 0),
'limit' => $limit,
'offset' => $offset,
]);
}
if ($method === 'POST') {
ApiResponse::requirePermission(PermissionService::ROLES_CREATE);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_ROLES_CREATE);
$input = ApiResponse::readJsonBody();
$result = directoryServicesFactory()->createRoleService()->createFromAdmin($input, ApiAuth::userId());
$input = $request->bodyAll();
$result = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createRoleService()->createFromAdmin($input, ApiAuth::userId());
ApiResponse::fromServiceResult($result, 201);
}

View File

@@ -3,31 +3,29 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\AccessServicesFactory;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Repository\Access\RolePermissionRepository;
ApiBootstrap::init();
$request = requestInput();
$uuid = trim((string) ($id ?? ''));
if ($uuid === '') {
ApiResponse::notFound();
}
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$method = $request->method();
if ($method === 'GET') {
ApiResponse::requirePermission(PermissionService::ROLES_VIEW);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_ROLES_VIEW);
$role = directoryServicesFactory()->createRoleService()->findByUuid($uuid);
$role = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createRoleService()->findByUuid($uuid);
if (!$role) {
ApiResponse::notFound();
}
$roleId = (int) ($role['id'] ?? 0);
$permissionKeys = [];
if ($roleId > 0) {
$permissionKeys = (new AccessServicesFactory())
->createRolePermissionRepository()
->listPermissionKeysByRoleIds([$roleId]);
$permissionKeys = app(RolePermissionRepository::class)->listPermissionKeysByRoleIds([$roleId]);
sort($permissionKeys);
}
@@ -45,23 +43,23 @@ if ($method === 'GET') {
}
if ($method === 'PUT' || $method === 'PATCH') {
ApiResponse::requirePermission(PermissionService::ROLES_UPDATE);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_ROLES_UPDATE);
$role = directoryServicesFactory()->createRoleService()->findByUuid($uuid);
$role = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createRoleService()->findByUuid($uuid);
if (!$role) {
ApiResponse::notFound();
}
$roleId = (int) ($role['id'] ?? 0);
$input = ApiResponse::readJsonBody();
$result = directoryServicesFactory()->createRoleService()->updateFromAdmin($roleId, $input, ApiAuth::userId());
$input = $request->bodyAll();
$result = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createRoleService()->updateFromAdmin($roleId, $input, ApiAuth::userId());
ApiResponse::fromServiceResult($result);
}
if ($method === 'DELETE') {
ApiResponse::requirePermission(PermissionService::ROLES_DELETE);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_ROLES_DELETE);
$result = directoryServicesFactory()->createRoleService()->deleteByUuid($uuid);
$result = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createRoleService()->deleteByUuid($uuid);
ApiResponse::fromServiceResult($result);
}

View File

@@ -1,17 +1,19 @@
<?php
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Service\Settings\SettingServicesFactory;
ApiBootstrap::init();
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$settingService = (new SettingServicesFactory())->createSettingService();
$request = requestInput();
$method = $request->method();
$settingService = (app(SettingServicesFactory::class))->createSettingService();
if ($method === 'GET') {
ApiResponse::requirePermission(PermissionService::SETTINGS_VIEW);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_SETTINGS_VIEW);
ApiResponse::success([
'data' => buildSettingsResponse($settingService),
@@ -19,10 +21,11 @@ if ($method === 'GET') {
}
if ($method === 'PUT' || $method === 'PATCH') {
ApiResponse::requirePermission(PermissionService::SETTINGS_UPDATE);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_SETTINGS_UPDATE);
$input = ApiResponse::readJsonBody();
$errors = [];
$input = $request->bodyAll();
$errors = formErrors();
$beforeSettings = buildSettingsResponse($settingService);
$setters = [
'app_title' => static fn(?string $v) => $settingService->setAppTitle($v),
@@ -39,6 +42,8 @@ if ($method === 'PUT' || $method === 'PATCH') {
'default_department_id' => static fn($v) => $settingService->setDefaultDepartmentId($v !== null ? (int) $v : null),
'user_inactivity_deactivate_days' => static fn($v) => $settingService->setUserInactivityDeactivateDays($v !== null ? (int) $v : null),
'user_inactivity_delete_days' => static fn($v) => $settingService->setUserInactivityDeleteDays($v !== null ? (int) $v : null),
'system_audit_enabled' => static fn($v) => $settingService->setSystemAuditEnabled((bool) $v),
'system_audit_retention_days' => static fn($v) => $settingService->setSystemAuditRetentionDays($v !== null ? (int) $v : null),
'microsoft_shared_client_id' => static fn(?string $v) => $settingService->setMicrosoftSharedClientId($v),
'microsoft_authority' => static fn(?string $v) => $settingService->setMicrosoftAuthority($v),
'smtp_host' => static fn(?string $v) => $settingService->setSmtpHost($v),
@@ -56,16 +61,28 @@ if ($method === 'PUT' || $method === 'PATCH') {
}
$ok = $setters[$key]($value);
if (!$ok) {
$errors[$key] = ['invalid_value'];
$errors->add($key, 'invalid_value');
}
}
if ($errors) {
ApiResponse::validationError($errors);
if ($errors->hasAny()) {
app(SystemAuditService::class)->record('admin.settings.update', 'failed', [
'actor_user_id' => ApiAuth::userId(),
'error_code' => 'validation_error',
'metadata' => ['errors' => array_keys($errors->toArray())],
]);
ApiResponse::validationFromFormErrors($errors);
}
$afterSettings = buildSettingsResponse($settingService);
app(SystemAuditService::class)->record('admin.settings.update', 'success', [
'actor_user_id' => ApiAuth::userId(),
'before' => $beforeSettings,
'after' => $afterSettings,
]);
ApiResponse::success([
'data' => buildSettingsResponse($settingService),
'data' => $afterSettings,
]);
}
@@ -92,6 +109,8 @@ function buildSettingsResponse(\MintyPHP\Service\Settings\SettingService $s): ar
'default_department_id' => $s->getDefaultDepartmentId(),
'user_inactivity_deactivate_days' => $s->getUserInactivityDeactivateDays(),
'user_inactivity_delete_days' => $s->getUserInactivityDeleteDays(),
'system_audit_enabled' => $s->isSystemAuditEnabled(),
'system_audit_retention_days' => $s->getSystemAuditRetentionDays(),
'microsoft_shared_client_id' => $s->getMicrosoftSharedClientId(),
'microsoft_authority' => $s->getMicrosoftAuthority(),
'smtp_host' => $s->getSmtpHost(),

View File

@@ -2,12 +2,11 @@
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Settings\SettingServicesFactory;
ApiBootstrap::init(requireAuth: false);
ApiResponse::requireMethod('GET');
$settingService = (new SettingServicesFactory())->createSettingService();
$settingService = app(\MintyPHP\Service\Settings\SettingService::class);
header('Cache-Control: public, max-age=60');

View File

@@ -3,14 +3,14 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Tenant\TenantServicesFactory;
define('MINTY_ALLOW_OUTPUT', true);
ApiBootstrap::init();
ApiResponse::requireMethod('GET');
ApiResponse::requirePermission(PermissionService::TENANTS_VIEW);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_TENANTS_VIEW);
$request = requestInput();
$uuid = trim((string) ($id ?? ''));
if ($uuid === '') {
@@ -20,7 +20,7 @@ if ($uuid === '') {
return;
}
$tenantAvatarService = (new TenantServicesFactory())->createTenantAvatarService();
$tenantAvatarService = (app(TenantServicesFactory::class))->createTenantAvatarService();
if (!$tenantAvatarService->isValidUuid($uuid)) {
http_response_code(404);
@@ -29,7 +29,7 @@ if (!$tenantAvatarService->isValidUuid($uuid)) {
return;
}
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($uuid);
$tenant = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createTenantService()->findByUuid($uuid);
$tenantId = (int) ($tenant['id'] ?? 0);
if ($tenantId <= 0) {
http_response_code(404);
@@ -40,7 +40,7 @@ if ($tenantId <= 0) {
ApiAuth::requireResourceAccess('tenants', $tenantId);
$size = isset($_GET['size']) ? (int) $_GET['size'] : null;
$size = $request->hasQuery('size') ? $request->queryInt('size') : null;
$path = $tenantAvatarService->findAvatarPath($uuid, $size);
if (!$path || !is_file($path)) {
http_response_code(404);

View File

@@ -3,28 +3,30 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\PermissionService;
ApiBootstrap::init();
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$request = requestInput();
$method = $request->method();
if ($method === 'GET') {
ApiResponse::requirePermission(PermissionService::TENANTS_VIEW);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_TENANTS_VIEW);
$limit = $request->queryInt('limit', 25);
$offset = $request->queryInt('offset', 0);
$options = [
'limit' => (int) ($_GET['limit'] ?? 25),
'offset' => (int) ($_GET['offset'] ?? 0),
'search' => trim((string) ($_GET['search'] ?? '')),
'order' => (string) ($_GET['order'] ?? 'description'),
'dir' => (string) ($_GET['dir'] ?? 'asc'),
'limit' => $limit,
'offset' => $offset,
'search' => $request->queryString('search'),
'order' => $request->queryString('order', 'description'),
'dir' => $request->queryString('dir', 'asc'),
'tenantUserId' => ApiAuth::userId(),
];
$scopedTenantId = ApiAuth::scopedTenantId();
if ($scopedTenantId) {
$options['tenantIds'] = [$scopedTenantId];
}
$result = directoryServicesFactory()->createTenantService()->listPaged($options);
$result = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createTenantService()->listPaged($options);
$rows = [];
foreach ($result['rows'] as $row) {
@@ -41,19 +43,19 @@ if ($method === 'GET') {
ApiResponse::success([
'data' => $rows,
'total' => $result['total'] ?? 0,
'limit' => (int) ($_GET['limit'] ?? 25),
'offset' => (int) ($_GET['offset'] ?? 0),
'limit' => $limit,
'offset' => $offset,
]);
}
if ($method === 'POST') {
ApiResponse::requirePermission(PermissionService::TENANTS_CREATE);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_TENANTS_CREATE);
if (ApiAuth::isTenantScopedToken()) {
ApiResponse::forbidden('tenant_scoped_token_forbidden');
}
$input = ApiResponse::readJsonBody();
$result = directoryServicesFactory()->createTenantService()->createFromAdmin($input, ApiAuth::userId());
$input = $request->bodyAll();
$result = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createTenantService()->createFromAdmin($input, ApiAuth::userId());
ApiResponse::fromServiceResult($result, 201);
}

View File

@@ -3,21 +3,21 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\PermissionService;
ApiBootstrap::init();
$request = requestInput();
$uuid = trim((string) ($id ?? ''));
if ($uuid === '') {
ApiResponse::notFound();
}
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$method = $request->method();
if ($method === 'GET') {
ApiResponse::requirePermission(PermissionService::TENANTS_VIEW);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_TENANTS_VIEW);
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($uuid);
$tenant = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createTenantService()->findByUuid($uuid);
if (!$tenant) {
ApiResponse::notFound();
}
@@ -63,9 +63,9 @@ if ($method === 'GET') {
}
if ($method === 'PUT' || $method === 'PATCH') {
ApiResponse::requirePermission(PermissionService::TENANTS_UPDATE);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_TENANTS_UPDATE);
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($uuid);
$tenant = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createTenantService()->findByUuid($uuid);
if (!$tenant) {
ApiResponse::notFound();
}
@@ -73,15 +73,15 @@ if ($method === 'PUT' || $method === 'PATCH') {
$tenantId = (int) ($tenant['id'] ?? 0);
ApiAuth::requireResourceAccess('tenants', $tenantId);
$input = ApiResponse::readJsonBody();
$result = directoryServicesFactory()->createTenantService()->updateFromAdmin($tenantId, $input, ApiAuth::userId());
$input = $request->bodyAll();
$result = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createTenantService()->updateFromAdmin($tenantId, $input, ApiAuth::userId());
ApiResponse::fromServiceResult($result);
}
if ($method === 'DELETE') {
ApiResponse::requirePermission(PermissionService::TENANTS_DELETE);
ApiResponse::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_TENANTS_DELETE);
$tenant = directoryServicesFactory()->createTenantService()->findByUuid($uuid);
$tenant = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createTenantService()->findByUuid($uuid);
if (!$tenant) {
ApiResponse::notFound();
}
@@ -89,7 +89,7 @@ if ($method === 'DELETE') {
$tenantId = (int) ($tenant['id'] ?? 0);
ApiAuth::requireResourceAccess('tenants', $tenantId);
$result = directoryServicesFactory()->createTenantService()->deleteByUuid($uuid);
$result = app(\MintyPHP\Service\Directory\DirectoryServicesFactory::class)->createTenantService()->deleteByUuid($uuid);
ApiResponse::fromServiceResult($result);
}

View File

@@ -3,15 +3,16 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\AccessServicesFactory;
use MintyPHP\Service\Access\AuthorizationDecision;
use MintyPHP\Service\Access\UserAuthorizationPolicy;
use MintyPHP\Service\User\UserServicesFactory;
use MintyPHP\Service\User\UserAccountService;
use MintyPHP\Service\User\UserAvatarService;
define('MINTY_ALLOW_OUTPUT', true);
ApiBootstrap::init();
ApiResponse::requireMethod('GET');
$request = requestInput();
$uuid = trim((string) ($id ?? ''));
if ($uuid === '') {
@@ -21,14 +22,13 @@ if ($uuid === '') {
return;
}
$userServicesFactory = new UserServicesFactory();
$userAccountService = $userServicesFactory->createUserAccountService();
$userAvatarService = $userServicesFactory->createUserAvatarService();
$userAccountService = app(UserAccountService::class);
$userAvatarService = app(UserAvatarService::class);
$user = $userAccountService->findByUuid($uuid);
$userId = (int) ($user['id'] ?? 0);
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
$authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class);
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_GET, [
'actor_user_id' => ApiAuth::userId(),
'target_user_id' => $userId,
@@ -50,7 +50,7 @@ if (!$user) {
return;
}
$size = isset($_GET['size']) ? (int) $_GET['size'] : null;
$size = $request->hasQuery('size') ? $request->queryInt('size') : null;
$path = $userAvatarService->findAvatarPath($uuid, $size);
if (!$path || !is_file($path)) {
http_response_code(404);

View File

@@ -3,15 +3,16 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\AccessServicesFactory;
use MintyPHP\Service\Access\AuthorizationDecision;
use MintyPHP\Service\Access\UserAuthorizationPolicy;
use MintyPHP\Service\User\UserServicesFactory;
use MintyPHP\Service\Tenant\TenantService;
use MintyPHP\Service\User\UserAccountService;
use MintyPHP\Service\User\UserApiWriteInputMapper;
ApiBootstrap::init();
$authorizationService = (new AccessServicesFactory())->createAuthorizationService();
$userServicesFactory = new UserServicesFactory();
$userAccountService = $userServicesFactory->createUserAccountService();
$request = requestInput();
$authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class);
$userAccountService = app(UserAccountService::class);
$apiDeny = static function (AuthorizationDecision $decision): void {
if ($decision->isAllowed()) {
return;
@@ -32,8 +33,7 @@ $apiDeny = static function (AuthorizationDecision $decision): void {
ApiResponse::error($error, $status > 0 ? $status : 403);
};
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$method = $request->method();
if ($method === 'GET') {
$decision = $authorizationService->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_INDEX_GET, [
@@ -41,19 +41,21 @@ if ($method === 'GET') {
]);
$apiDeny($decision);
$limit = max(1, min(100, (int) ($_GET['limit'] ?? 25)));
$offset = max(0, (int) ($_GET['offset'] ?? 0));
$limit = max(1, min(100, $request->queryInt('limit', 25)));
$offset = max(0, $request->queryInt('offset', 0));
$allowedOrders = ['id', 'uuid', 'first_name', 'last_name', 'display_name', 'email', 'created', 'modified', 'active', 'last_login_at'];
$order = in_array($_GET['order'] ?? '', $allowedOrders, true) ? $_GET['order'] : 'last_name';
$dir = in_array(strtolower($_GET['dir'] ?? ''), ['asc', 'desc'], true) ? strtolower($_GET['dir']) : 'asc';
$activeParam = $_GET['active'] ?? null;
$orderParam = $request->queryString('order');
$order = in_array($orderParam, $allowedOrders, true) ? $orderParam : 'last_name';
$dirParam = strtolower($request->queryString('dir'));
$dir = in_array($dirParam, ['asc', 'desc'], true) ? $dirParam : 'asc';
$activeParam = $request->query('active');
if ($activeParam !== null) {
$activeParam = in_array($activeParam, ['0', '1', 'true', 'false'], true) ? $activeParam : null;
}
$options = [
'limit' => $limit,
'offset' => $offset,
'search' => trim((string) ($_GET['search'] ?? '')),
'search' => $request->queryString('search'),
'order' => $order,
'dir' => $dir,
'active' => $activeParam,
@@ -61,7 +63,7 @@ if ($method === 'GET') {
];
$scopedTenantId = ApiAuth::scopedTenantId();
if ($scopedTenantId) {
$tenant = directoryServicesFactory()->createTenantService()->findById($scopedTenantId);
$tenant = app(TenantService::class)->findById($scopedTenantId);
$tenantUuid = trim((string) ($tenant['uuid'] ?? ''));
if ($tenantUuid === '') {
ApiResponse::notFound();
@@ -98,11 +100,11 @@ if ($method === 'GET') {
}
if ($method === 'POST') {
$input = ApiResponse::readJsonBody();
$normalizeResult = normalizeUserWriteInputToInternalIds($input);
$input = $request->bodyAll();
$normalizeResult = app(UserApiWriteInputMapper::class)->normalize($input);
if ($normalizeResult['ok'] !== true) {
$errors = $normalizeResult['errors'] ?? [];
ApiResponse::validationError($errors ?: ['input' => ['invalid_value']]);
ApiResponse::validationFromFormErrors(formErrorsFrom($errors ?: ['input' => ['invalid_value']]));
}
$input = $normalizeResult['input'] ?? $input;
@@ -122,118 +124,3 @@ if ($method === 'POST') {
}
ApiResponse::methodNotAllowed();
/**
* Normalize public UUID-based assignment payload to internal IDs used by services.
*
* @param array<string, mixed> $input
* @return array{ok: bool, input?: array<string, mixed>, errors?: array<string, array<int, string>>}
*/
function normalizeUserWriteInputToInternalIds(array $input): array
{
$errors = [];
foreach ([
'tenant_ids' => 'use_tenant_uuids',
'primary_tenant_id' => 'use_primary_tenant_uuid',
'role_ids' => 'use_role_uuids',
'department_ids' => 'use_department_uuids',
] as $legacyKey => $errorCode) {
if (array_key_exists($legacyKey, $input)) {
$errors[$legacyKey] = [$errorCode];
}
}
if ($errors) {
return ['ok' => false, 'errors' => $errors];
}
$tenantService = directoryServicesFactory()->createTenantService();
$roleService = directoryServicesFactory()->createRoleService();
$departmentService = directoryServicesFactory()->createDepartmentService();
$normalizeUuidList = static function (mixed $raw): array {
$values = is_array($raw) ? $raw : [$raw];
$uuids = [];
foreach ($values as $value) {
$uuid = trim((string) $value);
if ($uuid !== '') {
$uuids[] = $uuid;
}
}
return array_values(array_unique($uuids));
};
if (array_key_exists('tenant_uuids', $input)) {
$tenantUuids = $normalizeUuidList($input['tenant_uuids']);
$tenantIds = [];
foreach ($tenantUuids as $tenantUuid) {
$tenant = $tenantService->findByUuid($tenantUuid);
if (!$tenant) {
$errors['tenant_uuids'][] = 'not_found';
continue;
}
$tenantId = (int) ($tenant['id'] ?? 0);
if ($tenantId > 0) {
$tenantIds[] = $tenantId;
}
}
$input['tenant_ids'] = array_values(array_unique($tenantIds));
unset($input['tenant_uuids']);
}
if (array_key_exists('primary_tenant_uuid', $input)) {
$primaryTenantUuid = trim((string) ($input['primary_tenant_uuid'] ?? ''));
if ($primaryTenantUuid === '') {
$input['primary_tenant_id'] = 0;
} else {
$primaryTenant = $tenantService->findByUuid($primaryTenantUuid);
if (!$primaryTenant) {
$errors['primary_tenant_uuid'][] = 'not_found';
} else {
$input['primary_tenant_id'] = (int) ($primaryTenant['id'] ?? 0);
}
}
unset($input['primary_tenant_uuid']);
}
if (array_key_exists('role_uuids', $input)) {
$roleUuids = $normalizeUuidList($input['role_uuids']);
$roleIds = [];
foreach ($roleUuids as $roleUuid) {
$role = $roleService->findByUuid($roleUuid);
if (!$role) {
$errors['role_uuids'][] = 'not_found';
continue;
}
$roleId = (int) ($role['id'] ?? 0);
if ($roleId > 0) {
$roleIds[] = $roleId;
}
}
$input['role_ids'] = array_values(array_unique($roleIds));
unset($input['role_uuids']);
}
if (array_key_exists('department_uuids', $input)) {
$departmentUuids = $normalizeUuidList($input['department_uuids']);
$departmentIds = [];
foreach ($departmentUuids as $departmentUuid) {
$department = $departmentService->findByUuid($departmentUuid);
if (!$department) {
$errors['department_uuids'][] = 'not_found';
continue;
}
$departmentId = (int) ($department['id'] ?? 0);
if ($departmentId > 0) {
$departmentIds[] = $departmentId;
}
}
$input['department_ids'] = array_values(array_unique($departmentIds));
unset($input['department_uuids']);
}
if ($errors) {
return ['ok' => false, 'errors' => $errors];
}
return ['ok' => true, 'input' => $input];
}

View File

@@ -3,21 +3,20 @@
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\ApiBootstrap;
use MintyPHP\Http\ApiResponse;
use MintyPHP\Service\Access\AccessServicesFactory;
use MintyPHP\Service\Access\AuthorizationDecision;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Access\UserAuthorizationPolicy;
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
use MintyPHP\Service\User\UserServicesFactory;
use MintyPHP\Service\User\UserAccountService;
use MintyPHP\Service\User\UserApiWriteInputMapper;
use MintyPHP\Service\User\UserAssignmentService;
ApiBootstrap::init();
$accessServicesFactory = new AccessServicesFactory();
$authorizationService = $accessServicesFactory->createAuthorizationService();
$userServicesFactory = new UserServicesFactory();
$userAccountService = $userServicesFactory->createUserAccountService();
$userAssignmentService = $userServicesFactory->createUserAssignmentService();
$userRoleRepository = $userServicesFactory->createUserRoleRepository();
$rolePermissionRepository = $accessServicesFactory->createRolePermissionRepository();
$request = requestInput();
$authorizationService = app(\MintyPHP\Service\Access\AuthorizationService::class);
$userAccountService = app(UserAccountService::class);
$userAssignmentService = app(UserAssignmentService::class);
$userRoleRepository = app(\MintyPHP\Repository\Access\UserRoleRepository::class);
$rolePermissionRepository = app(\MintyPHP\Repository\Access\RolePermissionRepository::class);
$apiDeny = static function (AuthorizationDecision $decision): void {
if ($decision->isAllowed()) {
return;
@@ -44,7 +43,7 @@ if ($uuid === '') {
ApiResponse::notFound();
}
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$method = $request->method();
if ($method === 'GET') {
$user = $userAccountService->findByUuid($uuid);
@@ -65,13 +64,13 @@ if ($method === 'GET') {
$primaryTenantId = (int) ($user['primary_tenant_id'] ?? 0);
$primaryTenant = $userAssignmentService->findTenantSummaryInAssignments($assignments, $primaryTenantId);
$publicAssignments = $userAssignmentService->mapAssignmentsToPublic($assignments);
$publicCustomFields = UserCustomFieldValueService::buildPublicValuesByTenant(
$publicCustomFields = app(UserCustomFieldValueService::class)->buildPublicValuesByTenant(
$userId,
$assignments['tenants'] ?? [],
ApiAuth::scopedTenantId()
);
$publicPermissions = [];
if (ApiAuth::hasPermission(PermissionService::PERMISSIONS_VIEW)) {
if (ApiAuth::can(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_PERMISSIONS_VIEW)) {
$roleIds = $userRoleRepository->listRoleIdsByUserId($userId);
$publicPermissions = $rolePermissionRepository->listPermissionKeysByRoleIds($roleIds);
}
@@ -115,11 +114,11 @@ if ($method === 'PUT' || $method === 'PATCH') {
$user = $userAccountService->findByUuid($uuid);
$userId = (int) ($user['id'] ?? 0);
$input = ApiResponse::readJsonBody();
$normalizeResult = normalizeUserWriteInputToInternalIds($input);
$input = $request->bodyAll();
$normalizeResult = app(UserApiWriteInputMapper::class)->normalize($input);
if ($normalizeResult['ok'] !== true) {
$errors = $normalizeResult['errors'] ?? [];
ApiResponse::validationError($errors ?: ['input' => ['invalid_value']]);
ApiResponse::validationFromFormErrors(formErrorsFrom($errors ?: ['input' => ['invalid_value']]));
}
$input = $normalizeResult['input'] ?? $input;
@@ -160,118 +159,3 @@ if ($method === 'DELETE') {
}
ApiResponse::methodNotAllowed();
/**
* Normalize public UUID-based assignment payload to internal IDs used by services.
*
* @param array<string, mixed> $input
* @return array{ok: bool, input?: array<string, mixed>, errors?: array<string, array<int, string>>}
*/
function normalizeUserWriteInputToInternalIds(array $input): array
{
$errors = [];
foreach ([
'tenant_ids' => 'use_tenant_uuids',
'primary_tenant_id' => 'use_primary_tenant_uuid',
'role_ids' => 'use_role_uuids',
'department_ids' => 'use_department_uuids',
] as $legacyKey => $errorCode) {
if (array_key_exists($legacyKey, $input)) {
$errors[$legacyKey] = [$errorCode];
}
}
if ($errors) {
return ['ok' => false, 'errors' => $errors];
}
$tenantService = directoryServicesFactory()->createTenantService();
$roleService = directoryServicesFactory()->createRoleService();
$departmentService = directoryServicesFactory()->createDepartmentService();
$normalizeUuidList = static function (mixed $raw): array {
$values = is_array($raw) ? $raw : [$raw];
$uuids = [];
foreach ($values as $value) {
$uuid = trim((string) $value);
if ($uuid !== '') {
$uuids[] = $uuid;
}
}
return array_values(array_unique($uuids));
};
if (array_key_exists('tenant_uuids', $input)) {
$tenantUuids = $normalizeUuidList($input['tenant_uuids']);
$tenantIds = [];
foreach ($tenantUuids as $tenantUuid) {
$tenant = $tenantService->findByUuid($tenantUuid);
if (!$tenant) {
$errors['tenant_uuids'][] = 'not_found';
continue;
}
$tenantId = (int) ($tenant['id'] ?? 0);
if ($tenantId > 0) {
$tenantIds[] = $tenantId;
}
}
$input['tenant_ids'] = array_values(array_unique($tenantIds));
unset($input['tenant_uuids']);
}
if (array_key_exists('primary_tenant_uuid', $input)) {
$primaryTenantUuid = trim((string) ($input['primary_tenant_uuid'] ?? ''));
if ($primaryTenantUuid === '') {
$input['primary_tenant_id'] = 0;
} else {
$primaryTenant = $tenantService->findByUuid($primaryTenantUuid);
if (!$primaryTenant) {
$errors['primary_tenant_uuid'][] = 'not_found';
} else {
$input['primary_tenant_id'] = (int) ($primaryTenant['id'] ?? 0);
}
}
unset($input['primary_tenant_uuid']);
}
if (array_key_exists('role_uuids', $input)) {
$roleUuids = $normalizeUuidList($input['role_uuids']);
$roleIds = [];
foreach ($roleUuids as $roleUuid) {
$role = $roleService->findByUuid($roleUuid);
if (!$role) {
$errors['role_uuids'][] = 'not_found';
continue;
}
$roleId = (int) ($role['id'] ?? 0);
if ($roleId > 0) {
$roleIds[] = $roleId;
}
}
$input['role_ids'] = array_values(array_unique($roleIds));
unset($input['role_uuids']);
}
if (array_key_exists('department_uuids', $input)) {
$departmentUuids = $normalizeUuidList($input['department_uuids']);
$departmentIds = [];
foreach ($departmentUuids as $departmentUuid) {
$department = $departmentService->findByUuid($departmentUuid);
if (!$department) {
$errors['department_uuids'][] = 'not_found';
continue;
}
$departmentId = (int) ($department['id'] ?? 0);
if ($departmentId > 0) {
$departmentIds[] = $departmentId;
}
}
$input['department_ids'] = array_values(array_unique($departmentIds));
unset($input['department_uuids']);
}
if ($errors) {
return ['ok' => false, 'errors' => $errors];
}
return ['ok' => true, 'input' => $input];
}