agent foundation

This commit is contained in:
2026-03-06 00:44:52 +01:00
parent 9819cba733
commit 9a08f96c11
199 changed files with 8522 additions and 1880 deletions

View File

@@ -3,6 +3,7 @@
namespace MintyPHP\Service\Auth;
use MintyPHP\Auth;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Router;
@@ -23,8 +24,9 @@ class AuthService
private readonly EmailVerificationService $emailVerificationService,
private readonly AuthPermissionGateway $permissionGateway,
private readonly AuthTenantSsoGateway $tenantSsoGateway,
private readonly AuthSavedFilterGateway $savedFilterGateway,
private readonly SystemAuditService $systemAuditService
private readonly AuthSessionTenantContextService $authSessionTenantContextService,
private readonly SystemAuditService $systemAuditService,
private readonly SessionStoreInterface $sessionStore
) {
}
@@ -62,7 +64,8 @@ class AuthService
];
}
if (!($_SESSION['user']['active'] ?? 1)) {
$sessionUser = $this->sessionUser();
if (!($sessionUser['active'] ?? 1)) {
Auth::logout();
$this->recordAuthEvent('auth.login.failed', 'failed', [
'error_code' => 'account_inactive',
@@ -75,7 +78,7 @@ class AuthService
];
}
$userId = (int) ($_SESSION['user']['id'] ?? 0);
$userId = (int) ($sessionUser['id'] ?? 0);
if ($userId > 0 && !$this->isLocalPasswordLoginAllowedForUser($userId)) {
Auth::logout();
$this->recordAuthEvent('auth.login.failed', 'failed', [
@@ -91,7 +94,7 @@ class AuthService
if ($userId > 0) {
$this->permissionGateway->warmUserPermissions($userId, true);
$this->loadTenantDataIntoSession($userId);
if (!empty($_SESSION['no_active_tenant'])) {
if ($this->noActiveTenant()) {
Auth::logout();
$this->recordAuthEvent('auth.login.failed', 'failed', [
'error_code' => 'no_active_tenant',
@@ -108,12 +111,14 @@ class AuthService
$this->recordAuthEvent('auth.login.success', 'success', [
'actor_user_id' => $userId > 0 ? $userId : null,
'actor_tenant_id' => (int) ($_SESSION['current_tenant']['id'] ?? 0),
'actor_tenant_id' => $this->currentTenantIdFromSession(),
]);
return ['ok' => true];
}
// Returns true if at least ONE assigned tenant permits local login.
// A user is only blocked if ALL tenants enforce Microsoft-only login.
private function isLocalPasswordLoginAllowedForUser(int $userId): bool
{
if ($userId <= 0) {
@@ -168,18 +173,20 @@ class AuthService
}
Session::regenerate();
$_SESSION['user'] = $user;
$this->sessionStore->set('user', $user);
if ($preferredTenantId !== null && $preferredTenantId > 0) {
$setTenant = $this->userTenantContextService->setCurrentTenant($userId, $preferredTenantId);
if ($setTenant['ok'] ?? false) {
$_SESSION['user']['current_tenant_id'] = $preferredTenantId;
$sessionUser = $this->sessionUser();
$sessionUser['current_tenant_id'] = $preferredTenantId;
$this->sessionStore->set('user', $sessionUser);
}
}
$this->permissionGateway->warmUserPermissions($userId, true);
$this->loadTenantDataIntoSession($userId);
if (!empty($_SESSION['no_active_tenant'])) {
if ($this->noActiveTenant()) {
Auth::logout();
$this->recordAuthEvent('auth.login.failed', 'failed', [
'error_code' => 'login_no_active_tenant',
@@ -191,7 +198,7 @@ class AuthService
$this->userWriteRepository->updateLastLogin($userId, $loginProvider);
$this->recordAuthEvent('auth.login.success', 'success', [
'actor_user_id' => $userId,
'actor_tenant_id' => (int) ($_SESSION['current_tenant']['id'] ?? 0),
'actor_tenant_id' => $this->currentTenantIdFromSession(),
'metadata' => ['provider' => $loginProvider],
]);
return ['ok' => true, 'user' => $user];
@@ -209,7 +216,7 @@ class AuthService
if (!($result['ok'] ?? false)) {
// Check if user needs to verify email
if (!empty($result['needs_verification'])) {
$_SESSION['email_verification_email'] = $result['email'] ?? $email;
$this->sessionStore->set('email_verification_email', $result['email'] ?? $email);
Flash::error(t('Please verify your email first'), 'verify-email', 'login_not_verified');
Router::redirect('verify-email');
}
@@ -221,7 +228,7 @@ class AuthService
}
if ($remember) {
$userId = (int) ($_SESSION['user']['id'] ?? 0);
$userId = (int) ($this->sessionUser()['id'] ?? 0);
if ($userId > 0) {
$this->rememberMeService->rememberUser($userId);
}
@@ -250,8 +257,8 @@ class AuthService
public function logout(): void
{
$actorUserId = (int) ($_SESSION['user']['id'] ?? 0);
$actorTenantId = (int) ($_SESSION['current_tenant']['id'] ?? 0);
$actorUserId = (int) ($this->sessionUser()['id'] ?? 0);
$actorTenantId = $this->currentTenantIdFromSession();
$this->rememberMeService->forgetCurrentUser();
Auth::logout();
$this->recordAuthEvent('auth.logout', 'success', [
@@ -298,7 +305,9 @@ class AuthService
];
}
$sessionVersion = (int) ($_SESSION['user']['authz_version'] ?? 0);
// authz_version is bumped in DB whenever permissions/tenants change.
// A mismatch means the session is stale and needs to be refreshed.
$sessionVersion = (int) ($this->sessionUser()['authz_version'] ?? 0);
$dbVersion = max(1, (int) ($snapshot['authz_version'] ?? 1));
$refreshed = false;
@@ -312,13 +321,13 @@ class AuthService
];
}
$_SESSION['user'] = $user;
$this->sessionStore->set('user', $user);
$this->permissionGateway->warmUserPermissions($userId, true);
$this->loadTenantDataIntoSession($userId);
$refreshed = true;
}
if (!empty($_SESSION['no_active_tenant'])) {
if ($this->noActiveTenant()) {
return [
'ok' => false,
'logout_required' => true,
@@ -336,31 +345,34 @@ class AuthService
public function loadTenantDataIntoSession(int $userId): void
{
if ($userId <= 0) {
return;
}
$this->authSessionTenantContextService->hydrateForUser($userId);
}
// Load available tenants first
$availableTenants = $this->userTenantContextService->getAvailableTenants($userId);
$_SESSION['available_tenants'] = $availableTenants;
$_SESSION['available_departments_by_tenant'] = $this->userTenantContextService->getAvailableDepartmentsByTenant($userId);
$_SESSION['address_book_saved_filters'] = $this->savedFilterGateway->listAddressBookFilters($userId);
/**
* @return array<string, mixed>
*/
private function sessionUser(): array
{
$user = $this->sessionStore->get('user', []);
return is_array($user) ? $user : [];
}
if (!$availableTenants) {
$_SESSION['no_active_tenant'] = true;
unset($_SESSION['current_tenant']);
return;
}
$_SESSION['no_active_tenant'] = false;
/**
* @return array<string, mixed>
*/
private function sessionCurrentTenant(): array
{
$tenant = $this->sessionStore->get('current_tenant', []);
return is_array($tenant) ? $tenant : [];
}
// Load current tenant (with fallback to first available)
$currentTenant = $this->userTenantContextService->getCurrentTenant($userId);
if (!$currentTenant) {
// Fallback: use first available tenant
$currentTenant = $availableTenants[0];
}
if ($currentTenant) {
$_SESSION['current_tenant'] = $currentTenant;
}
private function currentTenantIdFromSession(): int
{
return (int) ($this->sessionCurrentTenant()['id'] ?? 0);
}
private function noActiveTenant(): bool
{
return (bool) $this->sessionStore->get('no_active_tenant', false);
}
}