ConcurrentUserLimit tanımlamalası

This commit is contained in:
Sedat ÖZTÜRK 2026-04-27 16:02:09 +03:00
parent c4671f7051
commit 70fb9ae499
17 changed files with 126 additions and 11 deletions

View file

@ -10,6 +10,7 @@ public class CreateUpdateUserInput
public string Email { get; set; } public string Email { get; set; }
public string PhoneNumber { get; set; } public string PhoneNumber { get; set; }
public string Password { get; set; } public string Password { get; set; }
public string WorkHour { get; set; }
public bool? IsActive { get; set; } public bool? IsActive { get; set; }
public bool? LockoutEnabled { get; set; } public bool? LockoutEnabled { get; set; }
public string[] RoleNames { get; set; } public string[] RoleNames { get; set; }

View file

@ -58,6 +58,10 @@ public static class PlatformSignInResultExtensions
{ {
return PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_WorkHour; return PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_WorkHour;
} }
if (resultP.IsNotAllowed_ConcurrentUserLimit)
{
return PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_ConcurrentUserLimit;
}
} }
/// Added --> /// Added -->

View file

@ -7,6 +7,7 @@ using Volo.Abp;
using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities;
using Volo.Abp.Identity; using Volo.Abp.Identity;
using Volo.Abp.TenantManagement; using Volo.Abp.TenantManagement;
using Volo.Abp.Data;
namespace Sozsoft.Platform.ListForms.DynamicApi; namespace Sozsoft.Platform.ListForms.DynamicApi;
@ -33,7 +34,7 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
[Authorize(IdentityPermissions.Users.Create)] [Authorize(IdentityPermissions.Users.Create)]
public async Task PostUserInsertAsync(DynamicApiBaseInput<CreateUpdateUserInput> input) public async Task PostUserInsertAsync(DynamicApiBaseInput<CreateUpdateUserInput> input)
{ {
await identityUserAppService.CreateAsync(new IdentityUserCreateDto var user = new IdentityUserCreateDto
{ {
UserName = input.Data.Email, UserName = input.Data.Email,
Name = input.Data.Name, Name = input.Data.Name,
@ -43,8 +44,11 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
IsActive = input.Data.IsActive ?? true, IsActive = input.Data.IsActive ?? true,
LockoutEnabled = true, LockoutEnabled = true,
Password = input.Data.Password, Password = input.Data.Password,
RoleNames = [] //input.Data.RoleNames, RoleNames = [], //input.Data.RoleNames,
}); };
user.SetProperty(PlatformConsts.AbpIdentity.User.WorkHour, input.Data.WorkHour);
await identityUserAppService.CreateAsync(user);
} }
[Authorize(IdentityPermissions.Users.Update)] [Authorize(IdentityPermissions.Users.Update)]
@ -54,7 +58,7 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
{ {
throw new UserFriendlyException(L["RecordNotFound"]); throw new UserFriendlyException(L["RecordNotFound"]);
} }
var id = Guid.Parse(input.Keys[0].ToString()); var id = Guid.Parse(input.Keys[0]!.ToString()!);
var entity = await identityUserAppService.GetAsync(id) ?? throw new EntityNotFoundException(L["RecordNotFound"]); var entity = await identityUserAppService.GetAsync(id) ?? throw new EntityNotFoundException(L["RecordNotFound"]);
var user = new IdentityUserUpdateDto var user = new IdentityUserUpdateDto
{ {
@ -71,6 +75,10 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
{ {
user.Password = input.Data.Password; user.Password = input.Data.Password;
} }
if (input.Data.WorkHour != null)
{
user.SetProperty(PlatformConsts.AbpIdentity.User.WorkHour, input.Data.WorkHour);
}
await identityUserAppService.UpdateAsync(id, user); await identityUserAppService.UpdateAsync(id, user);
} }
@ -94,7 +102,7 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
{ {
throw new UserFriendlyException(L["RecordNotFound"]); throw new UserFriendlyException(L["RecordNotFound"]);
} }
var id = Guid.Parse(input.Keys[0].ToString()); var id = Guid.Parse(input.Keys[0]!.ToString()!);
var entity = await identityRoleAppService.GetAsync(id) ?? throw new EntityNotFoundException(L["RecordNotFound"]); var entity = await identityRoleAppService.GetAsync(id) ?? throw new EntityNotFoundException(L["RecordNotFound"]);
var role = new IdentityRoleUpdateDto var role = new IdentityRoleUpdateDto
{ {
@ -142,7 +150,7 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
throw new UserFriendlyException(L["RecordNotFound"]); throw new UserFriendlyException(L["RecordNotFound"]);
} }
var id = Guid.Parse(input.Keys[0].ToString()); var id = Guid.Parse(input.Keys[0]!.ToString()!);
var entity = await tenantRepository.GetAsync(id) var entity = await tenantRepository.GetAsync(id)
?? throw new EntityNotFoundException(L["RecordNotFound"]); ?? throw new EntityNotFoundException(L["RecordNotFound"]);
@ -176,7 +184,7 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
{ {
throw new UserFriendlyException(L["RecordNotFound"]); throw new UserFriendlyException(L["RecordNotFound"]);
} }
var id = Guid.Parse(input.Keys[0].ToString()); var id = Guid.Parse(input.Keys[0]!.ToString()!);
await tenantRepository.DeleteAsync(id); await tenantRepository.DeleteAsync(id);
} }

