ConcurrentUser Çalışması
This commit is contained in:
parent
70fb9ae499
commit
ce3028ce56
17 changed files with 424 additions and 57 deletions
|
|
@ -28,7 +28,7 @@ public class MailQueueWorker : BackgroundWorkerBase
|
||||||
public IGuidGenerator GuidGenerator { get; }
|
public IGuidGenerator GuidGenerator { get; }
|
||||||
|
|
||||||
public MailQueueWorker(
|
public MailQueueWorker(
|
||||||
ISozsoftEmailSender ErpEmailSender,
|
ISozsoftEmailSender erpEmailSender,
|
||||||
IRepository<Entities.BackgroundWorker_MailQueue> repository,
|
IRepository<Entities.BackgroundWorker_MailQueue> repository,
|
||||||
IClock clock,
|
IClock clock,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
|
|
@ -39,7 +39,7 @@ public class MailQueueWorker : BackgroundWorkerBase
|
||||||
IGuidGenerator guidGenerator
|
IGuidGenerator guidGenerator
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
ErpEmailSender = ErpEmailSender;
|
ErpEmailSender = erpEmailSender;
|
||||||
Repository = repository;
|
Repository = repository;
|
||||||
Clock = clock;
|
Clock = clock;
|
||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
|
|
|
||||||
|
|
@ -338,8 +338,8 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
|
||||||
Id = EncodePathAsId(newFolderPath),
|
Id = EncodePathAsId(newFolderPath),
|
||||||
Name = input.Name,
|
Name = input.Name,
|
||||||
Type = "folder",
|
Type = "folder",
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.Now,
|
||||||
ModifiedAt = DateTime.UtcNow,
|
ModifiedAt = DateTime.Now,
|
||||||
Path = newFolderPath,
|
Path = newFolderPath,
|
||||||
ParentId = decodedParentId ?? string.Empty,
|
ParentId = decodedParentId ?? string.Empty,
|
||||||
IsReadOnly = false,
|
IsReadOnly = false,
|
||||||
|
|
@ -430,8 +430,8 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
|
||||||
Size = fileSize,
|
Size = fileSize,
|
||||||
Extension = normalizedExtension,
|
Extension = normalizedExtension,
|
||||||
MimeType = GetMimeType(normalizedExtension),
|
MimeType = GetMimeType(normalizedExtension),
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.Now,
|
||||||
ModifiedAt = DateTime.UtcNow,
|
ModifiedAt = DateTime.Now,
|
||||||
Path = filePath,
|
Path = filePath,
|
||||||
ParentId = decodedParentId ?? string.Empty,
|
ParentId = decodedParentId ?? string.Empty,
|
||||||
IsReadOnly = false,
|
IsReadOnly = false,
|
||||||
|
|
@ -500,13 +500,13 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
|
||||||
// Update metadata
|
// Update metadata
|
||||||
metadata.Name = input.Name;
|
metadata.Name = input.Name;
|
||||||
metadata.Path = newPath;
|
metadata.Path = newPath;
|
||||||
metadata.ModifiedAt = DateTime.UtcNow;
|
metadata.ModifiedAt = DateTime.Now;
|
||||||
|
|
||||||
// Update parent index
|
// Update parent index
|
||||||
var itemToUpdate = parentItems.First(x => x.Id == id);
|
var itemToUpdate = parentItems.First(x => x.Id == id);
|
||||||
itemToUpdate.Name = input.Name;
|
itemToUpdate.Name = input.Name;
|
||||||
itemToUpdate.Path = newPath;
|
itemToUpdate.Path = newPath;
|
||||||
itemToUpdate.ModifiedAt = DateTime.UtcNow;
|
itemToUpdate.ModifiedAt = DateTime.Now;
|
||||||
|
|
||||||
await SaveFolderIndexAsync(parentItems, tenantId, metadata.ParentId == string.Empty ? null : metadata.ParentId);
|
await SaveFolderIndexAsync(parentItems, tenantId, metadata.ParentId == string.Empty ? null : metadata.ParentId);
|
||||||
|
|
||||||
|
|
@ -570,7 +570,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
|
||||||
// Update metadata
|
// Update metadata
|
||||||
metadata.Path = newPath;
|
metadata.Path = newPath;
|
||||||
metadata.ParentId = input.TargetFolderId ?? string.Empty;
|
metadata.ParentId = input.TargetFolderId ?? string.Empty;
|
||||||
metadata.ModifiedAt = DateTime.UtcNow;
|
metadata.ModifiedAt = DateTime.Now;
|
||||||
|
|
||||||
// Add to target
|
// Add to target
|
||||||
targetItems.Add(metadata);
|
targetItems.Add(metadata);
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ public class PlatformIdentityAppService : ApplicationService
|
||||||
userRoleNames = userRoleNames,
|
userRoleNames = userRoleNames,
|
||||||
LockoutEnabled = user.LockoutEnabled,
|
LockoutEnabled = user.LockoutEnabled,
|
||||||
LockoutEnd = user.LockoutEnd,
|
LockoutEnd = user.LockoutEnd,
|
||||||
LockUser = user.LockoutEnabled && user.LockoutEnd.HasValue && user.LockoutEnd.Value.DateTime > DateTime.UtcNow,
|
LockUser = user.LockoutEnabled && user.LockoutEnd.HasValue && user.LockoutEnd.Value.DateTime > DateTime.Now,
|
||||||
LoginEndDate = user.GetLoginEndDate(),
|
LoginEndDate = user.GetLoginEndDate(),
|
||||||
ConcurrencyStamp = user.ConcurrencyStamp,
|
ConcurrencyStamp = user.ConcurrencyStamp,
|
||||||
LastPasswordChangeTime = user.LastPasswordChangeTime,
|
LastPasswordChangeTime = user.LastPasswordChangeTime,
|
||||||
|
|
@ -140,7 +140,7 @@ public class PlatformIdentityAppService : ApplicationService
|
||||||
if (UserInfo.LockUser)
|
if (UserInfo.LockUser)
|
||||||
{
|
{
|
||||||
await UserManager.SetLockoutEnabledAsync(user, true);
|
await UserManager.SetLockoutEnabledAsync(user, true);
|
||||||
await UserManager.SetLockoutEndDateAsync(user, DateTime.UtcNow.AddYears(1000));
|
await UserManager.SetLockoutEndDateAsync(user, DateTime.Now.AddYears(1000));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1055,10 +1055,17 @@
|
||||||
"BackgroundWorkers": [
|
"BackgroundWorkers": [
|
||||||
{
|
{
|
||||||
"name": "Notification Worker",
|
"name": "Notification Worker",
|
||||||
"cron": "5 * * * *",
|
"cron": "*/5 * * * *",
|
||||||
"workerType": "NotificationWorker",
|
"workerType": "NotificationWorker",
|
||||||
"isActive": true,
|
"isActive": true,
|
||||||
"dataSourceCode": "Default"
|
"dataSourceCode": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Session Cleanup Worker",
|
||||||
|
"cron": "*/5 * * * *",
|
||||||
|
"workerType": "SessionCleanupWorker",
|
||||||
|
"isActive": true,
|
||||||
|
"dataSourceCode": "Default"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ContactTitles": [
|
"ContactTitles": [
|
||||||
|
|
|
||||||
|
|
@ -3117,8 +3117,8 @@
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "Abp.Identity.ConcurrentUserLimitError",
|
"key": "Abp.Identity.ConcurrentUserLimitError",
|
||||||
"en": "The maximum number of simultaneous users has been reached. Please try again later.",
|
"en": "The maximum number of simultaneous users has been reached.",
|
||||||
"tr": "Eş zamanlı kullanıcı limiti dolmuştur. Lütfen daha sonra tekrar deneyiniz."
|
"tr": "Eş zamanlı kullanıcı limiti dolmuştur."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
@ -13682,6 +13682,12 @@
|
||||||
"en": "Is Active",
|
"en": "Is Active",
|
||||||
"tr": "Aktif"
|
"tr": "Aktif"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Listform.ListformField.MaxConcurrentUsers",
|
||||||
|
"en": "Max Concurrent Users",
|
||||||
|
"tr": "Maksimum Eşzamanlı Kullanıcı"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Listform.ListformField.IsVerified",
|
"key": "App.Listform.ListformField.IsVerified",
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
new EditingFormItemDto { Order=7, DataField = "PhoneNumber", ColSpan=1, IsRequired=false, EditorType2=EditorTypes.dxTextBox, EditorOptions=EditorOptionValues.PhoneEditorOptions },
|
new EditingFormItemDto { Order=7, DataField = "PhoneNumber", ColSpan=1, IsRequired=false, EditorType2=EditorTypes.dxTextBox, EditorOptions=EditorOptionValues.PhoneEditorOptions },
|
||||||
new EditingFormItemDto { Order=8, DataField = "FaxNumber", ColSpan=1, IsRequired=false, EditorType2=EditorTypes.dxTextBox, EditorOptions=EditorOptionValues.PhoneEditorOptions },
|
new EditingFormItemDto { Order=8, DataField = "FaxNumber", ColSpan=1, IsRequired=false, EditorType2=EditorTypes.dxTextBox, EditorOptions=EditorOptionValues.PhoneEditorOptions },
|
||||||
new EditingFormItemDto { Order=9, DataField = "IsActive", ColSpan=1, IsRequired=false, EditorType2=EditorTypes.dxCheckBox },
|
new EditingFormItemDto { Order=9, DataField = "IsActive", ColSpan=1, IsRequired=false, EditorType2=EditorTypes.dxCheckBox },
|
||||||
|
new EditingFormItemDto { Order=10, DataField = "MaxConcurrentUsers", ColSpan=1, IsRequired=false, EditorType2=EditorTypes.dxNumberBox },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
new() { Order=2, ColCount=2, ColSpan=1, ItemType="group", Items =
|
new() { Order=2, ColCount=2, ColSpan=1, ItemType="group", Items =
|
||||||
|
|
@ -182,7 +183,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "Name",
|
FieldName = "Name",
|
||||||
CaptionName = "App.Listform.ListformField.Name",
|
CaptionName = "App.Listform.ListformField.Name",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 2,
|
ListOrderNo = 2,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -199,7 +200,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "OrganizationName",
|
FieldName = "OrganizationName",
|
||||||
CaptionName = "App.Listform.ListformField.OrganizationName",
|
CaptionName = "App.Listform.ListformField.OrganizationName",
|
||||||
Width = 200,
|
Width = 200,
|
||||||
ListOrderNo = 3,
|
ListOrderNo = 3,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -216,7 +217,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "Founder",
|
FieldName = "Founder",
|
||||||
CaptionName = "App.Listform.ListformField.Founder",
|
CaptionName = "App.Listform.ListformField.Founder",
|
||||||
Width = 200,
|
Width = 200,
|
||||||
ListOrderNo = 4,
|
ListOrderNo = 4,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -233,7 +234,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.Int64,
|
SourceDbType = DbType.Int64,
|
||||||
FieldName = "VknTckn",
|
FieldName = "VknTckn",
|
||||||
CaptionName = "App.Listform.ListformField.VknTckn",
|
CaptionName = "App.Listform.ListformField.VknTckn",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 5,
|
ListOrderNo = 5,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -250,7 +251,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "TaxOffice",
|
FieldName = "TaxOffice",
|
||||||
CaptionName = "App.Listform.ListformField.TaxOffice",
|
CaptionName = "App.Listform.ListformField.TaxOffice",
|
||||||
Width = 150,
|
Width = 150,
|
||||||
ListOrderNo = 6,
|
ListOrderNo = 6,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -267,7 +268,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "Country",
|
FieldName = "Country",
|
||||||
CaptionName = "App.Listform.ListformField.Country",
|
CaptionName = "App.Listform.ListformField.Country",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 7,
|
ListOrderNo = 7,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -292,7 +293,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "City",
|
FieldName = "City",
|
||||||
CaptionName = "App.Listform.ListformField.City",
|
CaptionName = "App.Listform.ListformField.City",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 8,
|
ListOrderNo = 8,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -320,7 +321,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "District",
|
FieldName = "District",
|
||||||
CaptionName = "App.Listform.ListformField.District",
|
CaptionName = "App.Listform.ListformField.District",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 9,
|
ListOrderNo = 9,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -348,7 +349,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "Township",
|
FieldName = "Township",
|
||||||
CaptionName = "App.Listform.ListformField.Township",
|
CaptionName = "App.Listform.ListformField.Township",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 10,
|
ListOrderNo = 10,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -375,7 +376,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "Address1",
|
FieldName = "Address1",
|
||||||
CaptionName = "App.Listform.ListformField.Address1",
|
CaptionName = "App.Listform.ListformField.Address1",
|
||||||
Width = 150,
|
Width = 150,
|
||||||
ListOrderNo = 11,
|
ListOrderNo = 11,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -392,7 +393,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "Address2",
|
FieldName = "Address2",
|
||||||
CaptionName = "App.Listform.ListformField.Address2",
|
CaptionName = "App.Listform.ListformField.Address2",
|
||||||
Width = 150,
|
Width = 150,
|
||||||
ListOrderNo = 12,
|
ListOrderNo = 12,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -409,7 +410,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "PostalCode",
|
FieldName = "PostalCode",
|
||||||
CaptionName = "App.Listform.ListformField.PostalCode",
|
CaptionName = "App.Listform.ListformField.PostalCode",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 13,
|
ListOrderNo = 13,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -426,7 +427,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "Email",
|
FieldName = "Email",
|
||||||
CaptionName = "Abp.Account.EmailAddress",
|
CaptionName = "App.Listform.ListformField.Email",
|
||||||
Width = 170,
|
Width = 170,
|
||||||
ListOrderNo = 14,
|
ListOrderNo = 14,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -444,7 +445,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "Website",
|
FieldName = "Website",
|
||||||
CaptionName = "App.Listform.ListformField.Website",
|
CaptionName = "App.Listform.ListformField.Website",
|
||||||
Width = 170,
|
Width = 170,
|
||||||
ListOrderNo = 15,
|
ListOrderNo = 15,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -479,7 +480,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "PhoneNumber",
|
FieldName = "PhoneNumber",
|
||||||
CaptionName = "Abp.Identity.User.UserInformation.PhoneNumber",
|
CaptionName = "Abp.Identity.User.UserInformation.PhoneNumber",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 17,
|
ListOrderNo = 17,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -497,7 +498,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.Int64,
|
SourceDbType = DbType.Int64,
|
||||||
FieldName = "FaxNumber",
|
FieldName = "FaxNumber",
|
||||||
CaptionName = "App.Listform.ListformField.FaxNumber",
|
CaptionName = "App.Listform.ListformField.FaxNumber",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 18,
|
ListOrderNo = 18,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -515,7 +516,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.Boolean,
|
SourceDbType = DbType.Boolean,
|
||||||
FieldName = "IsActive",
|
FieldName = "IsActive",
|
||||||
CaptionName = "App.Listform.ListformField.IsActive",
|
CaptionName = "App.Listform.ListformField.IsActive",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 19,
|
ListOrderNo = 19,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -531,7 +532,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "MenuGroup",
|
FieldName = "MenuGroup",
|
||||||
CaptionName = "App.Listform.ListformField.MenuGroup",
|
CaptionName = "App.Listform.ListformField.MenuGroup",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 20,
|
ListOrderNo = 20,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
|
|
@ -547,6 +548,22 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
PermissionJson = DefaultFieldPermissionJson(TenantManagementPermissions.Tenants.Create, TenantManagementPermissions.Tenants.Default, TenantManagementPermissions.Tenants.Update, true, true, false),
|
PermissionJson = DefaultFieldPermissionJson(TenantManagementPermissions.Tenants.Create, TenantManagementPermissions.Tenants.Default, TenantManagementPermissions.Tenants.Update, true, true, false),
|
||||||
PivotSettingsJson = DefaultPivotSettingsJson
|
PivotSettingsJson = DefaultPivotSettingsJson
|
||||||
},
|
},
|
||||||
|
new ListFormField
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Int32,
|
||||||
|
FieldName = "MaxConcurrentUsers",
|
||||||
|
CaptionName = "App.Listform.ListformField.MaxConcurrentUsers",
|
||||||
|
Width = 100,
|
||||||
|
ListOrderNo = 21,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
IsDeleted = false,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(TenantManagementPermissions.Tenants.Create, TenantManagementPermissions.Tenants.Default, TenantManagementPermissions.Tenants.Update, true, true, false),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson
|
||||||
|
},
|
||||||
], autoSave: true);
|
], autoSave: true);
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
@ -4814,6 +4831,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
new() { Key=1,Name="MailQueueWorker" },
|
new() { Key=1,Name="MailQueueWorker" },
|
||||||
new() { Key=2,Name="SqlWorker" },
|
new() { Key=2,Name="SqlWorker" },
|
||||||
new() { Key=3,Name="NotificationWorker" },
|
new() { Key=3,Name="NotificationWorker" },
|
||||||
|
new() { Key=4,Name="SessionCleanupWorker" },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@ public enum WorkerTypeEnum
|
||||||
MailQueueWorker = 1,
|
MailQueueWorker = 1,
|
||||||
SqlWorker = 2,
|
SqlWorker = 2,
|
||||||
NotificationWorker = 3,
|
NotificationWorker = 3,
|
||||||
|
SessionCleanupWorker = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.BackgroundWorkers;
|
||||||
|
|
||||||
|
public interface ISessionCleanupWorker
|
||||||
|
{
|
||||||
|
Task StartAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
@ -95,6 +95,10 @@ public class PlatformBackgroundWorker : PlatformDomainService, IPlatformBackgrou
|
||||||
{
|
{
|
||||||
await LazyServiceProvider.GetRequiredService<NotificationWorker>().StartAsync(cancellationToken);
|
await LazyServiceProvider.GetRequiredService<NotificationWorker>().StartAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
else if (Worker.WorkerType == WorkerTypeEnum.SessionCleanupWorker)
|
||||||
|
{
|
||||||
|
await LazyServiceProvider.GetRequiredService<ISessionCleanupWorker>().StartAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
// Call AfterSp
|
// Call AfterSp
|
||||||
if (!Worker.AfterSp.IsNullOrWhiteSpace())
|
if (!Worker.AfterSp.IsNullOrWhiteSpace())
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ public class DynamicService : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
{
|
{
|
||||||
CompilationStatus = CompilationStatus.Success;
|
CompilationStatus = CompilationStatus.Success;
|
||||||
LastCompilationError = null;
|
LastCompilationError = null;
|
||||||
LastSuccessfulCompilation = DateTime.UtcNow;
|
LastSuccessfulCompilation = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ public class BlogPost : FullAuditedEntity<Guid>
|
||||||
public void Publish()
|
public void Publish()
|
||||||
{
|
{
|
||||||
IsPublished = true;
|
IsPublished = true;
|
||||||
PublishedAt = DateTime.UtcNow;
|
PublishedAt = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Unpublish()
|
public void Unpublish()
|
||||||
|
|
|
||||||
|
|
@ -684,7 +684,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
CategoryId = category.Id,
|
CategoryId = category.Id,
|
||||||
Author = item.Author,
|
Author = item.Author,
|
||||||
IsPublished = true,
|
IsPublished = true,
|
||||||
PublishedAt = DateTime.UtcNow
|
PublishedAt = DateTime.Now
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ namespace Sozsoft.Platform.DynamicServices
|
||||||
TenantId = tenantId,
|
TenantId = tenantId,
|
||||||
Assembly = assembly,
|
Assembly = assembly,
|
||||||
AssemblyName = assemblyName,
|
AssemblyName = assemblyName,
|
||||||
RequestTime = DateTime.UtcNow
|
RequestTime = DateTime.Now
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +73,7 @@ namespace Sozsoft.Platform.DynamicServices
|
||||||
{
|
{
|
||||||
TenantId = tenantId,
|
TenantId = tenantId,
|
||||||
ServiceName = serviceName,
|
ServiceName = serviceName,
|
||||||
RequestTime = DateTime.UtcNow
|
RequestTime = DateTime.Now
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sozsoft.Platform.BackgroundWorkers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using OpenIddict.Server;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
using Volo.Abp.Identity;
|
||||||
|
using Volo.Abp.MultiTenancy;
|
||||||
|
using Volo.Abp.TenantManagement;
|
||||||
|
using Volo.Abp.Uow;
|
||||||
|
using Sozsoft.Platform.Extensions;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.Identity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public class PlatformSessionCleanupWorker : ISessionCleanupWorker, ITransientDependency
|
||||||
|
{
|
||||||
|
private readonly IIdentitySessionRepository sessionRepo;
|
||||||
|
private readonly ITenantRepository tenantRepository;
|
||||||
|
private readonly ICurrentTenant currentTenant;
|
||||||
|
private readonly ILogger<PlatformSessionCleanupWorker> logger;
|
||||||
|
private readonly IOptions<OpenIddictServerOptions> openIddictOptions;
|
||||||
|
|
||||||
|
public PlatformSessionCleanupWorker(
|
||||||
|
IIdentitySessionRepository sessionRepo,
|
||||||
|
ITenantRepository tenantRepository,
|
||||||
|
ICurrentTenant currentTenant,
|
||||||
|
ILogger<PlatformSessionCleanupWorker> logger,
|
||||||
|
IOptions<OpenIddictServerOptions> openIddictOptions)
|
||||||
|
{
|
||||||
|
this.sessionRepo = sessionRepo;
|
||||||
|
this.tenantRepository = tenantRepository;
|
||||||
|
this.currentTenant = currentTenant;
|
||||||
|
this.logger = logger;
|
||||||
|
this.openIddictOptions = openIddictOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnitOfWork]
|
||||||
|
public virtual async Task StartAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var refreshTokenLifetime = openIddictOptions.Value.RefreshTokenLifetime ?? TimeSpan.FromMinutes(90);
|
||||||
|
var cutoff = DateTime.Now - refreshTokenLifetime - TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activeTenants = (await tenantRepository.GetListAsync(cancellationToken: cancellationToken))
|
||||||
|
.Where(t => t.GetIsActive())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
logger.LogDebug(
|
||||||
|
"IdentitySession temizliği: host + {Count} aktif tenant işlenecek (eşik: {Cutoff:s}).",
|
||||||
|
activeTenants.Count, cutoff);
|
||||||
|
|
||||||
|
var totalDeleted = 0;
|
||||||
|
|
||||||
|
// Host (null) + tüm aktif tenant'lar
|
||||||
|
var scopes = Enumerable.Repeat<Guid?>(null, 1)
|
||||||
|
.Concat(activeTenants.Select(t => (Guid?)t.Id));
|
||||||
|
|
||||||
|
foreach (var tenantId in scopes)
|
||||||
|
{
|
||||||
|
using (currentTenant.Change(tenantId))
|
||||||
|
{
|
||||||
|
var sessions = await sessionRepo.GetListAsync(cancellationToken: cancellationToken);
|
||||||
|
var staleSessions = sessions
|
||||||
|
.Where(s => (s.LastAccessed ?? s.SignedIn) < cutoff)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var session in staleSessions)
|
||||||
|
{
|
||||||
|
await sessionRepo.DeleteAsync(session, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDeleted += staleSessions.Count;
|
||||||
|
|
||||||
|
if (staleSessions.Count > 0)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"IdentitySession temizliği: {Scope} için {Deleted} atıl oturum silindi.",
|
||||||
|
tenantId.HasValue ? $"Tenant [{tenantId}]" : "Host", staleSessions.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalDeleted > 0)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"IdentitySession temizliği tamamlandı: toplam {Deleted} oturum silindi.",
|
||||||
|
totalDeleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "IdentitySession temizliği sırasında hata oluştu.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using OpenIddict.Abstractions;
|
||||||
|
using OpenIddict.Server;
|
||||||
|
using static OpenIddict.Server.OpenIddictServerEvents;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
using Volo.Abp.Identity;
|
||||||
|
using Volo.Abp.MultiTenancy;
|
||||||
|
using Volo.Abp.Security.Claims;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.Identity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /connect/revocation çağrısı başarıyla tamamlandığında ilgili kullanıcının
|
||||||
|
/// AbpSessions tablosundaki tüm satırlarını siler.
|
||||||
|
/// OpenIddict server pipeline'ına eklenir; MVC controller override gerektirmez.
|
||||||
|
/// </summary>
|
||||||
|
public class PlatformSessionRevocationHandler :
|
||||||
|
IOpenIddictServerHandler<HandleRevocationRequestContext>,
|
||||||
|
ITransientDependency
|
||||||
|
{
|
||||||
|
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
|
||||||
|
= OpenIddictServerHandlerDescriptor
|
||||||
|
.CreateBuilder<HandleRevocationRequestContext>()
|
||||||
|
.UseScopedHandler<PlatformSessionRevocationHandler>()
|
||||||
|
.SetOrder(int.MaxValue - 10)
|
||||||
|
.SetType(OpenIddictServerHandlerType.Custom)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
private readonly IIdentitySessionRepository sessionRepo;
|
||||||
|
private readonly ICurrentTenant currentTenant;
|
||||||
|
private readonly ILogger<PlatformSessionRevocationHandler> logger;
|
||||||
|
|
||||||
|
public PlatformSessionRevocationHandler(
|
||||||
|
IIdentitySessionRepository sessionRepo,
|
||||||
|
ICurrentTenant currentTenant,
|
||||||
|
ILogger<PlatformSessionRevocationHandler> logger)
|
||||||
|
{
|
||||||
|
this.sessionRepo = sessionRepo;
|
||||||
|
this.currentTenant = currentTenant;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask HandleAsync(HandleRevocationRequestContext context)
|
||||||
|
{
|
||||||
|
// Hata durumunda (geçersiz token, yetkisiz istek vb.) temizlik yapma.
|
||||||
|
if (context.IsRejected) return;
|
||||||
|
|
||||||
|
var userId = context.Principal?.GetClaim(OpenIddictConstants.Claims.Subject);
|
||||||
|
if (string.IsNullOrEmpty(userId) || !Guid.TryParse(userId, out var userGuid)) return;
|
||||||
|
|
||||||
|
// Token'dan tenant ID'yi al.
|
||||||
|
var tenantIdStr = context.Principal?.GetClaim(AbpClaimTypes.TenantId);
|
||||||
|
Guid? tenantId = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(tenantIdStr) && Guid.TryParse(tenantIdStr, out var parsedTenantId))
|
||||||
|
{
|
||||||
|
tenantId = parsedTenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Sadece token'ın ait olduğu tenant'taki oturumları sil.
|
||||||
|
using (currentTenant.Change(tenantId))
|
||||||
|
{
|
||||||
|
var sessions = await sessionRepo.GetListAsync(userId: userGuid);
|
||||||
|
foreach (var session in sessions)
|
||||||
|
{
|
||||||
|
await sessionRepo.DeleteAsync(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessions.Count > 0)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Token revocation: {Count} IdentitySession silindi. UserId: {UserId}, TenantId: {TenantId}",
|
||||||
|
sessions.Count, userGuid, tenantId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Session temizleme hatası revocation akışını bloklamamalı.
|
||||||
|
logger.LogWarning(ex,
|
||||||
|
"Token revocation sonrası IdentitySession temizliğinde hata. UserId: {UserId}", userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,18 +13,19 @@ using Microsoft.Extensions.Options;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
using Volo.Abp.Identity;
|
using Volo.Abp.Identity;
|
||||||
using Volo.Abp.Identity.AspNetCore;
|
using Volo.Abp.Identity.AspNetCore;
|
||||||
|
using Volo.Abp.MultiTenancy;
|
||||||
using Volo.Abp.Settings;
|
using Volo.Abp.Settings;
|
||||||
using Volo.Abp.TenantManagement;
|
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;
|
||||||
|
|
||||||
public interface IPlatformSignInManager
|
public interface IPlatformSignInManager
|
||||||
{
|
{
|
||||||
Task<bool> PreSignInCheckAsync(IdentityUser user);
|
Task<bool> PreSignInCheckAsync(IdentityUser user);
|
||||||
|
Task<bool> CheckConcurrentLimitAsync(IdentityUser user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
||||||
|
|
@ -35,6 +36,7 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
||||||
private readonly ITenantRepository tenantRepository;
|
private readonly ITenantRepository tenantRepository;
|
||||||
private readonly IdentityUserManager userManager;
|
private readonly IdentityUserManager userManager;
|
||||||
private readonly IIdentitySessionRepository identitySessionRepository;
|
private readonly IIdentitySessionRepository identitySessionRepository;
|
||||||
|
private readonly ICurrentTenant currentTenant;
|
||||||
|
|
||||||
public PlatformSignInManager(
|
public PlatformSignInManager(
|
||||||
IdentityUserManager userManager,
|
IdentityUserManager userManager,
|
||||||
|
|
@ -50,7 +52,8 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
||||||
IRepository<IpRestriction, Guid> repositoryIp,
|
IRepository<IpRestriction, Guid> repositoryIp,
|
||||||
IRepository<WorkHour, Guid> repositoryWorkHour,
|
IRepository<WorkHour, Guid> repositoryWorkHour,
|
||||||
ITenantRepository tenantRepository,
|
ITenantRepository tenantRepository,
|
||||||
IIdentitySessionRepository identitySessionRepository
|
IIdentitySessionRepository identitySessionRepository,
|
||||||
|
ICurrentTenant currentTenant
|
||||||
) : base(
|
) : base(
|
||||||
userManager,
|
userManager,
|
||||||
contextAccessor,
|
contextAccessor,
|
||||||
|
|
@ -68,6 +71,7 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
||||||
this.tenantRepository = tenantRepository;
|
this.tenantRepository = tenantRepository;
|
||||||
this.userManager = userManager;
|
this.userManager = userManager;
|
||||||
this.identitySessionRepository = identitySessionRepository;
|
this.identitySessionRepository = identitySessionRepository;
|
||||||
|
this.currentTenant = currentTenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> PreSignInCheckAsync(IdentityUser user)
|
public async Task<bool> PreSignInCheckAsync(IdentityUser user)
|
||||||
|
|
@ -102,7 +106,7 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
||||||
{
|
{
|
||||||
return new PlatformSignInResult() { IsNotAllowed_WorkHour = true };
|
return new PlatformSignInResult() { IsNotAllowed_WorkHour = true };
|
||||||
}
|
}
|
||||||
if (!await CanSignInConcurrentLimitAsync(user))
|
if (!await CheckConcurrentLimitAsync(user))
|
||||||
{
|
{
|
||||||
return new PlatformSignInResult() { IsNotAllowed_ConcurrentUserLimit = true };
|
return new PlatformSignInResult() { IsNotAllowed_ConcurrentUserLimit = true };
|
||||||
}
|
}
|
||||||
|
|
@ -265,11 +269,11 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prevents login when the tenant's concurrent user limit is reached.
|
/// Tenant'a tanımlı MaxConcurrentUsers limitini aşmamak için login'i engeller.
|
||||||
/// Counts distinct active users (by UserId) in AbpSessions for the tenant.
|
/// AbpSessions tablosundaki aktif oturumları sayar (farklı UserId'lere göre distinct).
|
||||||
/// A user who already has an active session is not counted as a new concurrent user.
|
/// Kullanıcının kendisinin zaten oturumu varsa (refresh senaryosu) yeni concurrent user sayılmaz.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<bool> CanSignInConcurrentLimitAsync(IdentityUser user)
|
public async Task<bool> CheckConcurrentLimitAsync(IdentityUser user)
|
||||||
{
|
{
|
||||||
if (!user.TenantId.HasValue)
|
if (!user.TenantId.HasValue)
|
||||||
{
|
{
|
||||||
|
|
@ -283,24 +287,32 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxConcurrentUsers = tenant.GetMaxConcurrentUsers();
|
var maxConcurrentUsers = tenant.GetMaxConcurrentUsers();
|
||||||
if (!maxConcurrentUsers.HasValue || maxConcurrentUsers.Value <= 0)
|
if (!maxConcurrentUsers.HasValue || maxConcurrentUsers.Value == 0)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessions = await identitySessionRepository.GetListAsync();
|
// Tenant bağlamını explicit olarak set et — hem password hem refresh token akışlarında
|
||||||
var activeUserCount = sessions
|
// doğru tenant izolasyonu için güvenli bir şekilde değiştirilir.
|
||||||
.Where(s => s.UserId != user.Id)
|
using (currentTenant.Change(user.TenantId))
|
||||||
.Select(s => s.UserId)
|
|
||||||
.Distinct()
|
|
||||||
.Count();
|
|
||||||
|
|
||||||
if (activeUserCount >= maxConcurrentUsers.Value)
|
|
||||||
{
|
{
|
||||||
Logger.LogWarning(PlatformEventIds.UserCannotSignInConcurrentUserLimit,
|
var sessions = await identitySessionRepository.GetListAsync();
|
||||||
"Tenant {TenantId} concurrent user limit of {Limit} reached. Active users: {ActiveCount}.",
|
|
||||||
user.TenantId, maxConcurrentUsers.Value, activeUserCount);
|
// Bu kullanıcı hariç diğer distinct aktif kullanıcı sayısını hesapla.
|
||||||
return false;
|
// Kullanıcının kendi oturumu varsa (refresh) o kişi tekrar sayılmaz.
|
||||||
|
var otherActiveUserCount = sessions
|
||||||
|
.Where(s => s.UserId != user.Id)
|
||||||
|
.Select(s => s.UserId)
|
||||||
|
.Distinct()
|
||||||
|
.Count();
|
||||||
|
|
||||||
|
if (otherActiveUserCount >= maxConcurrentUsers.Value)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(PlatformEventIds.UserCannotSignInConcurrentUserLimit,
|
||||||
|
"Tenant {TenantId} concurrent user limit of {Limit} reached. Active users: {ActiveCount}.",
|
||||||
|
user.TenantId, maxConcurrentUsers.Value, otherActiveUserCount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,25 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Sozsoft.Platform.Extensions;
|
using Sozsoft.Platform.Extensions;
|
||||||
using Sozsoft.Platform.Localization;
|
using Sozsoft.Platform.Localization;
|
||||||
using Sozsoft.Sender.Mail;
|
using Sozsoft.Sender.Mail;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using OpenIddict.Abstractions;
|
using OpenIddict.Abstractions;
|
||||||
|
using OpenIddict.Server.AspNetCore;
|
||||||
|
using Volo.Abp;
|
||||||
using Volo.Abp.AspNetCore.Mvc;
|
using Volo.Abp.AspNetCore.Mvc;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
using Volo.Abp.Identity;
|
using Volo.Abp.Identity;
|
||||||
using Volo.Abp.OpenIddict;
|
using Volo.Abp.OpenIddict;
|
||||||
using Volo.Abp.OpenIddict.Controllers;
|
using Volo.Abp.OpenIddict.Controllers;
|
||||||
|
using Volo.Abp.Security.Claims;
|
||||||
using Volo.Abp.Settings;
|
using Volo.Abp.Settings;
|
||||||
using Volo.Abp.Uow;
|
using Volo.Abp.Uow;
|
||||||
using Volo.Abp.Validation;
|
using Volo.Abp.Validation;
|
||||||
|
|
@ -31,17 +36,20 @@ public class PlatformTokenController : TokenController
|
||||||
private readonly ISozsoftEmailSender emailSender;
|
private readonly ISozsoftEmailSender emailSender;
|
||||||
private readonly ICaptchaManager captchaManager;
|
private readonly ICaptchaManager captchaManager;
|
||||||
private readonly IPlatformSignInManager platformSignInManager;
|
private readonly IPlatformSignInManager platformSignInManager;
|
||||||
|
private readonly IIdentitySessionRepository identitySessionRepository;
|
||||||
private IStringLocalizer LP;
|
private IStringLocalizer LP;
|
||||||
|
|
||||||
public PlatformTokenController(
|
public PlatformTokenController(
|
||||||
ISozsoftEmailSender emailSender,
|
ISozsoftEmailSender emailSender,
|
||||||
ICaptchaManager captchaManager,
|
ICaptchaManager captchaManager,
|
||||||
IPlatformSignInManager platformSignInManager,
|
IPlatformSignInManager platformSignInManager,
|
||||||
|
IIdentitySessionRepository identitySessionRepository,
|
||||||
IStringLocalizer<PlatformResource> LP) : base()
|
IStringLocalizer<PlatformResource> LP) : base()
|
||||||
{
|
{
|
||||||
this.emailSender = emailSender;
|
this.emailSender = emailSender;
|
||||||
this.captchaManager = captchaManager;
|
this.captchaManager = captchaManager;
|
||||||
this.platformSignInManager = platformSignInManager;
|
this.platformSignInManager = platformSignInManager;
|
||||||
|
this.identitySessionRepository = identitySessionRepository;
|
||||||
this.LP = LP;
|
this.LP = LP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,6 +232,117 @@ Your login code: {twoFactorToken}";
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task<IActionResult> SetSuccessResultAsync(OpenIddictRequest request, IdentityUser user)
|
||||||
|
{
|
||||||
|
var result = await base.SetSuccessResultAsync(request, user);
|
||||||
|
await ManageIdentitySessionAsync(user, request);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ManageIdentitySessionAsync(IdentityUser user, OpenIddictRequest request)
|
||||||
|
{
|
||||||
|
// Refresh grant session yönetimi HandleRefreshTokenAsync'da yapılır.
|
||||||
|
if (request.IsRefreshTokenGrantType()) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (CurrentTenant.Change(user.TenantId))
|
||||||
|
{
|
||||||
|
var existingSessions = await identitySessionRepository.GetListAsync(userId: user.Id);
|
||||||
|
if (existingSessions.Any())
|
||||||
|
{
|
||||||
|
foreach (var session in existingSessions)
|
||||||
|
await identitySessionRepository.DeleteAsync(session);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!await platformSignInManager.CheckConcurrentLimitAsync(user))
|
||||||
|
throw new UserFriendlyException(PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_ConcurrentUserLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
await InsertSessionAsync(user, request.ClientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (UserFriendlyException) { throw; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "IdentitySession yönetiminde hata oluştu. UserId: {UserId}", user.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnitOfWork]
|
||||||
|
protected override async Task<IActionResult> HandleRefreshTokenAsync(OpenIddictRequest request)
|
||||||
|
{
|
||||||
|
var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
|
var tenantId = ParseTenantId(info.Principal?.Claims.FirstOrDefault(c => c.Type == AbpClaimTypes.TenantId)?.Value);
|
||||||
|
|
||||||
|
IdentityUser refreshUser = null;
|
||||||
|
var userIdStr = info.Principal?.GetClaim(OpenIddictConstants.Claims.Subject);
|
||||||
|
if (!userIdStr.IsNullOrWhiteSpace() && Guid.TryParse(userIdStr, out var userId))
|
||||||
|
{
|
||||||
|
using (CurrentTenant.Change(tenantId))
|
||||||
|
{
|
||||||
|
refreshUser = await UserManager.FindByIdAsync(userId.ToString());
|
||||||
|
if (refreshUser != null)
|
||||||
|
{
|
||||||
|
if (!await platformSignInManager.CheckConcurrentLimitAsync(refreshUser))
|
||||||
|
throw new UserFriendlyException(PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_ConcurrentUserLimit);
|
||||||
|
|
||||||
|
var existingSessions = await identitySessionRepository.GetListAsync(userId: userId);
|
||||||
|
foreach (var session in existingSessions)
|
||||||
|
await identitySessionRepository.DeleteAsync(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IActionResult result;
|
||||||
|
using (CurrentTenant.Change(tenantId))
|
||||||
|
{
|
||||||
|
result = await base.HandleRefreshTokenAsync(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result is Microsoft.AspNetCore.Mvc.SignInResult && refreshUser != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (CurrentTenant.Change(refreshUser.TenantId))
|
||||||
|
{
|
||||||
|
await InsertSessionAsync(refreshUser, request.ClientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Refresh token sonrası IdentitySession oluşturulamadı. UserId: {UserId}", refreshUser.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InsertSessionAsync(IdentityUser user, string clientId)
|
||||||
|
{
|
||||||
|
var userAgent = HttpContext?.Request?.Headers["User-Agent"].ToString();
|
||||||
|
var deviceInfo = userAgent?.Length > 64 ? userAgent[..64] : userAgent;
|
||||||
|
|
||||||
|
var session = new IdentitySession(
|
||||||
|
GuidGenerator.Create(),
|
||||||
|
Guid.NewGuid().ToString("N"),
|
||||||
|
"Web",
|
||||||
|
deviceInfo,
|
||||||
|
user.Id,
|
||||||
|
user.TenantId,
|
||||||
|
clientId ?? "platform",
|
||||||
|
HttpContext?.Connection?.RemoteIpAddress?.ToString(),
|
||||||
|
Clock.Now
|
||||||
|
);
|
||||||
|
|
||||||
|
await identitySessionRepository.InsertAsync(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Guid? ParseTenantId(string value) =>
|
||||||
|
!string.IsNullOrWhiteSpace(value) && Guid.TryParse(value, out var id) ? id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue