Replace the single tenant avatar with a pair of theme-scoped brand logos.
Render only the theme-matching <img> server-side and swap src on theme
toggle via a JS hook — no reload, no double request, no CSS tricks.
Tenant logos
- TenantLogoService (ImageUploadTrait) with theme whitelist and per-theme
storage storage/tenants/{uuid}/logo/{light|dark}/, SIZES 128/256/512
- Public serving endpoint auth/tenant-logo-file so login can show the
logo pre-auth; matching authenticated admin preview endpoint
- appTenantLogoUrl(?size, ?theme) with 4-step fallback cascade; PDF +
mail always request 'light'
- Admin tenant edit: avatar block replaced by "Tenant logos" details
block inside the Master-data tab, two side-by-side slots via Pico
.grid with the core app-file-upload partial
- Policy rename ABILITY_ADMIN_TENANTS_AVATAR_VIEW -> LOGO_VIEW, action
routes logo / logo-delete / logo-file with theme body/query param
- API endpoint path kept (backward compat), internals on new service
- CLI tenant:logo-migrate-avatars moves legacy avatar/ -> logo/light/
idempotently (--dry-run, --yes, --cleanup)
- i18n "Tenant image" removed, 12 new keys synced across de/en
File upload component
- Full-width preview + filename/actions below (3D stack layout)
- Fixed 16:9 aspect ratio with 1rem inner padding for consistent
preview size across any logo aspect
- Transparency checker pattern as background so black logos stay
visible on dark mode and white logos on light mode
- form="" + deleteFormId support so the partial works with barrier
forms inside another form
Buttons
- width:100% dropped from button[type="submit"]; scoped back via
.login-main for the auth-flow primary CTA
- .outline base rule now tints background via color-mix of --app-color
so secondary/primary/danger outlines all gain a subtle surface
- .outline.secondary restyled Stripe-style in both themes: solid white
chip with soft shadow in light, solid elevated dark chip with white
text in dark; neutral border replaces role-colored border
- .app-action-success/.app-action-danger outlines get color-mix bg +
theme-aware outline-text tokens for stronger contrast
- Filled .primary/.app-action-success/.app-action-danger get raised
box-shadow (inset highlight + drop) — opt-in via class so chrome
buttons stay flat
- Dropped the legacy .secondary utility that was clobbering the
custom-property cascade with a hardcoded muted color
Theme swap
- Logo img carries data-src-light + data-src-dark; theme-toggle JS
swaps src when data-theme changes, keeping the topbar/login logo in
sync without a page reload
Quality gates: PHPUnit (2045), PHPStan L5, CS-Fixer, docs link/drift,
codex skills sync — all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
455 lines
25 KiB
PHP
455 lines
25 KiB
PHP
<?php
|
|
|
|
use MintyPHP\Http\IntendedUrlService;
|
|
use MintyPHP\Http\RequestRuntimeInterface;
|
|
use MintyPHP\Http\SessionStoreInterface;
|
|
use MintyPHP\Router;
|
|
use MintyPHP\Service\Auth\AuthServicesFactory;
|
|
use MintyPHP\Service\Security\SecurityServicesFactory;
|
|
use MintyPHP\Service\Tenant\TenantServicesFactory;
|
|
use MintyPHP\Service\User\UserServicesFactory;
|
|
use MintyPHP\Support\Flash;
|
|
|
|
$requestRuntime = app(RequestRuntimeInterface::class);
|
|
$sessionStore = app(SessionStoreInterface::class);
|
|
$intendedUrlService = app(IntendedUrlService::class);
|
|
|
|
// Store return_to from query parameter (set by session timeout or auth guard redirect)
|
|
// or from hidden form field (preserved through multi-stage login POST flow).
|
|
$returnTo = trim((string) (requestInput()->queryAll()['return_to'] ?? ''));
|
|
if ($returnTo === '') {
|
|
$returnTo = trim((string) (requestInput()->bodyAll()['return_to'] ?? ''));
|
|
}
|
|
|
|
if ($returnTo !== '') {
|
|
$intendedUrlService->store($returnTo, $sessionStore);
|
|
}
|
|
|
|
// Expose sanitized return_to for hidden form fields so it survives across
|
|
// POST stages even if the query string or session is lost.
|
|
$returnToSanitized = $intendedUrlService->sanitize($returnTo !== '' ? $returnTo : ($intendedUrlService->retrieve($sessionStore) ?? ''));
|
|
|
|
$session = $sessionStore->all();
|
|
|
|
if (!empty($session['user']['id'])) {
|
|
Router::redirect($intendedUrlService->retrieveAndForget($sessionStore));
|
|
return;
|
|
}
|
|
|
|
$tenantHint = trim((string) (requestInput()->queryAll()['tenant'] ?? requestInput()->bodyAll()['tenant_hint'] ?? ''));
|
|
$stage = 'resolve_email';
|
|
$email = '';
|
|
$tenantCandidates = [];
|
|
$tenantCandidateById = [];
|
|
$selectedTenantId = 0;
|
|
$selectedTenant = null;
|
|
$selectedTenantMethods = ['local' => false, 'microsoft' => false, 'microsoft_reason' => '', 'ldap' => false, 'ldap_reason' => ''];
|
|
$errorBag = formErrors();
|
|
$setLoginWarning = static function (string $message) use ($errorBag): void {
|
|
$errorBag->addGlobal($message);
|
|
};
|
|
$setRateLimitWarning = static function (int $retryAfterSeconds) use ($setLoginWarning): void {
|
|
$retryAfterSeconds = max(1, $retryAfterSeconds);
|
|
http_response_code(429);
|
|
header('Retry-After: ' . $retryAfterSeconds);
|
|
$setLoginWarning(t('Too many login attempts. Please wait and try again.'));
|
|
};
|
|
$rateLimiterService = (app(SecurityServicesFactory::class))->createRateLimiterService();
|
|
|
|
$loginResolveScope = 'login.resolve.ip';
|
|
$loginResolveLimit = 25;
|
|
$loginResolveWindow = 600;
|
|
$loginResolveBlock = 300;
|
|
|
|
$loginPasswordScope = 'login.password.email_ip';
|
|
$loginPasswordLimit = 5;
|
|
$loginPasswordWindow = 900;
|
|
$loginPasswordBlock = 900;
|
|
$authServicesFactory = app(AuthServicesFactory::class);
|
|
$authService = $authServicesFactory->createAuthService();
|
|
$rememberMeService = $authServicesFactory->createRememberMeService();
|
|
$tenantSsoService = $authServicesFactory->createTenantSsoService();
|
|
$tenantLogoService = (app(TenantServicesFactory::class))->createTenantLogoService();
|
|
$userServicesFactory = app(UserServicesFactory::class);
|
|
$userReadRepository = $userServicesFactory->createUserReadRepository();
|
|
$userTenantContextService = $userServicesFactory->createUserTenantContextService();
|
|
|
|
$resolveLoginCandidates = static function (string $inputEmail) use ($userReadRepository, $userTenantContextService, $tenantSsoService, $tenantLogoService): array {
|
|
$emailValue = strtolower(trim($inputEmail));
|
|
if ($emailValue === '' || !filter_var($emailValue, FILTER_VALIDATE_EMAIL)) {
|
|
return ['ok' => false];
|
|
}
|
|
|
|
$user = $userReadRepository->findByEmail($emailValue);
|
|
$isKnownActiveUser = $user && (int) ($user['active'] ?? 0) === 1 && (int) ($user['id'] ?? 0) > 0;
|
|
|
|
$tenants = [];
|
|
if ($isKnownActiveUser) {
|
|
$userId = (int) $user['id'];
|
|
$tenants = $userTenantContextService->getAssignedActiveTenants($userId);
|
|
}
|
|
|
|
// Fallback: domain-based SSO discovery for users not yet provisioned locally
|
|
$discovery = false;
|
|
$discoveryMethodsByTenantId = [];
|
|
if (!$tenants) {
|
|
$discoveryResults = $tenantSsoService->discoverTenantsByEmailDomain($emailValue);
|
|
if (!$discoveryResults) {
|
|
return ['ok' => false];
|
|
}
|
|
$discovery = true;
|
|
$tenants = array_map(static fn (array $r): array => $r['tenant'], $discoveryResults);
|
|
foreach ($discoveryResults as $r) {
|
|
$discoveryMethodsByTenantId[(int) $r['id']] = $r['methods'];
|
|
}
|
|
}
|
|
|
|
$candidates = [];
|
|
$candidateById = [];
|
|
foreach ($tenants as $tenant) {
|
|
$tenantId = (int) ($tenant['id'] ?? 0);
|
|
if ($tenantId <= 0) {
|
|
continue;
|
|
}
|
|
$tenantSlug = $tenantSsoService->tenantLoginSlug($tenant);
|
|
if ($tenantSlug === '') {
|
|
continue;
|
|
}
|
|
$methods = $discovery
|
|
? ($discoveryMethodsByTenantId[$tenantId] ?? ['local' => false, 'microsoft' => false, 'microsoft_reason' => '', 'ldap' => false, 'ldap_reason' => ''])
|
|
: $tenantSsoService->resolveTenantLoginMethods($tenantId);
|
|
$tenantUuid = (string) ($tenant['uuid'] ?? '');
|
|
$hasLogoLight = $tenantUuid !== '' && $tenantLogoService->hasLogo($tenantUuid, \MintyPHP\Service\Tenant\TenantLogoService::THEME_LIGHT);
|
|
$hasLogoDark = $tenantUuid !== '' && $tenantLogoService->hasLogo($tenantUuid, \MintyPHP\Service\Tenant\TenantLogoService::THEME_DARK);
|
|
$hasAvatar = $hasLogoLight || $hasLogoDark;
|
|
$logoTheme = $hasLogoLight ? 'light' : ($hasLogoDark ? 'dark' : '');
|
|
$candidate = [
|
|
'id' => $tenantId,
|
|
'uuid' => $tenantUuid,
|
|
'description' => (string) ($tenant['description'] ?? ''),
|
|
'slug' => $tenantSlug,
|
|
'has_avatar' => $hasAvatar,
|
|
'avatar_url' => $hasAvatar
|
|
? lurl('auth/tenant-logo-file?uuid=' . rawurlencode($tenantUuid) . '&theme=' . rawurlencode($logoTheme) . '&size=256')
|
|
: '',
|
|
'methods' => $methods,
|
|
];
|
|
$candidates[] = $candidate;
|
|
$candidateById[$tenantId] = $candidate;
|
|
}
|
|
|
|
if (!$candidates) {
|
|
return ['ok' => false];
|
|
}
|
|
|
|
return [
|
|
'ok' => true,
|
|
'email' => $emailValue,
|
|
'user' => $isKnownActiveUser ? $user : null,
|
|
'candidates' => $candidates,
|
|
'candidate_by_id' => $candidateById,
|
|
'discovery' => $discovery,
|
|
];
|
|
};
|
|
|
|
$resolveSelectedTenantId = static function (array $candidateByIdValue, int $requestedTenantIdValue, string $tenantHintValue): int {
|
|
if ($requestedTenantIdValue > 0 && isset($candidateByIdValue[$requestedTenantIdValue])) {
|
|
return $requestedTenantIdValue;
|
|
}
|
|
|
|
$hint = trim(strtolower($tenantHintValue));
|
|
if ($hint !== '') {
|
|
foreach ($candidateByIdValue as $candidate) {
|
|
$candidateSlug = strtolower((string) ($candidate['slug'] ?? ''));
|
|
if ($candidateSlug !== '' && $candidateSlug === $hint) {
|
|
return (int) ($candidate['id'] ?? 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
$first = reset($candidateByIdValue);
|
|
if (!is_array($first)) {
|
|
return 0;
|
|
}
|
|
return (int) ($first['id'] ?? 0);
|
|
};
|
|
|
|
if (requestInput()->isMethod('POST')) {
|
|
if (!actionRequireCsrf('login', flashOnFailure: false, redirectOnFailure: false)) {
|
|
$errorBag->addGlobal(t('Form expired, please try again'));
|
|
$stage = 'resolve_email';
|
|
} else {
|
|
$postedStage = trim((string) (requestInput()->bodyAll()['stage'] ?? 'resolve_email'));
|
|
$email = trim((string) (requestInput()->bodyAll()['email'] ?? ''));
|
|
$clientIp = trim((string) ($requestRuntime->ip()));
|
|
if ($clientIp === '') {
|
|
$clientIp = 'unknown';
|
|
}
|
|
|
|
if ($postedStage === 'reset') {
|
|
$email = '';
|
|
$stage = 'resolve_email';
|
|
} else {
|
|
if ($postedStage === 'resolve_email') {
|
|
$resolveLimitResult = $rateLimiterService->isBlocked($loginResolveScope, $clientIp);
|
|
if (!($resolveLimitResult['allowed'] ?? true)) {
|
|
$setRateLimitWarning((int) ($resolveLimitResult['retry_after'] ?? 60));
|
|
return;
|
|
}
|
|
}
|
|
|
|
$resolved = $resolveLoginCandidates($email);
|
|
if (!$resolved['ok']) {
|
|
$stage = 'resolve_email';
|
|
$setLoginWarning(t('We could not continue with these login details'));
|
|
if ($postedStage === 'resolve_email') {
|
|
$resolveFailureResult = $rateLimiterService->registerFailure(
|
|
$loginResolveScope,
|
|
$clientIp,
|
|
$loginResolveLimit,
|
|
$loginResolveWindow,
|
|
$loginResolveBlock
|
|
);
|
|
if (!($resolveFailureResult['allowed'] ?? true)) {
|
|
$setRateLimitWarning((int) ($resolveFailureResult['retry_after'] ?? $loginResolveBlock));
|
|
}
|
|
}
|
|
} else {
|
|
$email = (string) $resolved['email'];
|
|
$tenantCandidates = $resolved['candidates'];
|
|
$tenantCandidateById = $resolved['candidate_by_id'];
|
|
$postedTenantId = (int) (requestInput()->bodyAll()['tenant_id'] ?? 0);
|
|
|
|
if (in_array($postedStage, ['choose_tenant', 'login_password', 'login_ldap'], true)
|
|
&& ($postedTenantId <= 0 || !isset($tenantCandidateById[$postedTenantId]))) {
|
|
$stage = 'choose_tenant';
|
|
$setLoginWarning(t('We could not continue with these login details'));
|
|
$selectedTenantId = $resolveSelectedTenantId(
|
|
$tenantCandidateById,
|
|
0,
|
|
trim((string) (requestInput()->bodyAll()['tenant_hint'] ?? $tenantHint))
|
|
);
|
|
$selectedTenant = $tenantCandidateById[$selectedTenantId] ?? null;
|
|
$selectedTenantMethods = is_array($selectedTenant['methods'] ?? null)
|
|
? $selectedTenant['methods']
|
|
: $selectedTenantMethods;
|
|
} else {
|
|
$selectedTenantId = $resolveSelectedTenantId(
|
|
$tenantCandidateById,
|
|
$postedTenantId,
|
|
trim((string) (requestInput()->bodyAll()['tenant_hint'] ?? $tenantHint))
|
|
);
|
|
$selectedTenant = $tenantCandidateById[$selectedTenantId] ?? null;
|
|
if (!is_array($selectedTenant)) {
|
|
$stage = 'resolve_email';
|
|
$setLoginWarning(t('We could not continue with these login details'));
|
|
} else {
|
|
$selectedTenantMethods = $selectedTenant['methods'];
|
|
$selectedTenantSlug = trim((string) $selectedTenant['slug']);
|
|
$isMicrosoftOnlyTenant = empty($selectedTenantMethods['local'])
|
|
&& !empty($selectedTenantMethods['microsoft'])
|
|
&& empty($selectedTenantMethods['ldap'])
|
|
&& $selectedTenantSlug !== '';
|
|
|
|
if (
|
|
$postedStage === 'resolve_email'
|
|
&& count($tenantCandidates) === 1
|
|
&& $isMicrosoftOnlyTenant
|
|
) {
|
|
Router::redirect('auth/microsoft/start?tenant=' . rawurlencode($selectedTenantSlug));
|
|
return;
|
|
}
|
|
|
|
if (
|
|
$postedStage === 'choose_tenant'
|
|
&& $isMicrosoftOnlyTenant
|
|
) {
|
|
Router::redirect('auth/microsoft/start?tenant=' . rawurlencode($selectedTenantSlug));
|
|
return;
|
|
}
|
|
|
|
if ($postedStage === 'resolve_email') {
|
|
$stage = count($tenantCandidates) === 1 ? 'methods' : 'choose_tenant';
|
|
} elseif ($postedStage === 'choose_tenant') {
|
|
$stage = 'methods';
|
|
} elseif ($postedStage === 'login_ldap') {
|
|
$stage = 'methods';
|
|
$passwordAttemptKey = strtolower(trim($email)) . '|' . $clientIp;
|
|
$passwordLimitResult = $rateLimiterService->isBlocked($loginPasswordScope, $passwordAttemptKey);
|
|
if (!($passwordLimitResult['allowed'] ?? true)) {
|
|
$setRateLimitWarning((int) ($passwordLimitResult['retry_after'] ?? 60));
|
|
return;
|
|
}
|
|
|
|
if (empty($selectedTenantMethods['ldap'])) {
|
|
$setLoginWarning(t('LDAP login is not available for this tenant.'));
|
|
} else {
|
|
$ldapUsername = trim((string) (requestInput()->bodyAll()['ldap_username'] ?? ''));
|
|
$ldapPassword = (string) (requestInput()->bodyAll()['password'] ?? '');
|
|
$remember = !empty(requestInput()->bodyAll()['remember']);
|
|
|
|
$ldapAuthService = $authServicesFactory->createLdapAuthService();
|
|
$ssoUserLinkService = $authServicesFactory->createSsoUserLinkService();
|
|
|
|
$tenantRecord = app(\MintyPHP\Service\Auth\AuthGatewayFactory::class)
|
|
->createAuthTenantGateway()
|
|
->findById($selectedTenantId);
|
|
if (!$tenantRecord) {
|
|
$setLoginWarning(t('We could not continue with these login details'));
|
|
} else {
|
|
$ldapConfigResult = $tenantSsoService->getEffectiveLdapProviderConfig($tenantRecord);
|
|
if (!($ldapConfigResult['ok'] ?? false)) {
|
|
$setLoginWarning(t('LDAP login is not available for this tenant.'));
|
|
} else {
|
|
$ldapConfig = $ldapConfigResult['config'];
|
|
$allowedDomains = trim((string) ($ldapConfig['allowed_domains'] ?? ''));
|
|
if ($allowedDomains !== '') {
|
|
$domainList = array_filter(array_map('trim', explode(',', $allowedDomains)));
|
|
if (!$tenantSsoService->isEmailDomainAllowed($email, $domainList)) {
|
|
$setLoginWarning(t('Your email domain is not allowed for LDAP login on this tenant.'));
|
|
return;
|
|
}
|
|
}
|
|
|
|
$ldapResult = $ldapAuthService->authenticate($ldapConfig, $ldapUsername !== '' ? $ldapUsername : $email, $ldapPassword);
|
|
if (!($ldapResult['ok'] ?? false)) {
|
|
$ldapError = $ldapResult['error'] ?? 'unknown';
|
|
if ($ldapError === 'credentials_empty') {
|
|
$setLoginWarning(t('Please enter your username and password.'));
|
|
} elseif ($ldapError === 'connection_failed') {
|
|
$setLoginWarning(t('LDAP server is not reachable. Please try again later.'));
|
|
} else {
|
|
$setLoginWarning(t('Username or password not valid'));
|
|
}
|
|
$passwordFailureResult = $rateLimiterService->registerFailure(
|
|
$loginPasswordScope,
|
|
$passwordAttemptKey,
|
|
$loginPasswordLimit,
|
|
$loginPasswordWindow,
|
|
$loginPasswordBlock
|
|
);
|
|
if (!($passwordFailureResult['allowed'] ?? true)) {
|
|
$setRateLimitWarning((int) ($passwordFailureResult['retry_after'] ?? $loginPasswordBlock));
|
|
}
|
|
} else {
|
|
$linkResult = $ssoUserLinkService->linkOrProvisionLdapUser($tenantRecord, $ldapResult, $ldapConfig);
|
|
if (!($linkResult['ok'] ?? false)) {
|
|
$linkError = $linkResult['error'] ?? 'unknown';
|
|
if ($linkError === 'user_inactive') {
|
|
$setLoginWarning(t('Your account is deactivated.'));
|
|
} elseif ($linkError === 'email_missing') {
|
|
$setLoginWarning(t('Your LDAP account does not have an email address configured.'));
|
|
} else {
|
|
$setLoginWarning(t('Login could not be completed. Please contact your administrator.'));
|
|
}
|
|
} else {
|
|
$ldapUserId = (int) ($linkResult['user_id'] ?? 0);
|
|
$loginResult = $authService->loginUserById($ldapUserId, $selectedTenantId, 'ldap');
|
|
if (!($loginResult['ok'] ?? false)) {
|
|
$setLoginWarning(t('Login could not be completed. Please contact your administrator.'));
|
|
} else {
|
|
$rateLimiterService->reset($loginPasswordScope, $passwordAttemptKey);
|
|
if ($remember || $tenantSsoService->shouldAutoRememberOnLdapLogin($selectedTenantId)) {
|
|
$rememberMeService->rememberUser($ldapUserId);
|
|
}
|
|
$sessionStore->set('session_started_at', time());
|
|
$sessionStore->set('session_last_activity', time());
|
|
Router::redirect($intendedUrlService->retrieveAndForget($sessionStore));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} elseif ($postedStage === 'login_password') {
|
|
$stage = 'methods';
|
|
$passwordAttemptKey = strtolower(trim($email)) . '|' . $clientIp;
|
|
$passwordLimitResult = $rateLimiterService->isBlocked($loginPasswordScope, $passwordAttemptKey);
|
|
if (!($passwordLimitResult['allowed'] ?? true)) {
|
|
$setRateLimitWarning((int) ($passwordLimitResult['retry_after'] ?? 60));
|
|
return;
|
|
}
|
|
|
|
if (empty($selectedTenantMethods['local'])) {
|
|
$setLoginWarning(t('Password login is disabled for this tenant. Please use Microsoft login.'));
|
|
} else {
|
|
$password = (string) (requestInput()->bodyAll()['password'] ?? '');
|
|
$remember = !empty(requestInput()->bodyAll()['remember']);
|
|
$result = $authService->login($email, $password);
|
|
if (!($result['ok'] ?? false)) {
|
|
if (!empty($result['needs_verification'])) {
|
|
$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');
|
|
return;
|
|
}
|
|
$setLoginWarning(t((string) ($result['message'] ?? 'Email/password not valid')));
|
|
$passwordFailureResult = $rateLimiterService->registerFailure(
|
|
$loginPasswordScope,
|
|
$passwordAttemptKey,
|
|
$loginPasswordLimit,
|
|
$loginPasswordWindow,
|
|
$loginPasswordBlock
|
|
);
|
|
if (!($passwordFailureResult['allowed'] ?? true)) {
|
|
$setRateLimitWarning((int) ($passwordFailureResult['retry_after'] ?? $loginPasswordBlock));
|
|
}
|
|
} else {
|
|
$authenticatedUser = $sessionStore->get('user', []);
|
|
$userId = (int) (is_array($authenticatedUser) ? ($authenticatedUser['id'] ?? 0) : 0);
|
|
if ($userId <= 0 || !$authService->canLoginToTenant($userId, $selectedTenantId)) {
|
|
$authService->logout();
|
|
$setLoginWarning(t('No access to this tenant'));
|
|
$passwordFailureResult = $rateLimiterService->registerFailure(
|
|
$loginPasswordScope,
|
|
$passwordAttemptKey,
|
|
$loginPasswordLimit,
|
|
$loginPasswordWindow,
|
|
$loginPasswordBlock
|
|
);
|
|
if (!($passwordFailureResult['allowed'] ?? true)) {
|
|
$setRateLimitWarning((int) ($passwordFailureResult['retry_after'] ?? $loginPasswordBlock));
|
|
}
|
|
} else {
|
|
$setTenant = $userTenantContextService->setCurrentTenant($userId, $selectedTenantId);
|
|
if (!($setTenant['ok'] ?? false)) {
|
|
$authService->logout();
|
|
$setLoginWarning(t('No access to this tenant'));
|
|
$passwordFailureResult = $rateLimiterService->registerFailure(
|
|
$loginPasswordScope,
|
|
$passwordAttemptKey,
|
|
$loginPasswordLimit,
|
|
$loginPasswordWindow,
|
|
$loginPasswordBlock
|
|
);
|
|
if (!($passwordFailureResult['allowed'] ?? true)) {
|
|
$setRateLimitWarning((int) ($passwordFailureResult['retry_after'] ?? $loginPasswordBlock));
|
|
}
|
|
} else {
|
|
$rateLimiterService->reset($loginPasswordScope, $passwordAttemptKey);
|
|
$authService->loadTenantDataIntoSession($userId);
|
|
if ($remember) {
|
|
$rememberMeService->rememberUser($userId);
|
|
}
|
|
$sessionStore->set('session_started_at', time());
|
|
$sessionStore->set('session_last_activity', time());
|
|
Router::redirect($intendedUrlService->retrieveAndForget($sessionStore));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$stage = 'resolve_email';
|
|
$setLoginWarning(t('We could not continue with these login details'));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$errors = $errorBag->toFlatList();
|