View file

@ -1,4 +1,4 @@
{ {
"Languages": [ "Languages": [
{ {
"cultureName": "ar", "cultureName": "ar",
@ -3113,6 +3113,12 @@
"key": "Abp.Identity.LoginNotAllowed_WorkHour", "key": "Abp.Identity.LoginNotAllowed_WorkHour",
"en": "You cannot sign in outside of the allowed work hours.", "en": "You cannot sign in outside of the allowed work hours.",
"tr": "İzin verilen iş saatleri dışında giriş yapamazsınız." "tr": "İzin verilen iş saatleri dışında giriş yapamazsınız."
},
{
"resourceName": "Platform",
"key": "Abp.Identity.ConcurrentUserLimitError",
"en": "The maximum number of simultaneous users has been reached. Please try again later.",
"tr": "Eş zamanlı kullanıcı limiti dolmuştur. Lütfen daha sonra tekrar deneyiniz."
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",

View file

@ -68,6 +68,7 @@ public static class PlatformConsts
public const string Email = "Email"; public const string Email = "Email";
public const string Website = "Website"; public const string Website = "Website";
public const string MenuGroup = "MenuGroup"; public const string MenuGroup = "MenuGroup";
public const string MaxConcurrentUsers = "MaxConcurrentUsers";
} }
public static class AbpIdentity public static class AbpIdentity
@ -107,6 +108,7 @@ public static class PlatformConsts
public const string LoginEndDateError = GroupName + ".LoginEndDateError"; public const string LoginEndDateError = GroupName + ".LoginEndDateError";
public const string TenantIsPassive = GroupName + ".TenantIsPassive"; public const string TenantIsPassive = GroupName + ".TenantIsPassive";
public const string LoginNotAllowed_WorkHour = GroupName + ".LoginNotAllowed_WorkHour"; public const string LoginNotAllowed_WorkHour = GroupName + ".LoginNotAllowed_WorkHour";
public const string ConcurrentUserLimitError = GroupName + ".ConcurrentUserLimitError";
public const string CaptchaWrongCode = GroupName + ".CaptchaWrongCode"; public const string CaptchaWrongCode = GroupName + ".CaptchaWrongCode";
public const string TwoFactorWrongCode = GroupName + ".TwoFactorWrongCode"; public const string TwoFactorWrongCode = GroupName + ".TwoFactorWrongCode";
public const string SignOut = GroupName + ".SignOut"; public const string SignOut = GroupName + ".SignOut";
@ -450,6 +452,7 @@ public static class PlatformConsts
public static string LoginNotAllowed_TenantIsPassive { get; set; } = "UserCannotSignInTenantIsPassive"; public static string LoginNotAllowed_TenantIsPassive { get; set; } = "UserCannotSignInTenantIsPassive";
public static string LoginNotAllowed_TenantNotFound { get; set; } = "UserCannotSignInTenantNotFound"; public static string LoginNotAllowed_TenantNotFound { get; set; } = "UserCannotSignInTenantNotFound";
public static string LoginNotAllowed_WorkHour { get; set; } = "UserCannotSignInWorkHour"; public static string LoginNotAllowed_WorkHour { get; set; } = "UserCannotSignInWorkHour";
public static string LoginNotAllowed_ConcurrentUserLimit { get; set; } = "UserCannotSignInConcurrentUserLimit";
} }
public static class GridOptions public static class GridOptions

View file

@ -93,6 +93,7 @@ public static class PlatformModuleExtensionConfigurator
tenantConfig.ConfigureTenant(entity => tenantConfig.ConfigureTenant(entity =>
{ {
entity.AddOrUpdateProperty<bool>("IsActive"); entity.AddOrUpdateProperty<bool>("IsActive");
entity.AddOrUpdateProperty<int?>("MaxConcurrentUsers");
}); });
}); });

View file

@ -165,5 +165,14 @@ public static class AbpTenantExtensions
{ {
return tenant.GetProperty<string>(PlatformConsts.Tenants.MenuGroup); return tenant.GetProperty<string>(PlatformConsts.Tenants.MenuGroup);
} }
public static void SetMaxConcurrentUsers(this Tenant tenant, int? maxConcurrentUsers)
{
tenant.SetProperty(PlatformConsts.Tenants.MaxConcurrentUsers, maxConcurrentUsers.HasValue ? (object)maxConcurrentUsers.Value : null);
}
public static int? GetMaxConcurrentUsers(this Tenant tenant)
{
return tenant.GetProperty<int?>(PlatformConsts.Tenants.MaxConcurrentUsers);
}
} }

