listen ansichten verbessert

This commit is contained in:
2026-03-05 11:17:42 +01:00
parent 4b31fc7664
commit c5f657c8c8
133 changed files with 2806 additions and 636 deletions

19
.php-cs-fixer.dist.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
$finder = PhpCsFixer\Finder::create()
->in([
__DIR__ . '/config',
__DIR__ . '/lib',
__DIR__ . '/pages',
__DIR__ . '/tests',
])
->name('*.php');
return (new PhpCsFixer\Config())
->setRiskyAllowed(false)
->setRules([
'@PSR12' => true,
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
])
->setFinder($finder);

View File

@@ -68,7 +68,11 @@ docker/ # Dockerfiles, Nginx configs, PHP configs
1. **App** (`lib/App/`): DI container bootstrap. Registers all service/repository factories. No business logic.
2. **Repository** (`lib/Repository/`): All SQL lives here. No HTTP, session, or rendering.
3. **Service** (`lib/Service/`): Business rules and validation. No HTML. Uses factory pattern (`*ServicesFactory`).
3. **Service** (`lib/Service/`): Business rules and validation. No HTML. Four internal sub-types:
- **Service** (`*Service.php`): Business logic + orchestration. Coordinates repositories and gateways.
- **Gateway** (`*Gateway.php`): Adapter for a single external dependency (repository, settings, HTTP API, scope). No orchestration flow.
- **Factory** (`*Factory.php`): Only place inside `lib/Service/` that may call `new` on services/gateways/repositories. Registers via DI container.
- **Policy** (`*Policy.php` in `lib/Service/Access/`): Authorization decisions per resource type. Returns `AuthorizationDecision`. No HTTP, no business logic.
4. **Action** (`pages/**/*.php`): Reads input, checks permissions + tenant scope, calls services, sets view vars.
5. **View** (`pages/**/*.phtml`): Pure rendering. **No DB calls.** HTML escaping via `e(...)`.
6. **Template** (`templates/`): Layouts and partials.

View File

