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 PhoneNumber { get; set; }
public string Password { get; set; }
public string WorkHour { get; set; }
public bool? IsActive { get; set; }
public bool? LockoutEnabled { get; set; }
public string[] RoleNames { get; set; }

View file

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

View file

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

View file

@ -1,4 +1,4 @@
{
{
"Languages": [
{
"cultureName": "ar",
@ -3113,6 +3113,12 @@
"key": "Abp.Identity.LoginNotAllowed_WorkHour",
"en": "You cannot sign in outside of the allowed work hours.",
"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",

View file

@ -68,6 +68,7 @@ public static class PlatformConsts
public const string Email = "Email";
public const string Website = "Website";
public const string MenuGroup = "MenuGroup";
public const string MaxConcurrentUsers = "MaxConcurrentUsers";
}
public static class AbpIdentity
@ -107,6 +108,7 @@ public static class PlatformConsts
public const string LoginEndDateError = GroupName + ".LoginEndDateError";
public const string TenantIsPassive = GroupName + ".TenantIsPassive";
public const string LoginNotAllowed_WorkHour = GroupName + ".LoginNotAllowed_WorkHour";
public const string ConcurrentUserLimitError = GroupName + ".ConcurrentUserLimitError";
public const string CaptchaWrongCode = GroupName + ".CaptchaWrongCode";
public const string TwoFactorWrongCode = GroupName + ".TwoFactorWrongCode";
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_TenantNotFound { get; set; } = "UserCannotSignInTenantNotFound";
public static string LoginNotAllowed_WorkHour { get; set; } = "UserCannotSignInWorkHour";
public static string LoginNotAllowed_ConcurrentUserLimit { get; set; } = "UserCannotSignInConcurrentUserLimit";
}
public static class GridOptions

View file

@ -93,6 +93,7 @@ public static class PlatformModuleExtensionConfigurator
tenantConfig.ConfigureTenant(entity =>
{
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);
}
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_ConcurrentUserLimit { get; set; }
public override string ToString()
{
return
@ -40,6 +42,7 @@ public class PlatformSignInResult : SignInResult
ShouldChangePasswordPeriodic ? "ShouldChangePasswordPeriodic" :
IsNotAllowed_TenantIsPassive ? "NotAllowed_TenantIsPassive" :
IsNotAllowed_WorkHour ? "NotAllowed_WorkHour" :
IsNotAllowed_ConcurrentUserLimit ? "NotAllowed_ConcurrentUserLimit" :
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
.MapEfCoreProperty<PermissionDefinitionRecord, string>(
PlatformConsts.Permissions.MenuGroup,

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260426180109_Initial")]
[Migration("20260427070853_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -6164,6 +6164,11 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int?>("MaxConcurrentUsers")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<string>("MenuGroup")
.HasMaxLength(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),
Founder = table.Column<string>(type: "nvarchar(max)", nullable: 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),
MobileNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, 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")
.HasColumnName("LastModifierId");
b.Property<int?>("MaxConcurrentUsers")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<string>("MenuGroup")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");

View file

@ -24,6 +24,9 @@ public static class PlatformEventIds
public static EventId UserCannotSignInWorkHour =
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;
Description = L[PlatformConsts.AbpIdentity.User.LoginNotAllowed_WorkHour];
}
else if (resultP.IsNotAllowed_ConcurrentUserLimit)
{
PResult = PlatformLoginResultType.ConcurrentUserLimit;
Description = L[PlatformConsts.AbpIdentity.User.ConcurrentUserLimitError];
}
}
else
{

View file

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

View file

@ -18,6 +18,7 @@ using Volo.Abp.TenantManagement;
using Volo.Abp.Timing;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
using Microsoft.EntityFrameworkCore;
namespace Sozsoft.Platform.Identity;
@ -33,6 +34,7 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
private readonly IRepository<WorkHour, Guid> repositoryWorkHour;
private readonly ITenantRepository tenantRepository;
private readonly IdentityUserManager userManager;
private readonly IIdentitySessionRepository identitySessionRepository;
public PlatformSignInManager(
IdentityUserManager userManager,
@ -47,7 +49,8 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
IClock clock,
IRepository<IpRestriction, Guid> repositoryIp,
IRepository<WorkHour, Guid> repositoryWorkHour,
ITenantRepository tenantRepository
ITenantRepository tenantRepository,
IIdentitySessionRepository identitySessionRepository
) : base(
userManager,
contextAccessor,
@ -64,6 +67,7 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
this.repositoryWorkHour = repositoryWorkHour;
this.tenantRepository = tenantRepository;
this.userManager = userManager;
this.identitySessionRepository = identitySessionRepository;
}
public async Task<bool> PreSignInCheckAsync(IdentityUser user)
@ -98,6 +102,10 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
{
return new PlatformSignInResult() { IsNotAllowed_WorkHour = true };
}
if (!await CanSignInConcurrentLimitAsync(user))
{
return new PlatformSignInResult() { IsNotAllowed_ConcurrentUserLimit = true };
}
}
else
{
@ -256,5 +264,47 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
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
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
}
export default PlatformLoginResultType