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