@@ -193,6 +193,20 @@ docker compose exec php vendor/bin/phpunit --bootstrap vendor/autoload.php tests
docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress
```
### Coding Style (PHP CS Fixer)
CI-Check (ohne Dateien zu aendern):
```bash
docker compose exec php composer cs:check
```
Lokal automatisch formatieren:
```bash
docker compose exec php composer cs:fix
```
### Frontend-JS Smoke-Check (ohne Node)
- Browser-DevTools öffnen (Console + Preserve log)

View File

@@ -22,7 +22,8 @@
"mintyphp/debugger": "*",
"phpunit/phpunit": "*",
"phpstan/phpstan": "^1.9",
"icanhazstring/composer-unused": "^0.9.6"
"icanhazstring/composer-unused": "^0.9.6",
"friendsofphp/php-cs-fixer": "^3.66"
},
"autoload-dev": {
"psr-4": {
@@ -33,5 +34,9 @@
"psr-4": {
"MintyPHP\\": "lib/"
}
},
"scripts": {
"cs:check": "php-cs-fixer fix --config=.php-cs-fixer.dist.php --dry-run --diff --verbose",
"cs:fix": "php-cs-fixer fix --config=.php-cs-fixer.dist.php --verbose"
}
}

1404
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
<?php
return [
['path' => 'login', 'target' => 'auth/login', 'public' => true],
['path' => 'register', 'target' => 'auth/register', 'public' => true],

View File

@@ -1,4 +1,5 @@
<?php
return [
'app_title' => 'CoreCore',
'app_locale' => 'de',

View File

@@ -1,4 +1,5 @@
<?php
return [
'light' => 'Light',
'dark' => 'Dark',

View File

@@ -13,6 +13,54 @@ Verbindliche Kurzregeln für Architekturentscheidungen.
- Autorisierung läuft zentral über Policies.
- Externe API-Verträge sind UUID-first.
## Schichtenübersicht
```mermaid
flowchart TD
Browser([Browser / API-Client])
subgraph APP["lib/App/ — Composition Root"]
DI["AppContainer + Registrars"]
end
subgraph SVC["lib/Service/"]
direction LR
FAC["Factory"]
SERVICE["Service"]
GW["Gateway"]
POL["Policy"]
end
subgraph REPO["lib/Repository/"]
REP["Repository"]
end
subgraph pages["pages/**"]
ACT["Action (*.php)"]
VIEW["View (*.phtml)"]
end
subgraph TPL["templates/"]
LAY["Layouts & Partials"]
end
DB[(MariaDB)]
DI -->|"ruft zur Laufzeit"| FAC
FAC -->|"erzeugt"| SERVICE
FAC -->|"erzeugt"| GW
Browser --> ACT
ACT -->|"Input · AuthZ · Response"| SERVICE
ACT --> POL
ACT --> VIEW
VIEW --> LAY
SERVICE --> GW
GW --> REP
SERVICE --> REP
REP --> DB
```
## Request-Flow
1. `web/index.php` bootstrappt Routing/Konfiguration und initialisiert den `AppContainer`.

View File

@@ -95,6 +95,45 @@ Hinweise:
- `order` nur bekannte Sort-Key-Spalten
- Bei Search-/Filter-Änderung wird auf Seite 1 zurückgesetzt.
### Row Interaction (Globaler Standard)
Listen mit `rowDblClick.getUrl(...)` nutzen standardmäßig eine zugängliche Row-Interaktion:
- sichtbare Action-Spalte rechts (`Open`/`Edit`)
- Doppelklick bleibt als Fallback aktiv
- `Enter` auf fokussierter Zeile navigiert zur selben URL
Optionale Konfiguration über `createServerGrid(...)`:
```js
createServerGrid({
// ...
rowDblClick: {
getUrl: (rowData) => new URL(`admin/users/edit/${rowData?.cells?.[1]?.data}`, appBase).toString(),
},
rowInteraction: {
enabled: true,
showActionButton: true,
keepDblClick: true,
enableEnterOnRow: true,
actionColumnLabel: 'Actions',
resolveActionLabel: (url) => (url.includes('/edit/') ? 'Edit' : 'Open'),
}
});
```
Defaults:
- `rowInteraction.enabled`: `true`, wenn `rowDblClick.getUrl` vorhanden ist
- `showActionButton`: `true`
- `keepDblClick`: `true`
- `enableEnterOnRow`: `true`
- `actionColumnLabel`: `language.actions.column` oder `'Actions'`
Hinweis:
- Falls eine Seite `actions.enabled: true` explizit setzt, wird keine zusätzliche automatische Row-Action-Spalte injiziert.
### Filter UX 2.0 (Globaler Standard)
Für Listen gilt standardmäßig:
@@ -275,5 +314,5 @@ Prüfen im Browser:
1. DevTools öffnen (Console, optional Preserve log aktivieren).
2. Kernseiten laden (`/admin/users`, `/address-book`, Audit-Listen).
3. Filter setzen/zurücksetzen, Doppelklick-Navigation und Bulk-Aktionen testen.
3. Filter setzen/zurücksetzen, Row-Action-Button + Enter + Doppelklick sowie Bulk-Aktionen testen.
4. Erwartung: keine JavaScript-Errors und keine Unhandled Promise Rejections.

View File

@@ -1,6 +1,6 @@
# lib-Standards
Letzte Aktualisierung: 2026-03-03
Letzte Aktualisierung: 2026-03-05
Praktische Regeln nur für `lib/**`. Ergänzt `docs/architektur.md`.
@@ -9,12 +9,17 @@ Praktische Regeln nur für `lib/**`. Ergänzt `docs/architektur.md`.
- Verantwortlich für SQL, Filter, Paging und Mapping.
- Keine HTTP-, Session-, Redirect- oder Policy-Logik.
## 2) Service-Regeln
## 2) Service-Schicht im Detail
`lib/Service/` enthält vier klar getrennte Typen — jeder hat eine eigene Verantwortung:
### 2a) Service (`*Service.php`)
- Verantwortlich für Fachlogik und Orchestrierung.
- Koordiniert Repositories und Gateways; enthält Business-Regeln und Validierung.
- Keine Superglobals (`$_POST`, `$_GET`, `$_SESSION`).
- Keine Header- oder Redirect-Ausgabe.
- Keine direkten `DB::...`-Aufrufe im Service.
- Keine direkten `DB::...`-Aufrufe.
- Abhängigkeiten per Constructor Injection.
- DB-Locks und Transaktionen über Repository-Schicht (z. B. `DatabaseSessionRepository`).
- Audit-Hooks für fachliche Schreibaktionen laufen in Services (`SystemAuditService`), nicht in Templates.
@@ -22,6 +27,28 @@ Praktische Regeln nur für `lib/**`. Ergänzt `docs/architektur.md`.
- Scheduler-Run-/Job-Ereignisse werden zentral im `SchedulerRunService` in `SystemAuditService` geschrieben.
- CLI-System-Audit für zentrale Commands wird am Entrypoint (`bin/**`) geschrieben, mit minimaler Metadata (ohne Argumentwerte).
### 2b) Gateway (`*Gateway.php`)
- Adapter-Schicht: kapselt eine einzelne externe Abhängigkeit für Services.
- Typische Abhängigkeiten: ein Repository, Settings-Zugriff, externe HTTP-APIs, Scope-Daten.
- Kein Fach-Orchestrierungsfluss — ein Gateway löst genau eine Infrastruktur-Frage.
- Beispiele: `AuthPermissionGateway` (Permission-Lookup), `UserSettingsGateway` (Settings-Zugriff), `OidcHttpGateway` (externe OIDC-API).
- Abhängigkeiten per Constructor Injection; keine direkten `new ...Repository()` außerhalb von Factories.
### 2c) Factory (`*Factory.php` / `*ServicesFactory.php` / `*GatewayFactory.php`)
- Einzige erlaubte Stelle für `new ...Service()`, `new ...Gateway()`, `new ...Repository()` innerhalb von `lib/Service/`.
- Erzeugt konfigurierte Service-/Gateway-Gruppen für den DI-Container.
- Keine Fachlogik; reine Kompositions-Helfer.
- Werden im Composition Root (`lib/App/registerContainer.php`) registriert.
### 2d) Policy (`*Policy.php` / `*AuthorizationPolicy.php`)
- Liegen in `lib/Service/Access/`.
- Treffen Authorization-Entscheidungen (Ability-Checks) für einen bestimmten Ressourcentyp.
- Geben `AuthorizationDecision` zurück; kein HTTP-Redirect, keine Fachlogik.
- Werden über `AccessPolicyFactory` + `AuthorizationService` aufgelöst — nie direkt in Actions instanziiert.
## 3) Instanziierung
- Composition Root ist `web/index.php` + `lib/App/registerContainer.php`

View File

@@ -198,6 +198,7 @@
"Modified by": "Bearbeitet von",
"Modified": "Aktualisiert",
"No users found": "Keine Benutzer gefunden",
"Open": "Öffnen",
"Yes": "Ja",
"No": "Nein",
"User created": "Benutzer erstellt",
@@ -345,6 +346,7 @@
"Next": "Weiter",
"Page %d of %d": "Seite %d von %d",
"Page %d": "Seite %d",
"Rows per page": "Einträge pro Seite",
"Showing": "Zeige",
"of": "von",
"to": "bis",

View File

@@ -198,6 +198,7 @@
"Modified by": "Modified by",
"Modified": "Modified",
"No users found": "No users found",
"Open": "Open",
"Yes": "Yes",
"No": "No",
"User created": "User created",
@@ -345,6 +346,7 @@
"Next": "Next",
"Page %d of %d": "Page %d of %d",
"Page %d": "Page %d",
"Rows per page": "Rows per page",
"Showing": "Showing",
"of": "of",
"to": "to",

View File

@@ -11,7 +11,6 @@ use MintyPHP\Service\Access\AccessPolicyFactory;
use MintyPHP\Service\Access\AccessRepositoryFactory;
use MintyPHP\Service\Access\AccessServicesFactory;
use MintyPHP\Service\Access\PermissionGateway;
use MintyPHP\Service\Directory\DirectoryScopeGateway;
use MintyPHP\Service\AddressBook\AddressBookServicesFactory;
use MintyPHP\Service\Audit\AuditRepositoryFactory;
use MintyPHP\Service\Audit\AuditServicesFactory;
@@ -22,6 +21,7 @@ use MintyPHP\Service\Branding\BrandingServicesFactory;
use MintyPHP\Service\CustomField\CustomFieldServicesFactory;
use MintyPHP\Service\Directory\DirectoryGatewayFactory;
use MintyPHP\Service\Directory\DirectoryRepositoryFactory;
use MintyPHP\Service\Directory\DirectoryScopeGateway;
use MintyPHP\Service\Directory\DirectoryServicesFactory;
use MintyPHP\Service\Import\ImportRepositoryFactory;
use MintyPHP\Service\Import\ImportServicesFactory;
@@ -75,7 +75,6 @@ final class ServiceFactoryRegistrar implements ContainerRegistrar
$c->get(UserServicesFactory::class),
$c->get(AccessServicesFactory::class),
$c->get(SettingServicesFactory::class),
$c->get(UserRepositoryFactory::class),
$c->get(DirectoryServicesFactory::class),
$c->get(ImportRepositoryFactory::class)
));
@@ -114,10 +113,10 @@ final class ServiceFactoryRegistrar implements ContainerRegistrar
$c->get(DirectoryScopeGateway::class)
));
$container->set(UserGatewayFactory::class, static fn (AppContainer $c): UserGatewayFactory => new UserGatewayFactory(
$c->get(UserRepositoryFactory::class),
$c->get(AccessServicesFactory::class),
$c->get(SettingServicesFactory::class),
$c->get(TenantScopeService::class)
$c->get(TenantScopeService::class),
$c->get(DirectoryRepositoryFactory::class),
));
$container->set(UserServicesFactory::class, static fn (AppContainer $c): UserServicesFactory => new UserServicesFactory(
$c->get(AuditServicesFactory::class),

View File

@@ -7,6 +7,7 @@ use MintyPHP\App\Container\ContainerRegistrar;
use MintyPHP\Repository\Auth\ApiTokenRepository;
use MintyPHP\Repository\Auth\RememberTokenRepository;
use MintyPHP\Service\Access\RoleService;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Service\Branding\BrandingFaviconService;
use MintyPHP\Service\Branding\BrandingLogoService;
use MintyPHP\Service\Branding\BrandingServicesFactory;
@@ -18,7 +19,6 @@ use MintyPHP\Service\Settings\SettingService;
use MintyPHP\Service\Settings\SettingServicesFactory;
use MintyPHP\Service\Settings\ThemeConfigService;
use MintyPHP\Service\Tenant\TenantService;
use MintyPHP\Service\Audit\SystemAuditService;
final class SettingsRegistrar implements ContainerRegistrar
{

View File

@@ -6,14 +6,15 @@ use MintyPHP\App\AppContainer;
use MintyPHP\App\Container\ContainerRegistrar;
use MintyPHP\Repository\Access\UserRoleRepository;
use MintyPHP\Repository\Org\UserDepartmentRepository;
use MintyPHP\Repository\Tenant\UserTenantRepository;
use MintyPHP\Repository\User\UserReadRepository;
use MintyPHP\Repository\User\UserWriteRepository;
use MintyPHP\Repository\Tenant\UserTenantRepository;
use MintyPHP\Service\Auth\TenantSsoService;
use MintyPHP\Service\Branding\BrandingLogoService;
use MintyPHP\Service\User\UserAccessPdfService;
use MintyPHP\Service\User\UserAccessTemplateService;
use MintyPHP\Service\User\UserAccountService;
use MintyPHP\Service\User\UserApiWriteInputMapper;
use MintyPHP\Service\User\UserAssignmentService;
use MintyPHP\Service\User\UserAvatarService;
use MintyPHP\Service\User\UserDirectoryGateway;
@@ -21,7 +22,6 @@ use MintyPHP\Service\User\UserLifecycleRestoreService;
use MintyPHP\Service\User\UserLifecycleService;
use MintyPHP\Service\User\UserPasswordPolicyService;
use MintyPHP\Service\User\UserPasswordService;
use MintyPHP\Service\User\UserApiWriteInputMapper;
use MintyPHP\Service\User\UserRepositoryFactory;
use MintyPHP\Service\User\UserSavedFilterService;
use MintyPHP\Service\User\UserScopeGateway;

View File

@@ -2,7 +2,6 @@
namespace MintyPHP\Http;
use MintyPHP\Http\RequestContext;
use MintyPHP\Service\Audit\ApiAuditService;
use MintyPHP\Service\Security\RateLimiterService;
use MintyPHP\Service\Settings\SettingGateway;

View File

@@ -3,7 +3,6 @@
namespace MintyPHP\Http;
use MintyPHP\Http\Input\FormErrors;
use MintyPHP\Http\RequestContext;
use MintyPHP\Service\Access\AuthorizationService;
use MintyPHP\Service\Audit\ApiAuditService;
@@ -20,8 +19,7 @@ class ApiResponse
callable $apiAuditServiceResolver,
callable $authorizationServiceResolver,
callable $apiSystemAuditReporterResolver
): void
{
): void {
self::$apiAuditServiceResolver = $apiAuditServiceResolver;
self::$authorizationServiceResolver = $authorizationServiceResolver;
self::$apiSystemAuditReporterResolver = $apiSystemAuditReporterResolver;

View File

@@ -2,8 +2,8 @@
namespace MintyPHP\Http;
use MintyPHP\Router;
use MintyPHP\I18n;
use MintyPHP\Router;
class Request
{

View File

@@ -2,8 +2,8 @@
namespace MintyPHP\Repository\Audit;
use MintyPHP\Domain\Taxonomy\ImportAuditStatus;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\ImportAuditStatus;
use MintyPHP\Repository\Support\RepoQuery;
class ImportAuditRunRepository implements ImportAuditRunRepositoryInterface

View File

@@ -2,9 +2,9 @@
namespace MintyPHP\Repository\Audit;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\SystemAuditChannel;
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
class SystemAuditLogRepository implements SystemAuditLogRepositoryInterface

View File

@@ -2,10 +2,10 @@
namespace MintyPHP\Repository\Audit;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\UserLifecycleAction;
use MintyPHP\Domain\Taxonomy\UserLifecycleStatus;
use MintyPHP\Domain\Taxonomy\UserLifecycleTriggerType;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
class UserLifecycleAuditRepository implements UserLifecycleAuditRepositoryInterface

View File

@@ -2,8 +2,8 @@
namespace MintyPHP\Repository\Mail;
use MintyPHP\Domain\Taxonomy\MailLogStatus;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\MailLogStatus;
use MintyPHP\Repository\Support\RepoQuery;
class MailLogRepository implements MailLogRepositoryInterface

View File

@@ -2,8 +2,8 @@
namespace MintyPHP\Repository\Scheduler;
use MintyPHP\Domain\Taxonomy\ScheduledJobStatus;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\ScheduledJobStatus;
use MintyPHP\Repository\Support\RepoQuery;
class ScheduledJobRepository implements ScheduledJobRepositoryInterface

View File

@@ -2,9 +2,9 @@
namespace MintyPHP\Repository\Scheduler;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\ScheduledJobRunStatus;
use MintyPHP\Domain\Taxonomy\ScheduledJobTriggerType;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
class ScheduledJobRunRepository implements ScheduledJobRunRepositoryInterface

View File

@@ -2,8 +2,8 @@
namespace MintyPHP\Repository\Scheduler;
use MintyPHP\Domain\Taxonomy\SchedulerRuntimeResult;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\SchedulerRuntimeResult;
class SchedulerRuntimeRepository implements SchedulerRuntimeRepositoryInterface
{

View File

@@ -2,6 +2,7 @@
namespace MintyPHP\Repository\Stats;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\ImportAuditStatus;
use MintyPHP\Domain\Taxonomy\MailLogStatus;
use MintyPHP\Domain\Taxonomy\ScheduledJobRunStatus;
@@ -10,7 +11,6 @@ use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
use MintyPHP\Domain\Taxonomy\TenantStatus;
use MintyPHP\Domain\Taxonomy\UserLifecycleAction;
use MintyPHP\Domain\Taxonomy\UserLifecycleStatus;
use MintyPHP\DB;
class AdminStatsRepository implements AdminStatsRepositoryInterface
{
@@ -43,7 +43,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$inactiveDepartmentCount = (int) (DB::selectValue('select count(*) from departments where active = 0') ?? 0);
$activeRoleCount = (int) (DB::selectValue('select count(*) from roles where active = 1') ?? 0);
$inactiveRoleCount = (int) (DB::selectValue('select count(*) from roles where active = 0') ?? 0);
$rolesWithoutPermissionsCount = (int) (DB::selectValue(
'select count(*) from roles r left join role_permissions rp on rp.role_id = r.id where r.active = 1 and rp.role_id is null'
) ?? 0);
@@ -56,7 +56,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$usersWithoutTenantsCount = (int) (DB::selectValue(
'select count(*) from users u left join user_tenants ut on ut.user_id = u.id where u.active = 1 and ut.user_id is null'
) ?? 0);
$flattenStatsRow = static function ($row): array {
if (!is_array($row)) {
return [];
@@ -74,7 +74,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
}
return $flat;
};
$ssoStatsAvailable = (int) (DB::selectValue(
"select count(*) from information_schema.tables where table_schema = database() and table_name = 'tenant_auth_microsoft'"
) ?? 0) === 1;
@@ -86,7 +86,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$ssoTenantRows = [];
$usersMicrosoftOnlyRows = [];
$usersMicrosoftSyncGapRows = [];
if ($ssoStatsAvailable) {
$ssoEnabledTenantCount = (int) (DB::selectValue(
"select count(*) from tenants t " .
@@ -103,7 +103,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
if ($activeTenantCount > 0) {
$ssoEnabledTenantPercent = round(((float) $ssoEnabledTenantCount * 100) / (float) $activeTenantCount, 1);
}
$usersMicrosoftOnlyCount = (int) (DB::selectValue(
"select count(*) from users u " .
"where u.active = 1 " .
@@ -122,7 +122,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$tenantActiveStatus,
$tenantActiveStatus
) ?? 0);
$usersMicrosoftSyncGapsCount = (int) (DB::selectValue(
"select count(*) from ( " .
" select u.id, " .
@@ -147,7 +147,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
" or (x.need_mobile = 1 and x.mobile_value = '')",
$tenantActiveStatus
) ?? 0);
$ssoTenantRowsRaw = DB::select(
"select " .
" t.uuid as tenant_uuid, " .
@@ -182,7 +182,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
},
$ssoTenantRowsRaw
))) : [];
$usersMicrosoftOnlyRowsRaw = DB::select(
"select " .
" u.uuid as user_uuid, " .
@@ -217,7 +217,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
},
$usersMicrosoftOnlyRowsRaw
))) : [];
$usersMicrosoftSyncGapRowsRaw = DB::select(
"select " .
" x.user_uuid, x.user_display_name, x.user_email, " .
@@ -278,7 +278,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$usersMicrosoftSyncGapRowsRaw
))) : [];
}
$customFieldStatsAvailable = (int) (DB::selectValue(
"select count(*) from information_schema.tables " .
"where table_schema = database() " .
@@ -293,7 +293,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$customFieldsInactiveWithValuesHref = 'admin/tenants';
$customFieldsUnusedActiveHref = 'admin/tenants';
$customFieldIssueTenantRows = [];
if ($customFieldStatsAvailable) {
$loadCustomFieldIssueTenants = static function (string $query): array {
$rows = DB::select($query);
@@ -318,7 +318,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
}
return $items;
};
$customFieldsSelectionWithoutOptionsCount = (int) (DB::selectValue(
"select count(*) from ( " .
"select d.id " .
@@ -329,7 +329,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
"having count(o.id) = 0 " .
') missing_options'
) ?? 0);
$customFieldTenantsWithSelectionWithoutOptionsCount = (int) (DB::selectValue(
"select count(distinct missing_options.tenant_id) from ( " .
"select d.tenant_id " .
@@ -340,13 +340,13 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
"having count(o.id) = 0 " .
') missing_options'
) ?? 0);
$customFieldsInactiveWithValuesCount = (int) (DB::selectValue(
'select count(distinct d.id) from tenant_custom_field_definitions d ' .
'inner join user_custom_field_values v on v.definition_id = d.id ' .
'where d.active = 0'
) ?? 0);
$customFieldsUnusedActiveCount = (int) (DB::selectValue(
"select count(*) from ( " .
"select d.id " .
@@ -357,7 +357,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
"having count(v.id) = 0 " .
') unused_active'
) ?? 0);
$selectionWithoutOptionsTenantUuid = (string) (DB::selectValue(
"select t.uuid from tenants t " .
"inner join ( " .
@@ -372,7 +372,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
') issue on issue.tenant_id = t.id ' .
'limit 1'
) ?? '');
$inactiveWithValuesTenantUuid = (string) (DB::selectValue(
"select t.uuid from tenants t " .
"inner join ( " .
@@ -386,7 +386,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
') issue on issue.tenant_id = t.id ' .
'limit 1'
) ?? '');
$unusedActiveTenantUuid = (string) (DB::selectValue(
"select t.uuid from tenants t " .
"inner join ( " .
@@ -401,7 +401,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
') issue on issue.tenant_id = t.id ' .
'limit 1'
) ?? '');
if ($selectionWithoutOptionsTenantUuid !== '') {
$customFieldsSelectionWithoutOptionsHref = 'admin/tenants/edit/' . $selectionWithoutOptionsTenantUuid . '?tab=custom_fields';
$customFieldTenantsWithSelectionWithoutOptionsHref = $customFieldsSelectionWithoutOptionsHref;
@@ -412,7 +412,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
if ($unusedActiveTenantUuid !== '') {
$customFieldsUnusedActiveHref = 'admin/tenants/edit/' . $unusedActiveTenantUuid . '?tab=custom_fields';
}
$selectionIssueTenants = $loadCustomFieldIssueTenants(
"select t.uuid, t.description, count(*) as issue_count " .
"from tenants t " .
@@ -436,7 +436,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
'issue_count' => $row['issue_count'],
];
}
$inactiveIssueTenants = $loadCustomFieldIssueTenants(
"select t.uuid, t.description, count(distinct d.id) as issue_count " .
"from tenants t " .
@@ -454,7 +454,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
'issue_count' => $row['issue_count'],
];
}
$unusedIssueTenants = $loadCustomFieldIssueTenants(
"select t.uuid, t.description, count(*) as issue_count " .
"from tenants t " .
@@ -479,7 +479,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
];
}
}
$apiStatsAvailable = (int) (DB::selectValue(
"select count(*) from information_schema.tables " .
"where table_schema = database() and table_name in ('api_audit_log', 'user_api_tokens')"
@@ -493,7 +493,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$apiTokensExpiring7dCount = 0;
$apiTopErrorRows = [];
$apiSlowEndpointRows = [];
if ($apiStatsAvailable) {
$apiRequests24hCount = (int) (DB::selectValue(
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR)'
@@ -504,14 +504,14 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
if ($apiRequestsPrev24hCount > 0) {
$apiRequestTrendPercent = round((($apiRequests24hCount - $apiRequestsPrev24hCount) * 100) / $apiRequestsPrev24hCount, 1);
}
$apiErrors4xx24hCount = (int) (DB::selectValue(
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 400 and 499'
) ?? 0);
$apiErrors5xx24hCount = (int) (DB::selectValue(
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 500 and 599'
) ?? 0);
$durationCount = (int) (DB::selectValue(
'select count(*) from api_audit_log where created_at >= (NOW() - INTERVAL 24 HOUR) and status_code between 200 and 299 and duration_ms is not null'
) ?? 0);
@@ -523,12 +523,12 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
'order by duration_ms asc limit ' . $offset . ', 1'
) ?? 0);
}
$apiTokensExpiring7dCount = (int) (DB::selectValue(
'select count(*) from user_api_tokens ' .
'where revoked_at is null and expires_at is not null and expires_at > NOW() and expires_at <= (NOW() + INTERVAL 7 DAY)'
) ?? 0);
$apiTopErrorRowsRaw = DB::select(
"select " .
" coalesce(nullif(error_code, ''), concat('http_', status_code)) as error_label, " .
@@ -554,7 +554,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
},
$apiTopErrorRowsRaw
))) : [];
$apiSlowEndpointRowsRaw = DB::select(
"select " .
" path, " .
@@ -586,7 +586,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$apiSlowEndpointRowsRaw
))) : [];
}
$importStatsAvailable = (int) (DB::selectValue(
"select count(*) from information_schema.tables " .
"where table_schema = database() and table_name = 'import_audit_runs'"
@@ -602,7 +602,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$importRecentRunRows = [];
$importProfileRows = [];
$importTopErrorCodeRows = [];
if ($importStatsAvailable) {
$importRuns7dCount = (int) (DB::selectValue(
"select count(*) from import_audit_runs where started_at >= (NOW() - INTERVAL 7 DAY)"
@@ -613,7 +613,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
if ($importRunsPrev7dCount > 0) {
$importRunTrendPercent = round((($importRuns7dCount - $importRunsPrev7dCount) * 100) / $importRunsPrev7dCount, 1);
}
$importRowsCreated7dCount = (int) (DB::selectValue(
"select coalesce(sum(created_count), 0) from import_audit_runs where started_at >= (NOW() - INTERVAL 7 DAY)"
) ?? 0);
@@ -635,7 +635,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
if ($importRuns7dCount > 0) {
$importSuccessRate7dPercent = round(($importSuccessRuns7dCount * 100) / $importRuns7dCount, 1);
}
$importRecentRunRowsRaw = DB::select(
"select " .
" import_audit_runs.id, " .
@@ -680,7 +680,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
},
$importRecentRunRowsRaw
))) : [];
$importProfileRowsRaw = DB::select(
"select " .
" profile_key, " .
@@ -715,7 +715,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
},
$importProfileRowsRaw
))) : [];
$importTopErrorRowsRaw = DB::select(
"select error_codes_json from import_audit_runs " .
"where started_at >= (NOW() - INTERVAL 7 DAY) " .
@@ -1001,7 +1001,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$lifecycleRecentRiskRowsRaw
))) : [];
}
$scheduledStatsAvailable = (int) (DB::selectValue(
"select count(*) from information_schema.tables " .
"where table_schema = database() and table_name in ('scheduled_jobs', 'scheduled_job_runs', 'scheduler_runtime_status')"
@@ -1014,7 +1014,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$schedulerRunnerLastResult = '';
$schedulerRunnerLastErrorCode = '';
$schedulerRunnerIsActive = false;
if ($scheduledStatsAvailable) {
$scheduledEnabledJobsCount = (int) (DB::selectValue(
"select count(*) from scheduled_jobs where enabled = 1"
@@ -1034,12 +1034,12 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$schedulerTriggerScheduler,
$schedulerRunStatusFailed
) ?? 0);
$schedulerRunnerIsActive = (int) (DB::selectValue(
"select case when last_heartbeat_at >= (NOW() - INTERVAL 3 MINUTE) then 1 else 0 end " .
"from scheduler_runtime_status where id = 1"
) ?? 0) === 1;
$schedulerRuntimeRowRaw = DB::selectOne(
"select last_heartbeat_at, last_result, last_error_code from scheduler_runtime_status where id = 1 limit 1"
);
@@ -1048,7 +1048,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
$schedulerRunnerLastResult = trim((string) ($schedulerRuntimeRow['last_result'] ?? ''));
$schedulerRunnerLastErrorCode = trim((string) ($schedulerRuntimeRow['last_error_code'] ?? ''));
}
$usersUnverifiedCount = (int) (DB::selectValue(
'select count(*) from users where active = 1 and email_verified_at is null'
) ?? 0);
@@ -1069,7 +1069,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
static fn ($row) => $row['users'] ?? $row,
$neverLoggedUsers
) : [];
$mailLogFailedCount = (int) (DB::selectValue('select count(*) from mail_log where status = ?', $mailStatusFailed) ?? 0);
$mailLogSentCount = (int) (DB::selectValue('select count(*) from mail_log where status = ?', $mailStatusSent) ?? 0);
$mailLogLastSentAt = (string) (DB::selectValue('select max(sent_at) from mail_log where status = ?', $mailStatusSent) ?? '');
@@ -1081,7 +1081,7 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface
static fn ($row) => $row['mail_log'] ?? $row,
$mailLogRecentFailed
) : [];
$vars = get_defined_vars();
unset($vars['this']);

View File

@@ -2,8 +2,8 @@
namespace MintyPHP\Repository\Tenant;
use MintyPHP\Domain\Taxonomy\TenantStatus;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\TenantStatus;
use MintyPHP\Repository\Support\RepoQuery;
class TenantRepository implements TenantRepositoryInterface

View File

@@ -12,8 +12,7 @@ class AccessGatewayFactory
public function __construct(
private readonly AccessRepositoryFactory $accessRepositoryFactory,
private readonly AuditServicesFactory $auditServicesFactory
)
{
) {
}
public function createPermissionService(): PermissionService

View File

@@ -47,4 +47,3 @@ class AuthorizationDecision
return array_key_exists($key, $this->attributes) ? $this->attributes[$key] : $default;
}
}

View File

@@ -8,4 +8,3 @@ interface AuthorizationPolicyInterface
public function authorize(string $ability, array $context = []): AuthorizationDecision;
}

View File

@@ -2,8 +2,8 @@
namespace MintyPHP\Service\AddressBook;
use MintyPHP\Service\Directory\DirectoryScopeGateway;
use MintyPHP\Service\Access\RoleService;
use MintyPHP\Service\Directory\DirectoryScopeGateway;
use MintyPHP\Service\Org\DepartmentService;
use MintyPHP\Service\Tenant\TenantService;

View File

@@ -17,7 +17,8 @@ class AddressBookServicesFactory
private readonly UserServicesFactory $userServicesFactory,
private readonly DirectoryServicesFactory $directoryServicesFactory,
private readonly CustomFieldServicesFactory $customFieldServicesFactory
) {}
) {
}
public function createAddressBookService(): AddressBookService
{

View File

@@ -17,7 +17,8 @@ class AuditServicesFactory
public function __construct(
private readonly AuditRepositoryFactory $auditRepositoryFactory,
private readonly SettingGateway $settingGateway
) {}
) {
}
public function createApiAuditLogRepository(): ApiAuditLogRepositoryInterface
{

View File

@@ -4,8 +4,8 @@ namespace MintyPHP\Service\Audit;
use MintyPHP\Domain\Taxonomy\SystemAuditChannel;
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
use MintyPHP\Http\RequestContext;
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\RequestContext;
use MintyPHP\Repository\Audit\SystemAuditLogRepositoryInterface;
use MintyPHP\Repository\Support\RepoQuery;
use MintyPHP\Service\Settings\SettingGateway;
@@ -27,8 +27,7 @@ class SystemAuditService
string $eventType,
string $outcome = SystemAuditOutcome::Success->value,
array $context = []
): ?int
{
): ?int {
if (!$this->isEnabled()) {
return null;
}

View File

@@ -6,9 +6,9 @@ use MintyPHP\Auth;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Router;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Service\User\UserAccountService;
use MintyPHP\Service\User\UserTenantContextService;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Session;
use MintyPHP\Support\Flash;

View File

@@ -26,7 +26,8 @@ class AuthServicesFactory
private readonly AuthRepositoryFactory $authRepositoryFactory,
private readonly AuthGatewayFactory $authGatewayFactory,
private readonly DatabaseSessionRepository $databaseSessionRepository
) {}
) {
}
public function createApiTokenService(): ApiTokenService
{

View File

@@ -6,7 +6,6 @@ use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Service\User\UserAssignmentService;
use MintyPHP\Service\User\UserAvatarService;
class SsoUserLinkService
{

View File

@@ -2,9 +2,9 @@
namespace MintyPHP\Service\Content;
use MintyPHP\Repository\Content\PageRepository;
use MintyPHP\Repository\Content\PageContentRepository;
use MintyPHP\I18n;
use MintyPHP\Repository\Content\PageContentRepository;
use MintyPHP\Repository\Content\PageRepository;
class PageService
{
@@ -59,8 +59,7 @@ class PageService
string $content,
int $currentUserId = 0,
?string $locale = null
): array
{
): array {
$slug = trim($slug);
if ($slug === '') {
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];

View File

@@ -22,7 +22,8 @@ class DirectoryServicesFactory
private readonly AuditServicesFactory $auditServicesFactory,
private readonly DirectoryRepositoryFactory $directoryRepositoryFactory,
private readonly DirectoryGatewayFactory $directoryGatewayFactory
) {}
) {
}
public function createTenantService(): TenantService
{

View File

@@ -143,8 +143,12 @@ trait ImageUploadTrait
$scale = min($width / $srcWidth, $height / $srcHeight);
$dstWidth = (int) round($srcWidth * $scale);
$dstHeight = (int) round($srcHeight * $scale);
if ($dstWidth < 1) $dstWidth = 1;
if ($dstHeight < 1) $dstHeight = 1;
if ($dstWidth < 1) {
$dstWidth = 1;
}
if ($dstHeight < 1) {
$dstHeight = 1;
}
$dst = imagecreatetruecolor($dstWidth, $dstHeight);
if ($ext === 'png' || $ext === 'webp') {

View File

@@ -2,8 +2,8 @@
namespace MintyPHP\Service\Import;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Access\PermissionGateway;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Audit\ImportAuditService;
use MintyPHP\Service\Import\Profile\ImportProfileInterface;
use MintyPHP\Service\Settings\SettingGateway;

View File

@@ -13,7 +13,6 @@ use MintyPHP\Service\Import\Profile\UserImportGateway;
use MintyPHP\Service\Import\Profile\UserImportProfile;
use MintyPHP\Service\Settings\SettingGateway;
use MintyPHP\Service\Settings\SettingServicesFactory;
use MintyPHP\Service\User\UserRepositoryFactory;
use MintyPHP\Service\User\UserServicesFactory;
class ImportServicesFactory
@@ -30,10 +29,10 @@ class ImportServicesFactory
private readonly UserServicesFactory $userServicesFactory,
private readonly AccessServicesFactory $accessServicesFactory,
private readonly SettingServicesFactory $settingServicesFactory,
private readonly UserRepositoryFactory $userRepositoryFactory,
private readonly DirectoryServicesFactory $directoryServicesFactory,
private readonly ImportRepositoryFactory $importRepositoryFactory
) {}
) {
}
public function createImportService(): ImportService
{
@@ -45,7 +44,9 @@ class ImportServicesFactory
'users' => new UserImportProfile(new UserImportGateway(
$this->userServicesFactory->createUserReadRepository(),
$this->userServicesFactory->createUserAccountService(),
$this->userRepositoryFactory
$this->directoryServicesFactory->createTenantRepository(),
$this->directoryServicesFactory->createRoleRepository(),
$this->directoryServicesFactory->createDepartmentRepository(),
)),
'departments' => new DepartmentImportProfile(new DepartmentImportGateway(
$this->directoryServicesFactory->createDepartmentRepository(),

View File

@@ -60,4 +60,3 @@ class ImportStateStoreService
unset($_SESSION[self::SESSION_KEY][$token]);
}
}

View File

@@ -7,14 +7,15 @@ use MintyPHP\Repository\Org\DepartmentRepositoryInterface;
use MintyPHP\Repository\Tenant\TenantRepositoryInterface;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Service\User\UserAccountService;
use MintyPHP\Service\User\UserRepositoryFactory;
class UserImportGateway
{
public function __construct(
private readonly UserReadRepositoryInterface $userReadRepository,
private readonly UserAccountService $userAccountService,
private readonly UserRepositoryFactory $userRepositoryFactory
private readonly TenantRepositoryInterface $tenantRepository,
private readonly RoleRepositoryInterface $roleRepository,
private readonly DepartmentRepositoryInterface $departmentRepository,
) {
}
@@ -25,32 +26,32 @@ class UserImportGateway
public function findTenantByUuid(string $uuid): ?array
{
return $this->tenantRepository()->findByUuid($uuid);
return $this->tenantRepository->findByUuid($uuid);
}
public function findTenantById(int $id): ?array
{
return $this->tenantRepository()->find($id);
return $this->tenantRepository->find($id);
}
public function findRoleByUuid(string $uuid): ?array
{
return $this->roleRepository()->findByUuid($uuid);
return $this->roleRepository->findByUuid($uuid);
}
public function findRoleById(int $id): ?array
{
return $this->roleRepository()->find($id);
return $this->roleRepository->find($id);
}
public function findDepartmentByUuid(string $uuid): ?array
{
return $this->departmentRepository()->findByUuid($uuid);
return $this->departmentRepository->findByUuid($uuid);
}
public function findDepartmentById(int $id): ?array
{
return $this->departmentRepository()->find($id);
return $this->departmentRepository->find($id);
}
/**
@@ -61,19 +62,4 @@ class UserImportGateway
{
return $this->userAccountService->createFromAdmin($input, $currentUserId);
}
private function tenantRepository(): TenantRepositoryInterface
{
return $this->userRepositoryFactory->createTenantRepository();
}
private function roleRepository(): RoleRepositoryInterface
{
return $this->userRepositoryFactory->createRoleRepository();
}
private function departmentRepository(): DepartmentRepositoryInterface
{
return $this->userRepositoryFactory->createDepartmentRepository();
}
}

View File

@@ -13,7 +13,8 @@ class MailServicesFactory
public function __construct(
private readonly MailRepositoryFactory $mailRepositoryFactory,
private readonly SettingGateway $settingGateway
) {}
) {
}
public function createMailLogRepository(): MailLogRepositoryInterface
{

View File

@@ -2,10 +2,10 @@
namespace MintyPHP\Service\Scheduler;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Service\Scheduler\Handler\ScheduledJobHandlerInterface;
use MintyPHP\Service\Scheduler\Handler\SystemAuditPurgeJobHandler;
use MintyPHP\Service\Scheduler\Handler\UserLifecycleJobHandler;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Service\User\UserLifecycleService;
class ScheduledJobRegistry
@@ -30,8 +30,7 @@ class ScheduledJobRegistry
public function __construct(
UserLifecycleService $userLifecycleService,
SystemAuditService $systemAuditService
)
{
) {
$this->handlers = [
self::USER_LIFECYCLE_RUN => new UserLifecycleJobHandler($userLifecycleService),
self::SYSTEM_AUDIT_PURGE => new SystemAuditPurgeJobHandler($systemAuditService),

View File

@@ -23,7 +23,8 @@ class SchedulerServicesFactory
private readonly AuditServicesFactory $auditServicesFactory,
private readonly DatabaseSessionRepository $databaseSessionRepository,
private readonly SchedulerRepositoryFactory $schedulerRepositoryFactory
) {}
) {
}
public function createScheduledJobService(): ScheduledJobService
{

View File

@@ -10,7 +10,8 @@ class SecurityServicesFactory
public function __construct(
private readonly SecurityRepositoryFactory $securityRepositoryFactory
) {}
) {
}
public function createRateLimitRepository(): RateLimitRepositoryInterface
{

View File

@@ -5,8 +5,8 @@ namespace MintyPHP\Service\Settings;
use MintyPHP\Repository\Auth\ApiTokenRepository;
use MintyPHP\Repository\Auth\RememberTokenRepository;
use MintyPHP\Service\Access\RoleService;
use MintyPHP\Service\Org\DepartmentService;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Service\Org\DepartmentService;
use MintyPHP\Service\Tenant\TenantService;
class AdminSettingsService

View File

@@ -16,7 +16,8 @@ class SettingServicesFactory
public function __construct(
private readonly SettingRepositoryFactory $settingRepositoryFactory
) {}
) {
}
public function createSettingRepository(): SettingRepositoryInterface
{

View File

@@ -16,7 +16,8 @@ class TenantServicesFactory
public function __construct(
private readonly PermissionGateway $permissionGateway,
private readonly TenantRepositoryFactory $tenantRepositoryFactory
) {}
) {
}
public function createTenantScopeService(): TenantScopeService
{

View File

@@ -565,7 +565,7 @@ class UserAccountService
$errors[] = t('Tenant not found');
} else {
$userTenantIds = $this->userAssignmentService->buildAssignmentsForUser($userId)['tenants'] ?? [];
$userTenantIds = array_map(static fn(array $t): int => (int) ($t['id'] ?? 0), $userTenantIds);
$userTenantIds = array_map(static fn (array $t): int => (int) ($t['id'] ?? 0), $userTenantIds);
$tenantId = (int) $tenant['id'];
if (!in_array($tenantId, $userTenantIds, true)) {
$errors[] = t('No access to this tenant');

View File

@@ -9,72 +9,59 @@ use MintyPHP\Repository\Tenant\TenantRepositoryInterface;
class UserDirectoryGateway
{
public function __construct(
private readonly UserRepositoryFactory $userRepositoryFactory
private readonly TenantRepositoryInterface $tenantRepository,
private readonly RoleRepositoryInterface $roleRepository,
private readonly DepartmentRepositoryInterface $departmentRepository,
) {
}
public function listTenantIds(): array
{
return $this->tenantRepository()->listIds();
return $this->tenantRepository->listIds();
}
public function listTenantsByIds(array $tenantIds): array
{
return $this->tenantRepository()->listByIds($tenantIds);
return $this->tenantRepository->listByIds($tenantIds);
}
public function findTenant(int $tenantId): ?array
{
return $this->tenantRepository()->find($tenantId);
return $this->tenantRepository->find($tenantId);
}
public function findTenantByUuid(string $tenantUuid): ?array
{
return $this->tenantRepository()->findByUuid($tenantUuid);
return $this->tenantRepository->findByUuid($tenantUuid);
}
public function listActiveTenantIdsByIds(array $tenantIds): array
{
return $this->tenantRepository()->listActiveIdsByIds($tenantIds);
return $this->tenantRepository->listActiveIdsByIds($tenantIds);
}
public function listActiveRoleIds(): array
{
return $this->roleRepository()->listActiveIds();
return $this->roleRepository->listActiveIds();
}
public function listRolesByIds(array $roleIds): array
{
return $this->roleRepository()->listByIds($roleIds);
return $this->roleRepository->listByIds($roleIds);
}
public function listDepartmentsByIds(array $departmentIds, bool $includeInactive = true): array
{
return $this->departmentRepository()->listByIds($departmentIds, $includeInactive);
return $this->departmentRepository->listByIds($departmentIds, $includeInactive);
}
public function listActiveDepartmentIdsByTenantIds(array $tenantIds): array
{
return $this->departmentRepository()->listActiveIdsByTenantIds($tenantIds);
return $this->departmentRepository->listActiveIdsByTenantIds($tenantIds);
}
public function listDepartmentsByTenantIds(array $tenantIds): array
{
return $this->departmentRepository()->listByTenantIds($tenantIds);
}
private function tenantRepository(): TenantRepositoryInterface
{
return $this->userRepositoryFactory->createTenantRepository();
}
private function roleRepository(): RoleRepositoryInterface
{
return $this->userRepositoryFactory->createRoleRepository();
}
private function departmentRepository(): DepartmentRepositoryInterface
{
return $this->userRepositoryFactory->createDepartmentRepository();
return $this->departmentRepository->listByTenantIds($tenantIds);
}
}

View File

@@ -4,6 +4,7 @@ namespace MintyPHP\Service\User;
use MintyPHP\Service\Access\AccessServicesFactory;
use MintyPHP\Service\Access\PermissionGateway;
use MintyPHP\Service\Directory\DirectoryRepositoryFactory;
use MintyPHP\Service\Settings\SettingGateway;
use MintyPHP\Service\Settings\SettingServicesFactory;
use MintyPHP\Service\Tenant\TenantScopeService;
@@ -18,10 +19,10 @@ class UserGatewayFactory
private ?UserPermissionGateway $userPermissionGateway = null;
public function __construct(
private readonly UserRepositoryFactory $userRepositoryFactory,
private readonly AccessServicesFactory $accessServicesFactory,
private readonly SettingServicesFactory $settingServicesFactory,
private readonly TenantScopeService $tenantScopeService
private readonly TenantScopeService $tenantScopeService,
private readonly DirectoryRepositoryFactory $directoryRepositoryFactory,
) {
}
@@ -37,7 +38,11 @@ class UserGatewayFactory
public function createUserDirectoryGateway(): UserDirectoryGateway
{
return $this->userDirectoryGateway ??= new UserDirectoryGateway($this->userRepositoryFactory);
return $this->userDirectoryGateway ??= new UserDirectoryGateway(
$this->directoryRepositoryFactory->createTenantRepository(),
$this->directoryRepositoryFactory->createRoleRepository(),
$this->directoryRepositoryFactory->createDepartmentRepository(),
);
}
public function createUserPermissionGateway(): UserPermissionGateway
@@ -54,5 +59,4 @@ class UserGatewayFactory
{
return $this->settingGateway ??= $this->settingServicesFactory->createSettingGateway();
}
}

View File

@@ -2,16 +2,10 @@
namespace MintyPHP\Service\User;
use MintyPHP\Repository\Access\RoleRepository;
use MintyPHP\Repository\Access\RoleRepositoryInterface;
use MintyPHP\Repository\Access\UserRoleRepository;
use MintyPHP\Repository\Access\UserRoleRepositoryInterface;
use MintyPHP\Repository\Org\DepartmentRepository;
use MintyPHP\Repository\Org\DepartmentRepositoryInterface;
use MintyPHP\Repository\Org\UserDepartmentRepository;
use MintyPHP\Repository\Org\UserDepartmentRepositoryInterface;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Repository\Tenant\TenantRepositoryInterface;
use MintyPHP\Repository\Tenant\UserTenantRepository;
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
use MintyPHP\Repository\User\UserListQueryRepository;
@@ -29,9 +23,6 @@ class UserRepositoryFactory
private ?UserWriteRepository $userWriteRepository = null;
private ?UserListQueryRepository $userListQueryRepository = null;
private ?UserSavedFilterRepository $userSavedFilterRepository = null;
private ?TenantRepository $tenantRepository = null;
private ?RoleRepository $roleRepository = null;
private ?DepartmentRepository $departmentRepository = null;
private ?UserTenantRepository $userTenantRepository = null;
private ?UserRoleRepository $userRoleRepository = null;
private ?UserDepartmentRepository $userDepartmentRepository = null;
@@ -56,21 +47,6 @@ class UserRepositoryFactory
return $this->userSavedFilterRepository ??= new UserSavedFilterRepository();
}
public function createTenantRepository(): TenantRepositoryInterface
{
return $this->tenantRepository ??= new TenantRepository();
}
public function createRoleRepository(): RoleRepositoryInterface
{
return $this->roleRepository ??= new RoleRepository();
}
public function createDepartmentRepository(): DepartmentRepositoryInterface
{
return $this->departmentRepository ??= new DepartmentRepository();
}
public function createUserTenantRepository(): UserTenantRepositoryInterface
{
return $this->userTenantRepository ??= new UserTenantRepository();

View File

@@ -31,7 +31,8 @@ class UserServicesFactory
private readonly UserRepositoryFactory $userRepositoryFactory,
private readonly UserGatewayFactory $userGatewayFactory,
private readonly DatabaseSessionRepository $databaseSessionRepository
) {}
) {
}
public function createUserAccountService(): UserAccountService
{

View File

@@ -409,7 +409,9 @@ function appFaviconUrl(string $file): string
*/
function sortByDescription(array &$items): void
{
usort($items, static fn($a, $b) =>
usort(
$items,
static fn ($a, $b) =>
strcasecmp((string) ($a['description'] ?? ''), (string) ($b['description'] ?? ''))
);
}

View File

@@ -18,11 +18,17 @@ function gridLang(): array
'next' => t('Next'),
'navigate' => t('Page %d of %d'),
'page' => t('Page %d'),
'rowsPerPage' => t('Rows per page'),
'showing' => t('Showing'),
'of' => t('of'),
'to' => t('to'),
'results' => t('results'),
],
'actions' => [
'column' => t('Actions'),
'open' => t('Open'),
'edit' => t('Edit'),
],
'loading' => t('Loading...'),
'noRecordsFound' => t('No records found'),
'error' => t('An error happened while fetching the data'),

