add composer-unused, comprehensive docs, and project restructure

- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 15:27:35 +01:00
parent 3eb9cc0ac4
commit 25370a1a55
389 changed files with 40506 additions and 8071 deletions

View File

@@ -3,12 +3,15 @@
namespace MintyPHP\Service\Auth;
use MintyPHP\Auth;
use MintyPHP\Session;
use MintyPHP\Router;
use MintyPHP\Support\Flash;
use MintyPHP\Service\User\UserService;
use MintyPHP\Service\User\UserSavedFilterService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Auth\RememberMeService;
use MintyPHP\Service\Auth\EmailVerificationService;
use MintyPHP\Service\Auth\TenantSsoService;
use MintyPHP\Repository\User\UserRepository;
class AuthService
@@ -49,6 +52,14 @@ class AuthService
}
$userId = (int) ($_SESSION['user']['id'] ?? 0);
if ($userId > 0 && !self::isLocalPasswordLoginAllowedForUser($userId)) {
Auth::logout();
return [
'ok' => false,
'message' => 'Password login is disabled for all assigned tenants',
'flash_key' => 'login_password_disabled_all_tenants',
];
}
if ($userId > 0) {
PermissionService::getUserPermissions($userId, true);
self::loadTenantDataIntoSession($userId);
@@ -60,12 +71,86 @@ class AuthService
'flash_key' => 'login_no_active_tenant',
];
}
UserRepository::updateLastLogin($userId);
UserRepository::updateLastLogin($userId, 'local');
}
return ['ok' => true];
}
private static function isLocalPasswordLoginAllowedForUser(int $userId): bool
{
if ($userId <= 0) {
return false;
}
$availableTenants = UserService::getAvailableTenants($userId);
if (!$availableTenants) {
return true;
}
foreach ($availableTenants as $tenant) {
$tenantId = (int) ($tenant['id'] ?? 0);
if ($tenantId <= 0) {
continue;
}
if (TenantSsoService::isLocalPasswordLoginAllowed($tenantId)) {
return true;
}
}
return false;
}
public static function canLoginToTenant(int $userId, int $tenantId): bool
{
if ($userId <= 0 || $tenantId <= 0) {
return false;
}
foreach (UserService::getAvailableTenants($userId) as $tenant) {
if ((int) ($tenant['id'] ?? 0) === $tenantId) {
return true;
}
}
return false;
}
public static function loginUserById(int $userId, ?int $preferredTenantId = null, string $loginProvider = 'local'): array
{
if ($userId <= 0) {
return ['ok' => false, 'error' => 'user_not_found'];
}
$user = UserRepository::find($userId);
if (!$user) {
return ['ok' => false, 'error' => 'user_not_found'];
}
if (!((int) ($user['active'] ?? 1) === 1)) {
return ['ok' => false, 'error' => 'user_inactive'];
}
Session::regenerate();
$_SESSION['user'] = $user;
if ($preferredTenantId !== null && $preferredTenantId > 0) {
$setTenant = UserService::setCurrentTenant($userId, $preferredTenantId);
if ($setTenant['ok'] ?? false) {
$_SESSION['user']['current_tenant_id'] = $preferredTenantId;
}
}
PermissionService::getUserPermissions($userId, true);
self::loadTenantDataIntoSession($userId);
if (!empty($_SESSION['no_active_tenant'])) {
Auth::logout();
return ['ok' => false, 'error' => 'login_no_active_tenant'];
}
UserRepository::updateLastLogin($userId, $loginProvider);
return ['ok' => true, 'user' => $user];
}
public static function loginAndRedirect(
string $email,
string $password,
@@ -130,6 +215,69 @@ class AuthService
Router::redirect($target);
}
public static function refreshSessionAuthState(int $userId): array
{
if ($userId <= 0) {
return [
'ok' => false,
'logout_required' => true,
'reason' => 'user_missing',
];
}
$snapshot = UserRepository::findAuthzSnapshot($userId);
if (!$snapshot) {
return [
'ok' => false,
'logout_required' => true,
'reason' => 'user_not_found',
];
}
if ((int) ($snapshot['active'] ?? 0) !== 1) {
return [
'ok' => false,
'logout_required' => true,
'reason' => 'inactive',
];
}
$sessionVersion = (int) ($_SESSION['user']['authz_version'] ?? 0);
$dbVersion = max(1, (int) ($snapshot['authz_version'] ?? 1));
$refreshed = false;
if ($sessionVersion !== $dbVersion) {
$user = UserRepository::find($userId);
if (!$user || (int) ($user['active'] ?? 0) !== 1) {
return [
'ok' => false,
'logout_required' => true,
'reason' => 'inactive',
];
}
$_SESSION['user'] = $user;
PermissionService::getUserPermissions($userId, true);
self::loadTenantDataIntoSession($userId);
$refreshed = true;
}
if (!empty($_SESSION['no_active_tenant'])) {
return [
'ok' => false,
'logout_required' => true,
'reason' => 'no_active_tenant',
'refreshed' => $refreshed,
];
}
return [
'ok' => true,
'logout_required' => false,
'refreshed' => $refreshed,
];
}
public static function loadTenantDataIntoSession(int $userId): void
{
if ($userId <= 0) {
@@ -140,11 +288,9 @@ class AuthService
$availableTenants = UserService::getAvailableTenants($userId);
$_SESSION['available_tenants'] = $availableTenants;
$_SESSION['available_departments_by_tenant'] = UserService::getAvailableDepartmentsByTenant($userId);
$_SESSION['address_book_saved_filters'] = UserSavedFilterService::listForAddressBook($userId);
$hasTenantAdminAccess = PermissionService::userHas($userId, PermissionService::TENANTS_UPDATE)
|| PermissionService::userHas($userId, PermissionService::SETTINGS_UPDATE);
if (!$availableTenants && !$hasTenantAdminAccess) {
if (!$availableTenants) {
$_SESSION['no_active_tenant'] = true;
unset($_SESSION['current_tenant']);
return;
@@ -153,7 +299,7 @@ class AuthService
// Load current tenant (with fallback to first available)
$currentTenant = UserService::getCurrentTenant($userId);
if (!$currentTenant && !empty($availableTenants)) {
if (!$currentTenant) {
// Fallback: use first available tenant
$currentTenant = $availableTenants[0];
}