From 0f7b791b1764bb635085b89b5f5b7f5835376bfc Mon Sep 17 00:00:00 2001 From: Nima Haji Date: Sat, 23 May 2026 15:51:28 +0330 Subject: [PATCH] Flow Based on Subdomains #1 --- .../Auth/Interfaces/IUserRepository.cs | 1 + .../Features/Auth/Services/UserService.cs | 5 + .../Features/Tenant/Services/TenantService.cs | 6 +- .../Tenant/Services/TenantServiceContract.cs | 2 +- Domain/Exceptions/DupplicateUserExeption.cs | 8 + ...104349_Add(2,3)ValueToUserRole.Designer.cs | 321 ++++++++++++++++++ .../20260523104349_Add(2,3)ValueToUserRole.cs | 36 ++ .../Configurations/UserConfiguration.cs | 3 +- .../Migrations/AppDbContextModelSnapshot.cs | 2 +- .../Repositories/UserRepository.cs | 7 + api/Controllers/TenantController.cs | 2 +- api/Program.cs | 2 +- api/Properties/launchSettings.json | 10 + 13 files changed, 397 insertions(+), 8 deletions(-) create mode 100644 Domain/Exceptions/DupplicateUserExeption.cs create mode 100644 Infrastructure/Migrations/20260523104349_Add(2,3)ValueToUserRole.Designer.cs create mode 100644 Infrastructure/Migrations/20260523104349_Add(2,3)ValueToUserRole.cs diff --git a/Application/Features/Auth/Interfaces/IUserRepository.cs b/Application/Features/Auth/Interfaces/IUserRepository.cs index 37a16a5..04d31cb 100644 --- a/Application/Features/Auth/Interfaces/IUserRepository.cs +++ b/Application/Features/Auth/Interfaces/IUserRepository.cs @@ -8,6 +8,7 @@ public interface IUserRepository Task RegisterUserAsync(User user); Task GetUserByEmailAsync(string email); Task IsUserExistsByIdAsync(Guid userId); + Task IsUserExistsByEmailAsync(string email); Task SaveChangesAsync(); Task> GetAllUsersAsync(); Task GetUserByIdAsync(Guid userId); diff --git a/Application/Features/Auth/Services/UserService.cs b/Application/Features/Auth/Services/UserService.cs index da1725b..f4d1187 100644 --- a/Application/Features/Auth/Services/UserService.cs +++ b/Application/Features/Auth/Services/UserService.cs @@ -5,6 +5,7 @@ using Application.Features.Auth.Interfaces; using Domain.Entities; using Domain.Enums; +using Domain.Exceptions; namespace Application.Features.Auth.Services; @@ -34,6 +35,10 @@ public UserService(IUserRepository repository, IJwtTokenService jwtTokenGenerato public async Task RegisterUserAsync(RegisterUserRequestDto registerUserRequestDto) { var password = _passwordHasher.Hash(registerUserRequestDto.Password); + + if (await _userRepository.IsUserExistsByEmailAsync(registerUserRequestDto.Email)) + throw new DuplicateUserException("Email already exists"); + var user = new User(registerUserRequestDto.FullName, registerUserRequestDto.Email, registerUserRequestDto.PhoneNumber, password); await _userRepository.RegisterUserAsync(user); diff --git a/Application/Features/Tenant/Services/TenantService.cs b/Application/Features/Tenant/Services/TenantService.cs index c3bd366..7816d40 100644 --- a/Application/Features/Tenant/Services/TenantService.cs +++ b/Application/Features/Tenant/Services/TenantService.cs @@ -23,7 +23,7 @@ public TenantService(TenantRepositoryContract tenantRepository, IUSerContext use _userRepository = userRepository; } - public async Task Register(RegisterTenantDTO dto) + public async Task Register(RegisterTenantDTO dto) { //Todo: slug maker // Todo : Date manager @@ -41,7 +41,7 @@ public async Task Register(RegisterTenantDTO dto) user.AssignTenantToUser(tenant.Id); await _tenantRepository.SaveAsync(); - var tenantId = await _tenantRepository.GetCurrentTenantAsync(); - return $"{tenant.Name} Registered belong to user {user.FullName} created"; + // var tenantId = await _tenantRepository.GetCurrentTenantAsync(); + return tenant.Id; } } \ No newline at end of file diff --git a/Application/Features/Tenant/Services/TenantServiceContract.cs b/Application/Features/Tenant/Services/TenantServiceContract.cs index 6ff6dac..6cfdd15 100644 --- a/Application/Features/Tenant/Services/TenantServiceContract.cs +++ b/Application/Features/Tenant/Services/TenantServiceContract.cs @@ -4,5 +4,5 @@ namespace Application.Features.Tenant.Services; public interface TenantServiceContract { - Task Register(RegisterTenantDTO dto); + Task Register(RegisterTenantDTO dto); } \ No newline at end of file diff --git a/Domain/Exceptions/DupplicateUserExeption.cs b/Domain/Exceptions/DupplicateUserExeption.cs new file mode 100644 index 0000000..717e629 --- /dev/null +++ b/Domain/Exceptions/DupplicateUserExeption.cs @@ -0,0 +1,8 @@ +namespace Domain.Exceptions; + +public class DuplicateUserException:Exception +{ + public DuplicateUserException(string message) :base(message) + { + } +} \ No newline at end of file diff --git a/Infrastructure/Migrations/20260523104349_Add(2,3)ValueToUserRole.Designer.cs b/Infrastructure/Migrations/20260523104349_Add(2,3)ValueToUserRole.Designer.cs new file mode 100644 index 0000000..a548ee3 --- /dev/null +++ b/Infrastructure/Migrations/20260523104349_Add(2,3)ValueToUserRole.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("20260523104349_Add(2,3)ValueToUserRole")] + partial class Add23ValueToUserRole + { + /// + 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,2,3)"); + }); + }); + + 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/20260523104349_Add(2,3)ValueToUserRole.cs b/Infrastructure/Migrations/20260523104349_Add(2,3)ValueToUserRole.cs new file mode 100644 index 0000000..fcd5899 --- /dev/null +++ b/Infrastructure/Migrations/20260523104349_Add(2,3)ValueToUserRole.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class Add23ValueToUserRole : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropCheckConstraint( + name: "CK_Role_Valid_Values", + table: "Users"); + + migrationBuilder.AddCheckConstraint( + name: "CK_Role_Valid_Values", + table: "Users", + sql: "[Role] IN (0,1,2,3)"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropCheckConstraint( + name: "CK_Role_Valid_Values", + table: "Users"); + + migrationBuilder.AddCheckConstraint( + name: "CK_Role_Valid_Values", + table: "Users", + sql: "[Role] IN (0, 1)"); + } + } +} diff --git a/Infrastructure/Persistence/Configurations/UserConfiguration.cs b/Infrastructure/Persistence/Configurations/UserConfiguration.cs index 0787d87..e3a35b5 100644 --- a/Infrastructure/Persistence/Configurations/UserConfiguration.cs +++ b/Infrastructure/Persistence/Configurations/UserConfiguration.cs @@ -47,9 +47,10 @@ public void Configure(EntityTypeBuilder builder) .WithMany(t=>t.Users) .HasForeignKey(t => t.TenantId) .OnDelete(DeleteBehavior.Cascade); + builder.HasCheckConstraint( "CK_Role_Valid_Values", - "[Role] IN (0, 1)" + "[Role] IN (0,1,2,3)" ); } } \ No newline at end of file diff --git a/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs b/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs index 2d742b8..d8f7931 100644 --- a/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -213,7 +213,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users", null, t => { - t.HasCheckConstraint("CK_Role_Valid_Values", "[Role] IN (0, 1)"); + t.HasCheckConstraint("CK_Role_Valid_Values", "[Role] IN (0,1,2,3)"); }); }); diff --git a/Infrastructure/Persistence/Repositories/UserRepository.cs b/Infrastructure/Persistence/Repositories/UserRepository.cs index 0b13bad..ec5f3b4 100644 --- a/Infrastructure/Persistence/Repositories/UserRepository.cs +++ b/Infrastructure/Persistence/Repositories/UserRepository.cs @@ -33,6 +33,13 @@ public async Task IsUserExistsByIdAsync(Guid userId) return await _context.Users.AnyAsync(x => x.Id == userId); } + public async Task IsUserExistsByEmailAsync(string email) + { + return await _context + .Users + .AnyAsync(x => x.Email == email); + } + public async Task SaveChangesAsync() { await _context.SaveChangesAsync(); diff --git a/api/Controllers/TenantController.cs b/api/Controllers/TenantController.cs index f0a3b75..f35e8e9 100644 --- a/api/Controllers/TenantController.cs +++ b/api/Controllers/TenantController.cs @@ -19,7 +19,7 @@ public TenantController(TenantServiceContract tenantService, TenantProviderContr } [HttpPost] - [Authorize(Roles = "User,Admin")] + [Authorize(Roles = "SuperAdmin")] public async Task RegisterTenant([FromBody] RegisterTenantDTO dto) { // Todo: expire date base on plan diff --git a/api/Program.cs b/api/Program.cs index bbf1c1b..df852d1 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -105,7 +105,6 @@ }); var app = builder.Build(); -app.UseMiddleware(); using (var scope = app.Services.CreateScope()) { var services = scope.ServiceProvider; @@ -129,6 +128,7 @@ app.UseAuthentication(); app.UseAuthorization(); +app.UseMiddleware(); app.MapControllers(); diff --git a/api/Properties/launchSettings.json b/api/Properties/launchSettings.json index 9517289..79f215c 100644 --- a/api/Properties/launchSettings.json +++ b/api/Properties/launchSettings.json @@ -9,6 +9,16 @@ } }, "profiles": { + "Multi-Tenant": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://salon1.localhost:8596;http://salon2.localhost:8590", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, "http": { "commandName": "Project", "dotnetRunMessages": true,