From 2368ee38252e6bce299a3b4ccb3e0a6cf7e70eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:48:21 +0300 Subject: [PATCH] =?UTF-8?q?Claude=20g=C3=BCncellemesi=20Login.Result=20d?= =?UTF-8?q?=C3=BCzeltmeleri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Identity/PlatformAccountController.cs | 6 +- .../Identity/PlatformEventIds.cs | 3 + .../Identity/PlatformSessionCleanupWorker.cs | 8 ++- .../Identity/PlatformSignInManager.cs | 56 +++++++++++++------ .../Identity/PlatformTokenController.cs | 14 +++-- ui/src/constants/login.result.enum.ts | 5 +- 6 files changed, 65 insertions(+), 27 deletions(-) diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformAccountController.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformAccountController.cs index b83c1429..e05f6cab 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformAccountController.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformAccountController.cs @@ -86,9 +86,11 @@ public class PlatformAccountController : AccountController }); var result = new PlatformLoginResult(signInResult, LP); - if (result.PResult != PlatformLoginResultType.Succeeded && user.AccessFailedCount >= maxAccessFailedCount) + + // Captcha yalnızca gerçek parola hatasında gösterilir. Lisans limiti, tenant pasif, + // çalışma saati gibi PreSignInCheck sonuçları ezilirse kullanıcı asıl sebebi göremez. + if (result.PResult == PlatformLoginResultType.Failed && user.AccessFailedCount >= maxAccessFailedCount) { - // Display captcha result.PResult = PlatformLoginResultType.ShowCaptcha; } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformEventIds.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformEventIds.cs index 24784b52..83fc2f21 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformEventIds.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformEventIds.cs @@ -27,6 +27,9 @@ public static class PlatformEventIds public static EventId UserCannotSignInConcurrentUserLimit = new(19, PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_ConcurrentUserLimit); + + public static EventId UserCannotSignInBranchLimit = + new(20, PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_BranchLimit); } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionCleanupWorker.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionCleanupWorker.cs index 718768b5..fd84e9f2 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionCleanupWorker.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionCleanupWorker.cs @@ -17,9 +17,11 @@ using Sozsoft.Platform.Extensions; namespace Sozsoft.Platform.Identity; /// -/// AbpSessions tablosundaki atıl (idle) oturumları temizleyen Hangfire job'ı. -/// Hem Hangfire (DoWorkAsync) hem de PlatformBackgroundWorker altyapısından (ISessionCleanupWorker) çağrılabilir. -/// Atıl sayılma eşiği: OpenIddict refresh token ömrü + 5 dakika. +/// AbpSessions tablosundaki atıl (idle) oturumları temizler; böylece kapatılan tarayıcıların +/// tuttuğu concurrent user lisansları serbest kalır. +/// WorkerType = SessionCleanupWorker olan kayıt üzerinden PlatformBackgroundWorker tarafından çalıştırılır. +/// Atıl sayılma eşiği: OpenIddict refresh token ömrü + 5 dakika. Aktif kullanıcılarda her +/// refresh token isteğinde oturum satırı yeniden oluşturulduğu için SignedIn güncel kalır. /// public class PlatformSessionCleanupWorker : ISessionCleanupWorker, ITransientDependency { diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSignInManager.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSignInManager.cs index dc865bdb..a09a013e 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSignInManager.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSignInManager.cs @@ -33,6 +33,7 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager { private const string UserLicenseProductName = "Public.products.userLicense"; private const string BranchLicenseProductName = "Public.products.branchHosting"; + private const string ApprovedOrderStatus = "Approved"; private readonly IClock clock; private readonly IRepository repositoryIp; @@ -289,16 +290,24 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager /// Tenant'a ait aktif Order'lardaki kullanıcı lisansı limitini aşmamak için login'i engeller. /// AbpSessions tablosundaki aktif oturumları sayar (farklı UserId'lere göre distinct). /// Kullanıcının kendisinin zaten oturumu varsa (refresh senaryosu) yeni concurrent user sayılmaz. + /// Yalnızca Internal kullanıcılar lisans tüketir. /// public async Task CheckConcurrentLimitAsync(IdentityUser user) { - // Eğer tenantId yoksa, kullanıcı tenant'a bağlı değil demektir. + // Eğer tenantId yoksa, kullanıcı tenant'a bağlı değil demektir (host). // Bu durumda concurrent limit kontrolü yapılmaz. if (!user.TenantId.HasValue) { return true; } + // External kullanıcılar sayıma dahil edilmediği için limitle de kısıtlanmazlar. + // (Aksi halde lisans dolu iken lisans tüketmeyen kullanıcılar da bloke olurdu.) + if (user.GetUserType() != PlatformConsts.UserType.Internal) + { + return true; + } + var maxConcurrentUsers = await GetActiveUserLicenseQuantityAsync(user.TenantId.Value); // Tenant bağlamını explicit olarak set et — hem password hem refresh token akışlarında @@ -306,7 +315,10 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager using (currentTenant.Change(user.TenantId)) { using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); - var sessions = await identitySessionRepository.GetListAsync(); + + // maxResultCount explicit verilir; aksi halde sayfalama limiti sessizce + // eksik sayıma ve lisans limitinin aşılmasına yol açabilir. + var sessions = await identitySessionRepository.GetListAsync(maxResultCount: int.MaxValue); // Kullanıcının kendi oturumu varsa (refresh) o kişi tekrar sayılmaz. var activeUserIds = sessions @@ -316,11 +328,16 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager .ToList(); // Aktif session kullanıcılarını IdentityUserManager üzerinden topluca yükle - // ve yalnızca UserType = 0 (Internal) olanları concurrent limite dahil et. - var otherActiveUserCount = (await userManager.Users - .Where(u => activeUserIds.Contains(u.Id)) - .ToListAsync()) - .Count(u => u.GetUserType() == PlatformConsts.UserType.Internal); + // ve yalnızca UserType = Internal olanları concurrent limite dahil et. + // (UserType ExtraProperties içinde tutulduğu için filtre bellekte uygulanır.) + var otherActiveUserCount = activeUserIds.Count == 0 + ? 0 + : (await userManager.Users + .Where(u => activeUserIds.Contains(u.Id)) + .ToListAsync()) + .Count(u => u.GetUserType() == PlatformConsts.UserType.Internal); + + await uow.CompleteAsync(); if (otherActiveUserCount >= maxConcurrentUsers) { @@ -329,8 +346,6 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager user.TenantId, maxConcurrentUsers, otherActiveUserCount); return false; } - - await uow.CompleteAsync(); } return true; @@ -349,7 +364,7 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager if (activeBranchCount > branchLicenseQuantity) { - Logger.LogWarning(PlatformEventIds.UserCannotSignInConcurrentUserLimit, + Logger.LogWarning(PlatformEventIds.UserCannotSignInBranchLimit, "Tenant {TenantId} branch license quantity of {Limit} is lower than active branch count {ActiveCount}.", tenantId, branchLicenseQuantity, activeBranchCount); return false; @@ -376,19 +391,28 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager return await GetActiveOrderItemQuantityAsync(tenantId, BranchLicenseProductName); } + /// + /// Tenant'ın yürürlükteki (onaylı, aktif ve lisans tarihi geçerli) siparişlerindeki + /// ilgili ürün adedini toplar. Order kayıtları host tarafında tutulduğu için + /// tenant filtresi kapatılır ve TenantId ile açıkça filtrelenir. + /// private async Task GetActiveOrderItemQuantityAsync(Guid tenantId, string productName) { using (currentTenant.Change(null)) { - var orderQuery = await orderRepository.WithDetailsAsync(order => order.Items); - var activeOrders = await orderQuery - .Where(order => order.TenantId == tenantId && order.ApprovalStatus == "Approved" && order.IsActive) - .ToListAsync(); + var now = clock.Now; + // Include gerekmiyor; toplam veritabanında hesaplandığı için GetQueryableAsync yeterli. + var orderQuery = await orderRepository.GetQueryableAsync(); - return activeOrders + return await orderQuery + .Where(order => order.TenantId == tenantId + && order.ApprovalStatus == ApprovedOrderStatus + && order.IsActive + && (order.LicenseStartTime == null || order.LicenseStartTime <= now) + && (order.LicenseEndTime == null || order.LicenseEndTime >= now)) .SelectMany(order => order.Items) .Where(item => item.ProductName == productName) - .Sum(item => item.Quantity); + .SumAsync(item => (int?)item.Quantity) ?? 0; } } } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformTokenController.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformTokenController.cs index bb0e43d7..c74f9b2f 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformTokenController.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformTokenController.cs @@ -149,8 +149,10 @@ public class PlatformTokenController : TokenController var result = new PlatformLoginResult(signInResult, LP); if (!signInResult.Succeeded) { - // Max deneme sayisini gectiyse kapca goster - if (user.AccessFailedCount >= maxAccessFailedCount) + // Max deneme sayisini gectiyse kapca goster. + // Yalnızca gerçek parola hatasında; lisans limiti, tenant pasif, çalışma saati gibi + // PreSignInCheck sonuçları ezilirse kullanıcı asıl sebebi göremez. + if (result.PResult == PlatformLoginResultType.Failed && user.AccessFailedCount >= maxAccessFailedCount) { result.PResult = PlatformLoginResultType.ShowCaptcha; } @@ -263,11 +265,13 @@ Your login code: {twoFactorToken}"; { await ReplaceUserSessionAsync(user, request.ClientId, sessionId, checkConcurrentLimit: true); } - catch (UserFriendlyException) { throw; } catch (Exception ex) { - Logger.LogWarning(ex, "Failed to manage AbpSessions. UserId: {UserId}", user.Id); - throw new UserFriendlyException(PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_ConcurrentUserLimit); + // Oturum satırı oluşmazsa PlatformSessionValidationMiddleware sonraki her isteği 401'e + // düşürür; bu yüzden hata yutulmaz. Lisans limiti zaten UserFriendlyException olarak gelir, + // diğer hataları lisans hatası gibi göstermek yanıltıcı olur. + Logger.LogError(ex, "Failed to manage AbpSessions. UserId: {UserId}", user.Id); + throw; } } diff --git a/ui/src/constants/login.result.enum.ts b/ui/src/constants/login.result.enum.ts index be147192..25230a09 100644 --- a/ui/src/constants/login.result.enum.ts +++ b/ui/src/constants/login.result.enum.ts @@ -12,7 +12,10 @@ const PlatformLoginResultType = { NotAllowedIp: 19, // IpRestriction tablosu, Mesaj: Identity:IpRestrictionError LoginEndDateDue: 20, // LoginEndDate:>Today, Buton: ShowExtendMyLoginButton, Mesaj: Identity:LoginEndDateError, >LoginEndDate ShowCaptcha: 21, // AccessFailedCount>AccountCaptchaMaxFailedAccessAttempts, Sayfa: Show Captcha - ConcurrentUserLimit: 22, // MaxConcurrentUsers limiti dolduğunda, Mesaj: Identity:ConcurrentUserLimitError + TenantIsPassive: 22, // Tenant pasif, Mesaj: Identity:TenantIsPassive + NotAllowedWorkHour: 23, // WorkHour tanimi disinda giris, Mesaj: Identity:LoginNotAllowed_WorkHour + BranchLimit: 24, // Aktif sube sayisi sube lisansini asiyor, Mesaj: Identity:BranchLimitError + ConcurrentUserLimit: 25, // Aktif kullanici lisansi dolduğunda, Mesaj: Identity:ConcurrentUserLimitError } export default PlatformLoginResultType