View File

@@ -1,9 +1,9 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\Access\AuthorizationService;
use MintyPHP\Service\Access\UserAuthorizationPolicy;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,7 +1,7 @@
<?php
use MintyPHP\Service\AddressBook\AddressBookServicesFactory;
use MintyPHP\Service\Access\OperationsAuthorizationPolicy;
use MintyPHP\Service\AddressBook\AddressBookServicesFactory;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,7 +1,6 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\AddressBook\AddressBookServicesFactory;
use MintyPHP\Session;
use MintyPHP\Support\Guard;

View File

@@ -2,7 +2,6 @@
use MintyPHP\Buffer;
use MintyPHP\Router;
use MintyPHP\Service\AddressBook\AddressBookServicesFactory;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Service\Access\UiAccessService;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Service\Audit\ApiAuditService;
use MintyPHP\Support\Guard;

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Service\Access\UiAccessService;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Service\Audit\ImportAuditService;
use MintyPHP\Support\Guard;

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,4 +1,5 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Support\Guard;

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Support\Guard;
use MintyPHP\Session;
use MintyPHP\Support\Guard;
Guard::requireLogin();
Guard::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_MAIL_LOG_VIEW);

View File

@@ -1,9 +1,9 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Router;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
Guard::requireLogin();
Guard::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_MAIL_LOG_VIEW);

