From 37c0c5ad45592ef7dbae7ebc7865b178bac75f01 Mon Sep 17 00:00:00 2001 From: Nima Haji Date: Fri, 22 May 2026 16:28:37 +0330 Subject: [PATCH] Get tenantId from jwt --- Application/ApplicationServices.cs | 2 + .../Interfaces/TenantContextContract.cs | 6 + .../Interfaces/TenantRepositoryContract.cs | 2 + .../Tenant/Services/TenantProvider.cs | 10 + .../Features/Tenant/Services/TenantService.cs | 5 +- Domain/Entities/Appointment.cs | 3 +- Domain/Entities/Refreshtoken.cs | 6 +- Domain/Entities/Service.cs | 4 +- Domain/Entities/User.cs | 3 +- Domain/Interfaces/TenantEntityContract.cs | 6 + Domain/Interfaces/TenantProviderContract.cs | 7 + ...48_TenantIdAddedToRefreshToken.Designer.cs | 321 ++++++++++++++++++ ...60522112348_TenantIdAddedToRefreshToken.cs | 30 ++ .../RefreshTokenConfiguration.cs | 4 + .../Configurations/TenantConfiguration.cs | 1 + .../Migrations/AppDbContextModelSnapshot.cs | 3 + .../Repositories/TenantRepository.cs | 27 +- api/Controllers/TenantController.cs | 18 +- api/Middlewares/TenantResolutionMiddleware.cs | 65 ++++ api/Program.cs | 2 + 20 files changed, 515 insertions(+), 10 deletions(-) create mode 100644 Application/Features/Tenant/Interfaces/TenantContextContract.cs create mode 100644 Application/Features/Tenant/Services/TenantProvider.cs create mode 100644 Domain/Interfaces/TenantEntityContract.cs create mode 100644 Domain/Interfaces/TenantProviderContract.cs create mode 100644 Infrastructure/Migrations/20260522112348_TenantIdAddedToRefreshToken.Designer.cs create mode 100644 Infrastructure/Migrations/20260522112348_TenantIdAddedToRefreshToken.cs create mode 100644 api/Middlewares/TenantResolutionMiddleware.cs diff --git a/Application/ApplicationServices.cs b/Application/ApplicationServices.cs index 7202ade..b1fc3b0 100644 --- a/Application/ApplicationServices.cs +++ b/Application/ApplicationServices.cs @@ -4,6 +4,7 @@ using Application.Features.Auth.Services; using Application.Features.Service.Services; using Application.Features.Tenant.Services; +using Domain.Interfaces; using Microsoft.Extensions.DependencyInjection; namespace Application; @@ -16,6 +17,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } } \ No newline at end of file diff --git a/Application/Features/Tenant/Interfaces/TenantContextContract.cs b/Application/Features/Tenant/Interfaces/TenantContextContract.cs new file mode 100644 index 0000000..8bf6a5c --- /dev/null +++ b/Application/Features/Tenant/Interfaces/TenantContextContract.cs @@ -0,0 +1,6 @@ +namespace Application.Features.Tenant.Interfaces; + +public interface TenantContextContract +{ + +} \ No newline at end of file diff --git a/Application/Features/Tenant/Interfaces/TenantRepositoryContract.cs b/Application/Features/Tenant/Interfaces/TenantRepositoryContract.cs index 66d8b4c..9ceac05 100644 --- a/Application/Features/Tenant/Interfaces/TenantRepositoryContract.cs +++ b/Application/Features/Tenant/Interfaces/TenantRepositoryContract.cs @@ -5,5 +5,7 @@ namespace Application.Features.Tenant.Interfaces; public interface TenantRepositoryContract { Task RegisterTenant(Domain.Entities.Tenant tenant); + Task GetCurrentTenantAsync(); + Task IsSubscriptionValidAsync(); Task SaveAsync(); } \ No newline at end of file diff --git a/Application/Features/Tenant/Services/TenantProvider.cs b/Application/Features/Tenant/Services/TenantProvider.cs new file mode 100644 index 0000000..89befb0 --- /dev/null +++ b/Application/Features/Tenant/Services/TenantProvider.cs @@ -0,0 +1,10 @@ +using Domain.Interfaces; + +namespace Application.Features.Tenant.Services; + +public class TenantProvider : TenantProviderContract +{ + private Guid _tenantId; + public Guid GetTenantId() => _tenantId; + public void SetTenantId(Guid tenantId) => _tenantId = tenantId; +} \ No newline at end of file diff --git a/Application/Features/Tenant/Services/TenantService.cs b/Application/Features/Tenant/Services/TenantService.cs index b7aa15c..c3bd366 100644 --- a/Application/Features/Tenant/Services/TenantService.cs +++ b/Application/Features/Tenant/Services/TenantService.cs @@ -29,7 +29,7 @@ public async Task Register(RegisterTenantDTO dto) // Todo : Date manager // Todo : if for duplicate slug var tenant = new Domain.Entities.Tenant(dto.Name,dto.Slug,dto.SubscriptionExpireDate); - + var userId=_userContext.UserId; var user = await _userRepository.GetUserByIdAsync(userId); @@ -40,7 +40,8 @@ public async Task Register(RegisterTenantDTO dto) user.ChangeRoleTo(UserRole.Admin); user.AssignTenantToUser(tenant.Id); await _tenantRepository.SaveAsync(); - + + var tenantId = await _tenantRepository.GetCurrentTenantAsync(); return $"{tenant.Name} Registered belong to user {user.FullName} created"; } } \ No newline at end of file diff --git a/Domain/Entities/Appointment.cs b/Domain/Entities/Appointment.cs index e673f41..b1103c7 100644 --- a/Domain/Entities/Appointment.cs +++ b/Domain/Entities/Appointment.cs @@ -1,8 +1,9 @@ using Domain.Enums; +using Domain.Interfaces; namespace Domain.Entities { - public class Appointment + public class Appointment:TenantEntityContract { public Guid AppointmentId { get; private set; } public Guid UserId { get; private set; } diff --git a/Domain/Entities/Refreshtoken.cs b/Domain/Entities/Refreshtoken.cs index d198a14..ac28a98 100644 --- a/Domain/Entities/Refreshtoken.cs +++ b/Domain/Entities/Refreshtoken.cs @@ -1,13 +1,15 @@ +using Domain.Interfaces; + namespace Domain.Entities; -public class RefreshToken +public class RefreshToken:TenantEntityContract { public Guid Id { get; set; } public string Token { get; set; } = null!; public DateTime ExpiresAt { get; set; } public bool IsRevoked { get; set; } - + public Guid TenantId { get; set; } public Guid UserId { get; set; } public User User { get; set; } = null!; } diff --git a/Domain/Entities/Service.cs b/Domain/Entities/Service.cs index 33612b3..cd458fc 100644 --- a/Domain/Entities/Service.cs +++ b/Domain/Entities/Service.cs @@ -1,6 +1,8 @@ +using Domain.Interfaces; + namespace Domain.Entities; -public class Service +public class Service:TenantEntityContract { public Guid Id { get; private set; } public string Title { get; private set; } diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs index 5827fac..4497755 100644 --- a/Domain/Entities/User.cs +++ b/Domain/Entities/User.cs @@ -1,8 +1,9 @@ using Domain.Enums; +using Domain.Interfaces; namespace Domain.Entities; -public class User +public class User:TenantEntityContract { public Guid Id { get; private set; } public string FullName { get; private set; } diff --git a/Domain/Interfaces/TenantEntityContract.cs b/Domain/Interfaces/TenantEntityContract.cs new file mode 100644 index 0000000..1148a7f --- /dev/null +++ b/Domain/Interfaces/TenantEntityContract.cs @@ -0,0 +1,6 @@ +namespace Domain.Interfaces; + +public class TenantEntityContract +{ + public Guid TenantId { get; set; } +} \ No newline at end of file diff --git a/Domain/Interfaces/TenantProviderContract.cs b/Domain/Interfaces/TenantProviderContract.cs new file mode 100644 index 0000000..001801e --- /dev/null +++ b/Domain/Interfaces/TenantProviderContract.cs @@ -0,0 +1,7 @@ +namespace Domain.Interfaces; + +public interface TenantProviderContract +{ + Guid GetTenantId(); + void SetTenantId(Guid tenantId); +} \ No newline at end of file diff --git a/Infrastructure/Migrations/20260522112348_TenantIdAddedToRefreshToken.Designer.cs b/Infrastructure/Migrations/20260522112348_TenantIdAddedToRefreshToken.Designer.cs new file mode 100644 index 0000000..80801d6 --- /dev/null +++ b/Infrastructure/Migrations/20260522112348_TenantIdAddedToRefreshToken.Designer.cs @@ -0,0 +1,321 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522112348_TenantIdAddedToRefreshToken")] + partial class TenantIdAddedToRefreshToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Appointment", b => + { + b.Property("AppointmentId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("AppointmentId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UserId"); + + b.ToTable("Appointments", t => + { + t.HasCheckConstraint("CK_Status_Valid_Values", "[Status] IN (0, 1,2)"); + }); + }); + + modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b => + { + b.Property("AppointmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("ServiceId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("AppointmentId", "ServiceId"); + + b.HasIndex("ServiceId"); + + b.ToTable("AppointmentServiceLinks", (string)null); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsRevoked") + .HasColumnType("bit"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Service", (string)null); + }); + + modelBuilder.Entity("Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateJoined") + .HasColumnType("datetime2"); + + b.Property("ExpireSubscriptionDate") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)"); + + b.Property("SubscriptionStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Tenants", (string)null); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PasswordResetAttemptsCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("PasswordResetCodeExpireAt") + .HasColumnType("datetime2"); + + b.Property("PasswordResetCodeHash") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(11) + .HasColumnType("nvarchar(11)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Users", null, t => + { + t.HasCheckConstraint("CK_Role_Valid_Values", "[Role] IN (0, 1)"); + }); + }); + + modelBuilder.Entity("Domain.Entities.Appointment", b => + { + b.HasOne("Domain.Entities.Tenant", "Tenant") + .WithMany("Appointments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany("Appointments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b => + { + b.HasOne("Domain.Entities.Appointment", "Appointment") + .WithMany("AppointmentServices") + .HasForeignKey("AppointmentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Domain.Entities.Service", "Service") + .WithMany("AppointmentServices") + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Appointment"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Service", b => + { + b.HasOne("Domain.Entities.Tenant", "Tenant") + .WithMany("Services") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Domain.Entities.Appointment", b => + { + b.Navigation("AppointmentServices"); + }); + + modelBuilder.Entity("Domain.Entities.Service", b => + { + b.Navigation("AppointmentServices"); + }); + + modelBuilder.Entity("Domain.Entities.Tenant", b => + { + b.Navigation("Appointments"); + + b.Navigation("Services"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Navigation("Appointments"); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Migrations/20260522112348_TenantIdAddedToRefreshToken.cs b/Infrastructure/Migrations/20260522112348_TenantIdAddedToRefreshToken.cs new file mode 100644 index 0000000..52138fc --- /dev/null +++ b/Infrastructure/Migrations/20260522112348_TenantIdAddedToRefreshToken.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class TenantIdAddedToRefreshToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TenantId", + table: "RefreshTokens", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "TenantId", + table: "RefreshTokens"); + } + } +} diff --git a/Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs b/Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs index 632b924..57e75e6 100644 --- a/Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs +++ b/Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs @@ -21,5 +21,9 @@ public void Configure(EntityTypeBuilder builder) .WithMany(u => u.RefreshTokens) .HasForeignKey(x => x.UserId) .OnDelete(DeleteBehavior.Cascade); + + builder + .Property(rf => rf.TenantId) + .IsRequired(); } } \ No newline at end of file diff --git a/Infrastructure/Persistence/Configurations/TenantConfiguration.cs b/Infrastructure/Persistence/Configurations/TenantConfiguration.cs index 0748fea..b1b1af7 100644 --- a/Infrastructure/Persistence/Configurations/TenantConfiguration.cs +++ b/Infrastructure/Persistence/Configurations/TenantConfiguration.cs @@ -30,5 +30,6 @@ public void Configure(EntityTypeBuilder builder) .HasMaxLength(200) .IsRequired() .IsUnicode(); + } } \ No newline at end of file diff --git a/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs b/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs index 7c1f873..2d742b8 100644 --- a/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -87,6 +87,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsRevoked") .HasColumnType("bit"); + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + b.Property("Token") .IsRequired() .HasMaxLength(500) diff --git a/Infrastructure/Persistence/Repositories/TenantRepository.cs b/Infrastructure/Persistence/Repositories/TenantRepository.cs index babe646..db37c31 100644 --- a/Infrastructure/Persistence/Repositories/TenantRepository.cs +++ b/Infrastructure/Persistence/Repositories/TenantRepository.cs @@ -1,5 +1,7 @@ using Application.Features.Tenant.Interfaces; using Domain.Entities; +using Domain.Exceptions; +using Domain.Interfaces; using Microsoft.Extensions.Logging; namespace Infrastructure.Persistence.Repositories; @@ -7,10 +9,11 @@ namespace Infrastructure.Persistence.Repositories; public class TenantRepository:TenantRepositoryContract { private readonly AppDbContext _context; - - public TenantRepository(AppDbContext context) + private readonly TenantProviderContract _tenantProvider; + public TenantRepository(AppDbContext context, TenantProviderContract tenantProvider) { _context = context; + _tenantProvider = tenantProvider; } public async Task RegisterTenant(Tenant tenant) @@ -18,6 +21,26 @@ public async Task RegisterTenant(Tenant tenant) await _context .AddAsync(tenant); } + + public async Task GetCurrentTenantAsync() + { + var tenantId=_tenantProvider.GetTenantId(); + if (tenantId==Guid.Empty) + throw new UnauthorizedAccessException("TenantId not set."); + var tenant =_context + .Tenants + .FirstOrDefault(t => t.Id == tenantId); + if (tenant == null) + throw new NotFoundException("Tenant not found."); + return tenant; + } + + public async Task IsSubscriptionValidAsync() + { + var tenant = await GetCurrentTenantAsync(); + return tenant.IsSubscriptionValid(); + } + public async Task SaveAsync() { await _context diff --git a/api/Controllers/TenantController.cs b/api/Controllers/TenantController.cs index f6a67f0..f0a3b75 100644 --- a/api/Controllers/TenantController.cs +++ b/api/Controllers/TenantController.cs @@ -1,5 +1,6 @@ using Application.Features.Tenant.DTO_s; using Application.Features.Tenant.Services; +using Domain.Interfaces; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -9,10 +10,12 @@ namespace api.Controllers; public class TenantController : ControllerBase { private readonly TenantServiceContract _tenantService; + private readonly TenantProviderContract _tenantProvider; - public TenantController(TenantServiceContract tenantService) + public TenantController(TenantServiceContract tenantService, TenantProviderContract tenantProvider) { _tenantService = tenantService; + _tenantProvider = tenantProvider; } [HttpPost] @@ -23,4 +26,17 @@ public async Task RegisterTenant([FromBody] RegisterTenantDTO dto var result=await _tenantService.Register(dto); return Ok(result); } + + [HttpGet] + [Authorize] + public async Task GetTenantId() + { + var tenantId=_tenantProvider.GetTenantId(); + return Ok(new + { + tenantId = tenantId.ToString(), + isAuthenticated = User.Identity?.IsAuthenticated, + claims = User.Claims.Select(c => new { c.Type, c.Value }) + }); + } } \ No newline at end of file diff --git a/api/Middlewares/TenantResolutionMiddleware.cs b/api/Middlewares/TenantResolutionMiddleware.cs new file mode 100644 index 0000000..af25179 --- /dev/null +++ b/api/Middlewares/TenantResolutionMiddleware.cs @@ -0,0 +1,65 @@ +using Application.Features.Tenant.Interfaces; +using Domain.Interfaces; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; + +namespace api.Middlewares; + +public class TenantResolutionMiddleware +{ + private readonly RequestDelegate _next; + + public TenantResolutionMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync( + HttpContext context, + TenantProviderContract tenantProvider, + TenantRepositoryContract tenantRepository) + { + var path = context.Request.Path.Value?.ToLower(); + if (string.IsNullOrEmpty(path) || + path.StartsWith("/swagger") || + path.StartsWith("/health") || + path.StartsWith("/api/") || + path == "/") + { + await _next(context); + return; + } + Guid tenantId = Guid.Empty; + + // روش ۳: از JWT claim (برای درخواست‌های احراز هویت شده) + if (context.User.Identity?.IsAuthenticated == true) + { + var claim = context.User.FindFirst("TenantId")?.Value; + Guid.TryParse(claim, out tenantId); + } + + // اگر JWT وجود نداشت، می‌توانید از روش‌های دیگر (هدر یا ساب‌دامین) استفاده کنید + // که خودتان کامنت کرده‌اید. + + if (tenantId == Guid.Empty) + { + context.Response.StatusCode = 400; + await context.Response.WriteAsync("TenantId is missing"); + return; + } + + tenantProvider.SetTenantId(tenantId); + + // بررسی اعتبار اشتراک Tenant + var isValid = await tenantRepository.IsSubscriptionValidAsync(); + if (!isValid) + { + context.Response.StatusCode = 403; + await context.Response.WriteAsync("Subscription is inactive or expired"); + return; + } + + await _next(context); + } +} \ No newline at end of file diff --git a/api/Program.cs b/api/Program.cs index 29541b4..bbf1c1b 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using System.Text; +using api.Middlewares; using Application; using Application.Common; using Domain.Entities; @@ -104,6 +105,7 @@ }); var app = builder.Build(); +app.UseMiddleware(); using (var scope = app.Services.CreateScope()) { var services = scope.ServiceProvider;