View file

@ -30,6 +30,8 @@ public class PlatformSignInResult : SignInResult
public bool IsNotAllowed_WorkHour { get; set; } public bool IsNotAllowed_WorkHour { get; set; }
public bool IsNotAllowed_ConcurrentUserLimit { get; set; }
public override string ToString() public override string ToString()
{ {
return return
@ -40,6 +42,7 @@ public class PlatformSignInResult : SignInResult
ShouldChangePasswordPeriodic ? "ShouldChangePasswordPeriodic" : ShouldChangePasswordPeriodic ? "ShouldChangePasswordPeriodic" :
IsNotAllowed_TenantIsPassive ? "NotAllowed_TenantIsPassive" : IsNotAllowed_TenantIsPassive ? "NotAllowed_TenantIsPassive" :
IsNotAllowed_WorkHour ? "NotAllowed_WorkHour" : IsNotAllowed_WorkHour ? "NotAllowed_WorkHour" :
IsNotAllowed_ConcurrentUserLimit ? "NotAllowed_ConcurrentUserLimit" :
base.ToString(); base.ToString();
} }
} }

View file

@ -217,6 +217,15 @@ public static class PlatformEfCoreEntityExtensionMappings
} }
); );
ObjectExtensionManager.Instance
.MapEfCoreProperty<Tenant, int?>(
PlatformConsts.Tenants.MaxConcurrentUsers,
(entityBuilder, propertyBuilder) =>
{
propertyBuilder.HasDefaultValue(0);
}
);
ObjectExtensionManager.Instance ObjectExtensionManager.Instance
.MapEfCoreProperty<PermissionDefinitionRecord, string>( .MapEfCoreProperty<PermissionDefinitionRecord, string>(
PlatformConsts.Permissions.MenuGroup, PlatformConsts.Permissions.MenuGroup,

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations namespace Sozsoft.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20260426180109_Initial")] [Migration("20260427070853_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -6164,6 +6164,11 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("uniqueidentifier") .HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId"); .HasColumnName("LastModifierId");
b.Property<int?>("MaxConcurrentUsers")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<string>("MenuGroup") b.Property<string>("MenuGroup")
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("nvarchar(64)"); .HasColumnType("nvarchar(64)");

View file

@ -350,6 +350,7 @@ namespace Sozsoft.Platform.Migrations
FaxNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true), FaxNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
Founder = table.Column<string>(type: "nvarchar(max)", nullable: true), Founder = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true), IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
MaxConcurrentUsers = table.Column<int>(type: "int", nullable: true, defaultValue: 0),
MenuGroup = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true), MenuGroup = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
MobileNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true), MobileNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
OrganizationName = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true), OrganizationName = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),

View file

@ -6161,6 +6161,11 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("uniqueidentifier") .HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId"); .HasColumnName("LastModifierId");
b.Property<int?>("MaxConcurrentUsers")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<string>("MenuGroup") b.Property<string>("MenuGroup")
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("nvarchar(64)"); .HasColumnType("nvarchar(64)");

View file

@ -24,6 +24,9 @@ public static class PlatformEventIds
public static EventId UserCannotSignInWorkHour = public static EventId UserCannotSignInWorkHour =
new(18, PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_WorkHour); new(18, PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_WorkHour);
public static EventId UserCannotSignInConcurrentUserLimit =
new(19, PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_ConcurrentUserLimit);
} }

View file

@ -64,6 +64,11 @@ public class PlatformLoginResult : AbpLoginResult
PResult = PlatformLoginResultType.NotAllowedWorkHour; PResult = PlatformLoginResultType.NotAllowedWorkHour;
Description = L[PlatformConsts.AbpIdentity.User.LoginNotAllowed_WorkHour]; Description = L[PlatformConsts.AbpIdentity.User.LoginNotAllowed_WorkHour];
} }
else if (resultP.IsNotAllowed_ConcurrentUserLimit)
{
PResult = PlatformLoginResultType.ConcurrentUserLimit;
Description = L[PlatformConsts.AbpIdentity.User.ConcurrentUserLimitError];
}
} }
else else
{ {

View file

@ -15,6 +15,7 @@ public enum PlatformLoginResultType : byte
LoginEndDateDue, LoginEndDateDue,
ShowCaptcha, ShowCaptcha,
TenantIsPassive, TenantIsPassive,
NotAllowedWorkHour NotAllowedWorkHour,
ConcurrentUserLimit
} }

View file