View File

@@ -1,10 +1,10 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
Guard::requireLogin();
$request = requestInput();

View File

@@ -1,7 +1,7 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
use MintyPHP\Support\Guard;
Guard::requireLogin();
gridRequireGetRequest();

View File

@@ -1,10 +1,10 @@
<?php
use MintyPHP\Router;
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
use MintyPHP\Service\Access\PermissionGateway;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\Access\PermissionGateway;
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
Guard::requireLogin();

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -2,8 +2,8 @@
use MintyPHP\Buffer;
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
use MintyPHP\Support\Guard;
use MintyPHP\Session;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -2,8 +2,8 @@
use MintyPHP\Buffer;
use MintyPHP\Service\Access\RoleAuthorizationPolicy;
use MintyPHP\Support\Guard;
use MintyPHP\Session;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Service\Access\UiAccessService;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Router;
use MintyPHP\Domain\Taxonomy\ScheduledJobRunStatus;
use MintyPHP\Domain\Taxonomy\SchedulerRuntimeResult;
use MintyPHP\Router;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Service\Access\UiAccessService;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Support\Guard;
Guard::requireAbilityOrForbidden(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_STATS_VIEW);

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -1,7 +1,7 @@
<?php
use MintyPHP\Router;
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
use MintyPHP\Router;
use MintyPHP\Service\Access\OperationsAuthorizationPolicy;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Session;

