sozsoft-platform/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSignInManager.cs
2026-07-08 14:53:23 +03:00

389 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Extensions;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Identity;
using Volo.Abp.Identity.AspNetCore;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
using Volo.Abp.TenantManagement;
using Volo.Abp.Timing;
using Volo.Abp.Uow;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
namespace Sozsoft.Platform.Identity;
public interface IPlatformSignInManager
{
Task<bool> PreSignInCheckAsync(IdentityUser user);
Task<bool> CheckConcurrentLimitAsync(IdentityUser user);
}
public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
{
private const string UserLicenseProductName = "Public.products.userLicense";
private const string BranchLicenseProductName = "Public.products.branchHosting";
private readonly IClock clock;
private readonly IRepository<IpRestriction, Guid> repositoryIp;
private readonly IRepository<WorkHour, Guid> repositoryWorkHour;
private readonly ITenantRepository tenantRepository;
private readonly IdentityUserManager userManager;
private readonly IIdentitySessionRepository identitySessionRepository;
private readonly ICurrentTenant currentTenant;
private readonly IUnitOfWorkManager unitOfWorkManager;
private readonly IRepository<Order, Guid> orderRepository;
private readonly IRepository<Branch, Guid> branchRepository;
public PlatformSignInManager(
IdentityUserManager userManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<IdentityUser> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<IdentityUser>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<IdentityUser> confirmation,
IOptions<AbpIdentityOptions> options,
ISettingProvider settingProvider,
IClock clock,
IRepository<IpRestriction, Guid> repositoryIp,
IRepository<WorkHour, Guid> repositoryWorkHour,
ITenantRepository tenantRepository,
IIdentitySessionRepository identitySessionRepository,
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
IRepository<Order, Guid> orderRepository,
IRepository<Branch, Guid> branchRepository
) : base(
userManager,
contextAccessor,
claimsFactory,
optionsAccessor,
logger,
schemes,
confirmation,
options,
settingProvider)
{
this.clock = clock;
this.repositoryIp = repositoryIp;
this.repositoryWorkHour = repositoryWorkHour;
this.tenantRepository = tenantRepository;
this.userManager = userManager;
this.identitySessionRepository = identitySessionRepository;
this.currentTenant = currentTenant;
this.unitOfWorkManager = unitOfWorkManager;
this.orderRepository = orderRepository;
this.branchRepository = branchRepository;
}
public async Task<bool> PreSignInCheckAsync(IdentityUser user)
{
var result = await PreSignInCheck(user);
return result?.Succeeded ?? true;
}
protected override async Task<SignInResult> PreSignInCheck(IdentityUser user)
{
var result = await base.PreSignInCheck(user);
if (result == null)
{
if (!await CanSignInVerifiedAsync(user))
{
return new PlatformSignInResult() { IsNotAllowed_NotVerified = true };
}
if (!CanSignInEndDate(user))
{
return new PlatformSignInResult() { IsNotAllowed_LoginEndDateDue = true };
}
if (!await CanSignInIpAsync(user))
{
return new PlatformSignInResult() { IsNotAllowed_NotAllowedIp = true };
}
if (!await CanSignInTenantActiveAsync(user))
{
return new PlatformSignInResult() { IsNotAllowed_TenantIsPassive = true };
}
if (!await CanSignInWorkHourAsync(user))
{
return new PlatformSignInResult() { IsNotAllowed_WorkHour = true };
}
if (!await CheckBranchLicenseLimitAsync(user))
{
return new PlatformSignInResult() { IsNotAllowed_BranchLimit = true };
}
if (!await CheckConcurrentLimitAsync(user))
{
return new PlatformSignInResult() { IsNotAllowed_ConcurrentUserLimit = true };
}
}
else
{
// Bu kontrol modules\identity\src\Volo.Abp.Identity.AspNetCore\Volo\Abp\Identity\AspNetCore\AbpSignInManager.cs
// içinde zaten yapılıyor fakat her senaryoya NotAllowed diyor, bizim ayrıt etmemiz gerektiği için tekrar yapıyoruz.
if (user.ShouldChangePasswordOnNextLogin)
{
return new PlatformSignInResult() { ShouldChangePasswordOnNextLogin = true };
}
if (await userManager.ShouldPeriodicallyChangePasswordAsync(user))
{
return new PlatformSignInResult() { ShouldChangePasswordPeriodic = true };
}
}
return result;
}
/// <summary>
/// Used to prevent login outside of defined work hours.
/// </summary>
private async Task<bool> CanSignInWorkHourAsync(IdentityUser user)
{
var workHourName = user.GetWorkHour();
if (string.IsNullOrWhiteSpace(workHourName))
{
return true;
}
var workHours = await repositoryWorkHour.GetListAsync(a => a.Name == workHourName);
if (workHours.IsNullOrEmpty())
{
return true;
}
var now = clock.Now;
var currentTime = now.TimeOfDay;
var dayOfWeek = now.DayOfWeek;
var isAllowed = workHours.Any(wh =>
{
var dayMatches = dayOfWeek switch
{
DayOfWeek.Monday => wh.Monday == true,
DayOfWeek.Tuesday => wh.Tuesday == true,
DayOfWeek.Wednesday => wh.Wednesday == true,
DayOfWeek.Thursday => wh.Thursday == true,
DayOfWeek.Friday => wh.Friday == true,
DayOfWeek.Saturday => wh.Saturday == true,
DayOfWeek.Sunday => wh.Sunday == true,
_ => false
};
if (!dayMatches) return false;
return currentTime >= wh.StartTime.TimeOfDay && currentTime <= wh.EndTime.TimeOfDay;
});
if (!isAllowed)
{
Logger.LogWarning(PlatformEventIds.UserCannotSignInWorkHour, "User cannot sign in outside work hours.");
return false;
}
return true;
}
/// <summary>
/// Tenant IsActive
/// </summary>
private async Task<bool> CanSignInTenantActiveAsync(IdentityUser user)
{
if (!user.TenantId.HasValue)
{
return true;
}
var tenant = await tenantRepository.FindAsync(user.TenantId.Value);
if (tenant == null)
{
return false;
}
if (!tenant.GetIsActive())
{
Logger.LogWarning(PlatformEventIds.UserCannotSignInTenantIsPassive, "Tenant is inactive.");
return false;
}
return true;
}
/// <summary>
/// Used to prevent login for additional scenarios;
/// Account Not Verified
/// </summary>
private async Task<bool> CanSignInVerifiedAsync(IdentityUser user)
{
var rva = await SettingProvider.GetAsync<bool>(PlatformConsts.AbpIdentity.Profile.General.RequireVerifiedAccount);
if (rva && !user.GetIsVerified())
{
Logger.LogWarning(PlatformEventIds.UserCannotSignInWithoutVerifiedAccount, "User cannot sign in without a verified account.");
return false;
}
return true;
}
/// <summary>
/// Used to prevent login if end date is due
/// </summary>
private bool CanSignInEndDate(IdentityUser user)
{
var endDate = user.GetLoginEndDate();
if (endDate != null && clock.Now > endDate)
{
//izin yok
Logger.LogWarning(PlatformEventIds.UserCannotSignInLoginEndDateDue, "User cannot sign in, login end date is due.");
return false;
}
//izin ver
return true;
}
/// <summary>
/// Used to prevent login for allowed IPs;
/// IP Not Allowed
/// </summary>
private async Task<bool> CanSignInIpAsync(IdentityUser user)
{
// User, Role, Global Restrictions
var roles = await UserManager.GetRolesAsync(user);
//var roleIds = roleManager.Roles.Where(a => roles.Contains(a.Name)).Select(a => a.Id).ToArray();
var restrictions = await repositoryIp.GetListAsync(a =>
(a.ResourceType == "User" && a.ResourceId == user.UserName) ||
(a.ResourceType == "Role" && roles.Contains(a.ResourceId)) ||
(a.ResourceType == "Global"));
if (!restrictions.IsNullOrEmpty())
{
var ip = Context.GetRemoteIPAddress();
var ipString = ip?.ToString();
if (ip == null || (ip != null && !restrictions.Select(a => a.IP).Any(a => a == ipString)))
{
//izin yok
Logger.LogWarning(PlatformEventIds.UserCannotSignInWithoutAllowedIp, "User cannot sign in without an allowed IP.");
return false;
}
}
//izin ver
return true;
}
/// <summary>
/// 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.
/// </summary>
public async Task<bool> CheckConcurrentLimitAsync(IdentityUser user)
{
// Eğer tenantId yoksa, kullanıcı tenant'a bağlı değil demektir.
// Bu durumda concurrent limit kontrolü yapılmaz.
if (!user.TenantId.HasValue)
{
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
// doğru tenant izolasyonu için güvenli bir şekilde değiştirilir.
using (currentTenant.Change(user.TenantId))
{
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
var sessions = await identitySessionRepository.GetListAsync();
// Bu kullanıcı hariç diğer distinct aktif kullanıcı sayısını hesapla.
// 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)
{
Logger.LogWarning(PlatformEventIds.UserCannotSignInConcurrentUserLimit,
"Tenant {TenantId} concurrent user limit of {Limit} reached. Active users: {ActiveCount}.",
user.TenantId, maxConcurrentUsers, otherActiveUserCount);
return false;
}
await uow.CompleteAsync();
}
return true;
}
private async Task<bool> CheckBranchLicenseLimitAsync(IdentityUser user)
{
if (!user.TenantId.HasValue)
{
return true;
}
var tenantId = user.TenantId.Value;
var activeBranchCount = await GetActiveBranchCountAsync(tenantId);
var branchLicenseQuantity = await GetActiveBranchLicenseQuantityAsync(tenantId);
if (activeBranchCount > branchLicenseQuantity)
{
Logger.LogWarning(PlatformEventIds.UserCannotSignInConcurrentUserLimit,
"Tenant {TenantId} branch license quantity of {Limit} is lower than active branch count {ActiveCount}.",
tenantId, branchLicenseQuantity, activeBranchCount);
return false;
}
return true;
}
private async Task<int> GetActiveBranchCountAsync(Guid tenantId)
{
using (currentTenant.Change(tenantId))
{
return await branchRepository.CountAsync(branch => branch.TenantId == tenantId && branch.IsActive == true);
}
}
private async Task<int> GetActiveUserLicenseQuantityAsync(Guid tenantId)
{
return await GetActiveOrderItemQuantityAsync(tenantId, UserLicenseProductName);
}
private async Task<int> GetActiveBranchLicenseQuantityAsync(Guid tenantId)
{
return await GetActiveOrderItemQuantityAsync(tenantId, BranchLicenseProductName);
}
private async Task<int> 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();
return activeOrders
.SelectMany(order => order.Items)
.Where(item => item.ProductName == productName)
.Sum(item => item.Quantity);
}
}
}