@ -18,6 +18,7 @@ using Volo.Abp.TenantManagement;
using Volo.Abp.Timing; using Volo.Abp.Timing;
using IdentityUser = Volo.Abp.Identity.IdentityUser; using IdentityUser = Volo.Abp.Identity.IdentityUser;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
using Microsoft.EntityFrameworkCore;
namespace Sozsoft.Platform.Identity; namespace Sozsoft.Platform.Identity;
@ -33,6 +34,7 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
private readonly IRepository<WorkHour, Guid> repositoryWorkHour; private readonly IRepository<WorkHour, Guid> repositoryWorkHour;
private readonly ITenantRepository tenantRepository; private readonly ITenantRepository tenantRepository;
private readonly IdentityUserManager userManager; private readonly IdentityUserManager userManager;
private readonly IIdentitySessionRepository identitySessionRepository;
public PlatformSignInManager( public PlatformSignInManager(
IdentityUserManager userManager, IdentityUserManager userManager,
@ -47,7 +49,8 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
IClock clock, IClock clock,
IRepository<IpRestriction, Guid> repositoryIp, IRepository<IpRestriction, Guid> repositoryIp,
IRepository<WorkHour, Guid> repositoryWorkHour, IRepository<WorkHour, Guid> repositoryWorkHour,
ITenantRepository tenantRepository ITenantRepository tenantRepository,
IIdentitySessionRepository identitySessionRepository
) : base( ) : base(
userManager, userManager,
contextAccessor, contextAccessor,
@ -64,6 +67,7 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
this.repositoryWorkHour = repositoryWorkHour; this.repositoryWorkHour = repositoryWorkHour;
this.tenantRepository = tenantRepository; this.tenantRepository = tenantRepository;
this.userManager = userManager; this.userManager = userManager;
this.identitySessionRepository = identitySessionRepository;
} }
public async Task<bool> PreSignInCheckAsync(IdentityUser user) public async Task<bool> PreSignInCheckAsync(IdentityUser user)
@ -98,6 +102,10 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
{ {
return new PlatformSignInResult() { IsNotAllowed_WorkHour = true }; return new PlatformSignInResult() { IsNotAllowed_WorkHour = true };
} }
if (!await CanSignInConcurrentLimitAsync(user))
{
return new PlatformSignInResult() { IsNotAllowed_ConcurrentUserLimit = true };
}
} }
else else
{ {
@ -256,5 +264,47 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
return true; return true;
} }
/// <summary>
/// Prevents login when the tenant's concurrent user limit is reached.
/// Counts distinct active users (by UserId) in AbpSessions for the tenant.
/// A user who already has an active session is not counted as a new concurrent user.
/// </summary>
private async Task<bool> CanSignInConcurrentLimitAsync(IdentityUser user)
{
if (!user.TenantId.HasValue)
{
return true;
}
var tenant = await tenantRepository.FindAsync(user.TenantId.Value);
if (tenant == null)
{
return true;
}
var maxConcurrentUsers = tenant.GetMaxConcurrentUsers();
if (!maxConcurrentUsers.HasValue || maxConcurrentUsers.Value <= 0)
{
return true;
}
var sessions = await identitySessionRepository.GetListAsync();
var activeUserCount = sessions
.Where(s => s.UserId != user.Id)
.Select(s => s.UserId)
.Distinct()
.Count();
if (activeUserCount >= maxConcurrentUsers.Value)
{
Logger.LogWarning(PlatformEventIds.UserCannotSignInConcurrentUserLimit,
"Tenant {TenantId} concurrent user limit of {Limit} reached. Active users: {ActiveCount}.",
user.TenantId, maxConcurrentUsers.Value, activeUserCount);
return false;
}
return true;
}
} }

View file

@ -12,6 +12,7 @@ const PlatformLoginResultType = {
NotAllowedIp: 19, // IpRestriction tablosu, Mesaj: Identity:IpRestrictionError NotAllowedIp: 19, // IpRestriction tablosu, Mesaj: Identity:IpRestrictionError
LoginEndDateDue: 20, // LoginEndDate:>Today, Buton: ShowExtendMyLoginButton, Mesaj: Identity:LoginEndDateError, >LoginEndDate LoginEndDateDue: 20, // LoginEndDate:>Today, Buton: ShowExtendMyLoginButton, Mesaj: Identity:LoginEndDateError, >LoginEndDate
ShowCaptcha: 21, // AccessFailedCount>AccountCaptchaMaxFailedAccessAttempts, Sayfa: Show Captcha ShowCaptcha: 21, // AccessFailedCount>AccountCaptchaMaxFailedAccessAttempts, Sayfa: Show Captcha
ConcurrentUserLimit: 22, // MaxConcurrentUsers limiti dolduğunda, Mesaj: Identity:ConcurrentUserLimitError
} }
export default PlatformLoginResultType export default PlatformLoginResultType