View File

@@ -21,13 +21,14 @@ $dir = $filters['dir'];
$computedOrderKeys = ['theme', 'sso', 'active_users', 'inactive_users'];
$userTenantRepository = app(\MintyPHP\Repository\Tenant\UserTenantRepository::class);
$tenantMicrosoftAuthRepository = app(TenantMicrosoftAuthRepository::class);
$tenantAvatarService = app(\MintyPHP\Service\Tenant\TenantAvatarService::class);
$settingGateway = app(\MintyPHP\Service\Settings\SettingGateway::class);
$fetchRows = static function (
array $tenantRows,
array $themes,
string $globalDefaultTheme
) use ($userTenantRepository, $tenantMicrosoftAuthRepository, $settingGateway): array {
) use ($userTenantRepository, $tenantMicrosoftAuthRepository, $tenantAvatarService, $settingGateway): array {
$tenantIds = [];
foreach ($tenantRows as $tenantRow) {
$tenantId = (int) ($tenantRow['id'] ?? 0);
@@ -69,10 +70,11 @@ $fetchRows = static function (
$usersActive = (int) ($tenantActiveUserCounts[$tenantId] ?? 0);
$usersInactive = max(0, $usersTotal - $usersActive);
$tenantStatus = TenantStatus::normalizeOr((string) ($row['status'] ?? ''), TenantStatus::Active);
$tenantUuid = (string) ($row['uuid'] ?? '');
$rows[] = [
'id' => $row['id'] ?? null,
'uuid' => $row['uuid'] ?? '',
'uuid' => $tenantUuid,
'is_default' => $tenantId > 0 && $defaultTenantId === $tenantId,
'description' => $row['description'] ?? '',
'status' => $tenantStatus->value,
@@ -92,6 +94,7 @@ $fetchRows = static function (
'inactive_users' => $usersInactive,
'created' => dt($row['created'] ?? ''),
'modified' => dt($row['modified'] ?? ''),
'has_avatar' => $tenantUuid !== '' && $tenantAvatarService->hasAvatar($tenantUuid),
];
}

View File

@@ -6,6 +6,7 @@ use MintyPHP\Service\Access\TenantAuthorizationPolicy;
use MintyPHP\Service\CustomField\TenantCustomFieldService;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
Guard::requireLogin();
$request = requestInput();

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -2,8 +2,8 @@
use MintyPHP\Buffer;
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
use MintyPHP\Support\Guard;
use MintyPHP\Session;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -49,6 +49,7 @@ $pageConfig = [
'filterChipMeta' => $filterChipMeta,
'gridLang' => $gridLang,
'labels' => [
'avatar' => t('Avatar'),
'description' => t('Description'),
'status' => t('Status'),
'theme' => t('Theme'),

View File

@@ -4,8 +4,8 @@ use MintyPHP\Buffer;
use MintyPHP\Domain\Taxonomy\UserLifecycleAction;
use MintyPHP\Domain\Taxonomy\UserLifecycleStatus;
use MintyPHP\Domain\Taxonomy\UserLifecycleTriggerType;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Service\Access\UiAccessService;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Service\Audit\UserLifecycleAuditService;
use MintyPHP\Support\Guard;

View File

@@ -2,8 +2,8 @@
use MintyPHP\Router;
use MintyPHP\Session;
use MintyPHP\Support\Guard;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
Guard::requireLogin();
Guard::requireAbility(\MintyPHP\Service\Access\SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE);

View File

@@ -3,8 +3,8 @@
use MintyPHP\Buffer;
use MintyPHP\Domain\Taxonomy\UserLifecycleAction;
use MintyPHP\Router;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Service\Access\UiAccessService;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\I18n;
use MintyPHP\Http\Request;
use MintyPHP\I18n;
use MintyPHP\Router;
use MintyPHP\Service\Access\UiAccessService;
use MintyPHP\Service\Access\UiCapabilityMap;

View File

@@ -1,8 +1,8 @@
<?php
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Support\Guard;
Guard::requireLogin();

View File

@@ -3,8 +3,8 @@
use MintyPHP\Buffer;
use MintyPHP\Http\Request;
use MintyPHP\Router;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Service\Access\UiAccessService;
use MintyPHP\Service\Access\UiCapabilityMap;
use MintyPHP\Service\Access\UserAuthorizationPolicy;
use MintyPHP\Session;
use MintyPHP\Support\Guard;

View File

@@ -51,28 +51,28 @@ if (!($emailResult['allowed'] ?? true)) {
$userReadRepository = (app(UserServicesFactory::class))->createUserReadRepository();
$user = $userReadRepository->findByEmail($email);
if (!$user) {
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
$rateLimiter->registerFailure('api.auth.login.email_ip', $emailKey, 5, 900, 900);
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
$rateLimiter->registerFailure('api.auth.login.email_ip', $emailKey, 5, 900, 900);
ApiResponse::error('invalid_credentials', 401);
}
// ---- Account checks ----
if (!(int) ($user['active'] ?? 0)) {
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
$rateLimiter->registerFailure('api.auth.login.email_ip', $emailKey, 5, 900, 900);
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
$rateLimiter->registerFailure('api.auth.login.email_ip', $emailKey, 5, 900, 900);
ApiResponse::error('account_inactive', 401);
}
if (empty($user['email_verified_at'])) {
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
$rateLimiter->registerFailure('api.auth.login.email_ip', $emailKey, 5, 900, 900);
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
$rateLimiter->registerFailure('api.auth.login.email_ip', $emailKey, 5, 900, 900);
ApiResponse::error('invalid_credentials', 401);
}
// ---- Password verification ----
if (!password_verify($password, (string) ($user['password'] ?? ''))) {
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
$rateLimiter->registerFailure('api.auth.login.email_ip', $emailKey, 5, 900, 900);
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
$rateLimiter->registerFailure('api.auth.login.email_ip', $emailKey, 5, 900, 900);
ApiResponse::error('invalid_credentials', 401);
}

Some files were not shown because too many files have changed in this diff Show More